Android_自定义签到View

一个类似于进度和打卡进度的自定义view


如下图:

Android_自定义签到View_第1张图片

看GIF岂不是更好

Android_自定义签到View_第2张图片


这个view在现在的app中挺常见的,基本都是这个套路,


之前写过一个可以双向滑动的和这个view的类似,那个滑动的view处理的onTouch事件,以及判断了我们应该滑动哪个小球,有兴趣的可以看下之前的连接
双向滑动的SeekBarhttp://blog.csdn.net/givemeacondom/article/details/52397589


这个就比较简单了,都是静态的绘制,唯一的交互就是UI中的签到按钮,点击一次通知自定义view绘制;


  • 透漏自定义属性
  • 确定view的size,以及处理测量模式
  • 根据确定的比例,计算我们自定义view中需要的坐标(背景,矩形区域,圆形的白色点,以及选中状态下的,对号的path坐标)
  • 然后就是绘制,透漏外界设置数据接口

上面就是实现的思路,我们一步步看下代码,最后会奉上源代码的下载链接;

这是自定义属性的抽取

 <declare-styleable name="SignInView">
        <attr name="sign_in_bg_clor" format="color" />
        <attr name="sign_in_pb_clor" format="color" />
        <attr name="sign_in_check_clor" format="color" />
        <attr name="sign_in_text_clor" format="color" />
        <attr name="sign_in_text_size" format="dimension" />
    declare-styleable>

自定义view中获取属性

    private void initAttrs(Context context, AttributeSet attrs, int defStyleAttr) {
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SignInView, defStyleAttr, R.style.def_sign_in_style);
        int indexCount = typedArray.getIndexCount();
        for (int i = 0; i < indexCount; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.SignInView_sign_in_bg_clor:
                    signInBgColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.SignInView_sign_in_pb_clor:
                    signInPbColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.SignInView_sign_in_check_clor:
                    signInCheckColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.SignInView_sign_in_text_clor:
                    singInTextColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.SignInView_sign_in_text_size:
                    singInTextSize = typedArray.getDimensionPixelSize(attr, 0);
                    break;
            }
        }
        typedArray.recycle();
    }

确定自定义view的大小

根据需求我们的这个view默认充满屏幕,所以只需要处理height的测量模式即可

  @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        Log.e("--TAG---", "onMeasure--->>");

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int measureHeight;

        if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) {
            measureHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEF_HEIGHT, getResources().getDisplayMetrics());
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(measureHeight, MeasureSpec.EXACTLY);
        }

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

计算我们需要绘制的内容坐标,这个其实是view的思路的最重要的,我们需要知道我们要绘制的东西在那个坐标上,大概就是初中坐标系的知识,回想一下,基本都能绘制出来,至于怎么绘制,就是谷歌提供给我们的API没什么技术难度,

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.e("--TAG---", "onSizeChanged--->>");
        viewPadding = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DEF_PADDING, getResources().getDisplayMetrics());
        int textMarginTop = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, TEXT_MARGIN_TOP, getResources().getDisplayMetrics());
    // 在回调中获取view的宽度和高度,并计算小球的半径
        viewWidth = w;
        viewHeight = h;
        signInBallRadio = (int) (viewHeight * SIGN_IN_BALL_SCALE / 2);
        signRectHeight = (int) (signInBallRadio * SIGN_BG_RECT_SCALE);
// 矩形的背景区域,
        signInBgRectF = new RectF(viewPadding + signInBallRadio, viewHeight * SECTION_SCALE - signInBallRadio - signRectHeight, viewWidth - viewPadding - signInBallRadio, viewHeight * SECTION_SCALE - signInBallRadio);

        circleY = (int) (signInBgRectF.top + signRectHeight / 2);
        descY = (int) (viewHeight * SECTION_SCALE) + signInBallRadio + textMarginTop;
        // 核心代码就一个方法
        calculateCirclePoints(viewData);

        progressRectF = signInPbRectFs.get(0);

    }

计算每个小球的位置

    private void calculateCirclePoints(List viewData) {
    //获取数据源中分开的份数,比如一周7天,中间的小段是不是有6段,所以是size-1
        if (null != viewData) {
            int intervalSize = viewData.size() - 1;
            // 计算一份的长度,view的宽度减去两边的padding,以及总共小球的半径的综合然后除以,一共多少份
            int onePiece = (viewWidth - 2 * viewPadding - signInBallRadio * 2 * viewData.size()) / intervalSize;
            // 计算每个小球的坐标,x 和y,
            for (int i = 0; i < viewData.size(); i++) {
            // 这里其实很简单,就是一个小小的规律,当前的小球坐标
            // 第一个小球  0个onePiece + 1个小球半径
            // 第二个小球  1个onePiece + 3个小球半径
            // 第三个小球  2个onePiece + 5个小球半径
            // ####........求第N个的距离......?是不是一个送分题
            // 第N个的公式: i * onePiece + ((i + 1) * 2 - 1)*R
                Point circleP = new Point(viewPadding + i * onePiece + ((i + 1) * 2 - 1) * signInBallRadio, circleY);
                Point descP = new Point((int) (viewPadding + i * onePiece + ((i + 1) * 2 - 1) * signInBallRadio - signInTextPaint.measureText(viewData.get(i)) / 2), descY);

// 计算对勾的路径
                Path signInPath = new Path();
                signInPath.moveTo(circleP.x - signInBallRadio / 2, circleP.y);
                signInPath.lineTo(circleP.x, circleP.y + signInBallRadio / 2);
                signInPath.lineTo(circleP.x + signInBallRadio / 2, circleP.y - signInBallRadio + signInBallRadio / 2);

                RectF signInPbRectF = new RectF(viewPadding + signInBallRadio, viewHeight * SECTION_SCALE - signInBallRadio - signRectHeight, circleP.x, viewHeight * SECTION_SCALE - signInBallRadio);
// 保存计算的坐标;
                signInPaths.add(signInPath);
                circlePoints.add(circleP);
                descPoints.add(descP);
                signInPbRectFs.add(signInPbRectF);
            }
        }
    }

脑细胞累死了,休息休息下,拿着计算好的坐标去canvas绘制吧

 /**
     * 属性动画的形式绘制进度条,暂时未启用,效果预览可以的
     *
     * @param canvas
     */
    private void drawSignInPbRectWithAnim(final Canvas canvas) {
        if (isNeedReturn()) {
            return;
        }
        final RectF targetRectF = signInPbRectFs.get(currentSignTag);
        RectF beforeRectF;
        if (currentSignTag >= 1) {
            beforeRectF = signInPbRectFs.get(currentSignTag - 1);
            progressRectF.left = beforeRectF.left;
        }
        progressRectF.right = targetRectF.right * persent;
        canvas.drawRect(progressRectF, signInPbPaint);
    }

// 绘制签到进度的方法,如果想看动画的可以自己把ondraw中的方法替换成上面的即可
    private void drawSignInPbRectNoAnim(final Canvas canvas) {
        if (isNeedReturn()) {
            return;
        }
        canvas.drawRect(signInPbRectFs.get(currentSignTag), signInPbPaint);
    }

    private boolean isNeedReturn() {
        return currentSignTag < 0 || currentSignTag >= viewData.size();
    }
// 绘制选中的签到状态下的圆圈以及白色的对勾路径
    private void drawSingInCheckCircle(Canvas canvas) {
        if (isNeedReturn()) {
            return;
        }
        for (int i = -1; i < currentSignTag; i++) {
            Point p = circlePoints.get(i + 1);
            Path path = signInPaths.get(i + 1);
            canvas.drawCircle(p.x, p.y, signInBallRadio, signInPbPaint);
            canvas.drawPath(path, signInCheckPaint);
        }
    }
// 绘制签到信息描述
    private void drawSignDesc(Canvas canvas) {
        for (int i = 0; i < viewData.size(); i++) {
            Point p = descPoints.get(i);
            canvas.drawText(viewData.get(i), p.x, p.y, signInTextPaint);
        }
    }
// 绘制白色的未签到背景
    private void drawSignInNormalCircle(Canvas canvas) {
        for (Point p : circlePoints) {
            canvas.drawCircle(p.x, p.y, signInBallRadio, signInBgPaint);
        }
    }

    private void drawSignInBgRect(Canvas canvas) {
        canvas.drawRect(signInBgRectF, signInBgPaint);
    }

重要的在这里,源代码下载地址

https://github.com/GuoFeilong/BehivorDemo

希望大家多多star谢谢!

你可能感兴趣的:(Android系列)