自定义View 实现签到效果

作者: LiShang007 | 来源:发表于2019-06-25 15:51 被阅读23次

最近公司有一个需求是关于签到方面的效果,想着自己对自定义View这块不是很熟悉,所以就想着自己动手来实现下,以此来学习下自定义View。
首先来一张自己实现的效果图

img.jpg

GitHub
引用

implementation 'com.lishang:checkInProgress:1.0.1'
属性 类型 描述
text_date_size sp 日期文字大小
text_date_color color 日期文字颜色
radius dp 签到圆半径
circle_color color 圆的背景色
line_height dp 线高
line_color color 线的颜色
text_score_size sp 签到积分字体大小
text_score_color color 签到积分文字颜色
check_in_bitmap drawble 签到后的图片
check_in_color color 没有签到图片时,签到的颜色
check_in_hook_color color 没有签到图片时,签到内部勾的颜色
check_in_hook_size dp 没有签到图片时,签到内部勾的大小
circle_margin dp 签到圆顶部与日期字体距离
circle_stroke_width dp 签到圆描边宽度
circle_stroke_color color 签到圆边描颜色
check_in_progress_show boolean 是否显示签到进度
check_in_progress_color color 签到进度颜色
check_in_leak_show boolean 是否支持补签
circle_style enum 签到圆样式 fill 填充 stroke描边(circle_stroke_width、circle_stroke_color生效)
align enum 位置 top/center/bottom

使用

  <com.lishang.checkin.CheckInProgress
    android:id="@+id/checkIn_1"
    android:layout_width="match_parent"
    android:layout_height="100dp"
    android:layout_gravity="center_horizontal"
    android:layout_marginTop="10dp"
    android:background="#408ce2"
    app:align="center"
    app:check_in_color="#ceebfd"
    app:check_in_hook_color="#2d66d9"
    app:check_in_leak_show="true"
    app:circle_color="#2d66d9"
    app:circle_margin="5dp"
    app:circle_stroke_color="#ceebfd"
    app:circle_stroke_width="1dp"
    app:circle_style="stroke"
    app:line_color="#2d66d9"
    app:line_height="1dp"
    app:radius="10dp"
    app:text_date_color="#edffff"
    app:text_date_size="12sp"
    app:text_score_color="#9bccff"
    app:text_score_size="12sp" />


checkIn.setAdapter(new CheckInProgress.Adapter() {
        /**
         * 日期
         * @param position
         * @return
         */
        @Override
        public String getDateText(int position) {
            CheckIn in = list.get(position);
            return in.date;
        }

        /**
         * 积分
         * @param position
         * @return
         */
        @Override
        public String getScoreText(int position) {
            CheckIn in = list.get(position);
            return in.score;
        }

        /**
         * 是否签到
         * @param position
         * @return
         */
        @Override
        public boolean isCheckIn(int position) {
            CheckIn in = list.get(position);
            return in.isCheckIn;
        }

        /**
         * 数量
         * @return
         */
        @Override
        public int size() {
            return list.size();
        }

        /**
         * 是否支持补签
         * @param position
         * @return
         */
        @Override
        public boolean isLeakCheckIn(int position) {
            CheckIn in = list.get(position);

            return in.isLeakChekIn;
        }
    });

checkIn.setOnClickCheckInListener(new                       
  OnClickCheckInListener() {
        @Override
        public void OnClick(int position) {
            CheckIn checkIn = list1.get(position);
            if (checkIn.isCheckIn) {
                Toast.makeText(getApplicationContext(), "已签到", Toast.LENGTH_SHORT).show();
            } else {
                if (checkIn.isLeakChekIn) {
                    Toast.makeText(getApplicationContext(), "补卡", Toast.LENGTH_SHORT).show();
                    checkIn.isLeakChekIn = false;
                    checkIn.isCheckIn = true;

                    Log.e("CheckIn", Arrays.toString(list1.toArray()));

                    checkIn.getAdapter().notifyDataSetChanged();
                } else {
                    Toast.makeText(getApplicationContext(), "签到", Toast.LENGTH_SHORT).show();
                }
            }
        }
    });

代码简要概括

自定义View,主要需要实现两个方法:

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

主要用来测量当前控件的宽高widthMeasureSpec和heightMeasureSpec这两个值通常情况下都是由父视图经过计算后传递给子视图的,说明父视图会在一定程度上决定子视图的大小。

认识 MeasureSpec

在测量自定义view的大小之前,我们需要认识一个类MeasureSpec,它封装了父布局传递给子布局的布局要求,每个MeasureSpec代表了一组宽度和高度的要求 MeasureSpec由size和mode组成。

specMode一共有三种类型,如下所示:
1. EXACTLY

表示父视图希望子视图的大小应该是由specSize的值来决定的,系统默认会按照这个规则来设置子视图的大小,简单的说(当设置width或height为match_parent时,模式为EXACTLY,因为子view会占据剩余容器的空间,所以它大小是确定的)

2. AT_MOST

表示子视图最多只能是specSize中指定的大小。(当设置为wrap_content时,模式为AT_MOST, 表示子view的大小最多是多少,这样子view会根据这个上限来设置自己的尺寸)

3. UNSPECIFIED

表示开发人员可以将视图按照自己的意愿设置成任意的大小,没有任何限制。这种情况比较少见,不太会用到。

onDraw

用来绘制View需要显示的内容

下面来看代码

 @Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //计算元素位置
    onCalculation();

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
   
    //当View的高是wrap_content 时,高度设置为实际测量的高度
    if (heightMode == MeasureSpec.AT_MOST && verticalHeight != 0) {
        heightSize = verticalHeight;
    }

    setMeasuredDimension(widthSize, heightSize);
}

onCalculation()方法用了测量View上各个元素的位置,并保存下来

   /**
 * 先计算好画布上每个元素的位置
 */
private void onCalculation() {
    if (adapter == null) return;
    calculationDate();
    calculationScore();

    //元素垂直高度
    int total = datePointPool.get(0).y - getPaddingTop(); //日期的高度
    total += (circleMargin); // + 间距
    total += (radius) * 2; //+积分圆的直径
    verticalHeight = total;

}

/**
 * 日期元素位置
 */
private void calculationDate() {
    int left = getPaddingLeft();
    int right = getPaddingRight();
    int top = getPaddingTop();

    int width = getMeasuredWidth() - left - right;


    int margin = width / (adapter.size());

    //日期位置
    int cy = 0;
    for (int i = 0; i < adapter.size(); i++) {

        String str = adapter.getDateText(i);
        Rect rect = new Rect();
        datePaint.getTextBounds(str, 0, str.length(), rect);
        int y = top + rect.height();
        if (cy < y) {
            cy = y;
        }
    }

    for (int i = 0; i < adapter.size(); i++) {
        int cx = left + margin / 2 + i * margin;
        Point point = new Point(cx, cy);
        datePointPool.put(i, point);
    }
}

/**
 * 积分元素位置
 */
private void calculationScore() {

    int radiusPx = (radius);
    int left = datePointPool.get(0).x;
    int right = datePointPool.get(datePointPool.size() - 1).x;
    int top = datePointPool.get(0).y;

    int width = right - left;
    int cy = top + radiusPx + (circleMargin);

    int margin = width / (adapter.size() - 1);
    for (int i = 0; i < adapter.size(); i++) {

        int cx = left + i * margin;
        Point p = new Point(cx, cy);
        circlePointPool.put(i, p);

        scorePaint.setTextSize((textScoreSize));
        scorePaint.setStyle(Paint.Style.FILL);
        scorePaint.setColor(textScoreColor);
        scorePaint.setTextAlign(Paint.Align.CENTER);
        String str = "+" + adapter.getScoreText(i);
        if (adapter.isLeakCheckIn(i) && checkInLeakShow) {
            str = "补";
        }
        Rect rect = new Rect();
        scorePaint.getTextBounds(str, 0, str.length(), rect);

        Paint.FontMetricsInt fontMetrics = scorePaint.getFontMetricsInt();

        Point point = new Point(p.x, p.y + (fontMetrics.descent - fontMetrics.ascent) / 2 - fontMetrics.descent);
        scorePointPool.put(i, point);
    }

}

onDraw(Canvas canvas) 进行View内部元素绘制

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    drawDate(canvas);
    drawBgLine(canvas);
    drawScore(canvas);
}
 /**
 * 画日期
 *
 * @param canvas
 */
private void drawDate(Canvas canvas) {

    int margin = calculationAlign();
    if (datePointPool.size() != 0) {

        for (int i = 0; i < adapter.size(); i++) {

            String str = adapter.getDateText(i);

            datePaint.setColor(textDateColor);

            Point point = datePointPool.get(i);

            canvas.drawText(str, point.x, point.y + margin, datePaint);

        }

    }
}

private void drawBgLine(Canvas canvas) {
    int margin = calculationAlign();

    int startX = datePointPool.get(0).x;
    int startY = datePointPool.get(0).y + (radius) + (circleMargin) + margin;
    int stopX = datePointPool.get(datePointPool.size() - 1).x;
    int stopY = startY;
    canvas.drawLine(startX, startY, stopX, stopY, linePaint);


}

private void drawScore(Canvas canvas) {
    int radiusPx = (radius);
    int margin = calculationAlign();

    for (int i = 0; i < adapter.size(); i++) {
        Point p = circlePointPool.get(i);


        if (adapter.isCheckIn(i)) {

            if (checkInProgressShow && i + 1 < adapter.size()) {
                //进度
                scorePaint.setStyle(Paint.Style.FILL);
                scorePaint.setColor(checkInProgressColor);
                scorePaint.setStrokeWidth((lineHeight));

                Point p1 = circlePointPool.get(i + 1);
                canvas.drawLine(p.x, p.y + margin, p1.x, p1.y + margin, scorePaint);
            }

            if (checkIn != null) {
                float scale = radiusPx * 2.0f / checkIn.getWidth();
                Matrix matrix = new Matrix();
                matrix.postScale(scale, scale);
                canvas.save();
                canvas.translate(p.x - radiusPx, p.y + margin - radiusPx);
                canvas.drawBitmap(checkIn, matrix, scorePaint);
                canvas.restore();
            } else {
                scorePaint.setColor(checkInColor);
                scorePaint.setStyle(Paint.Style.FILL);
                canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint);

                //画勾
                scorePaint.setStyle(Paint.Style.FILL);
                scorePaint.setColor(checkInHookColor);
                scorePaint.setStrokeWidth((checkInHookSize));
                int startX = p.x - radiusPx / 4 * 3;
                int startY = p.y + margin;
                int stopX = p.x - radiusPx / 4;
                int stopY = p.y + margin + radiusPx / 2;
                canvas.drawLine(startX, startY, stopX, stopY, scorePaint);

                startX = stopX;
                startY = stopY;
                stopX = p.x + radiusPx / 4 * 3;
                stopY = p.y + margin - radiusPx / 2;
                canvas.drawLine(startX, startY, stopX, stopY, scorePaint);

                canvas.drawCircle(startX, startY, checkInHookSize / 2.0f, scorePaint);
            }


        } else {
            scorePaint.setStyle(Paint.Style.FILL);
            scorePaint.setColor(circleColor);
            canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint);

            if (circleStyle == Paint.Style.STROKE) {
                scorePaint.setColor(circleStrokeColor);
                scorePaint.setStrokeWidth((circleStrokeWidth));
                scorePaint.setStyle(Paint.Style.STROKE);
                canvas.drawCircle(p.x, p.y + margin, radiusPx, scorePaint);
            }

            scorePaint.setTextSize((textScoreSize));
            scorePaint.setStyle(Paint.Style.FILL);
            scorePaint.setColor(textScoreColor);
            scorePaint.setTextAlign(Paint.Align.CENTER);
            String str = "+" + adapter.getScoreText(i);
            if (adapter.isLeakCheckIn(i) && checkInLeakShow) {
                str = "补";
            }

            Point point = scorePointPool.get(i);

            canvas.drawText(str, point.x, point.y + margin, scorePaint);
        }

    }
}

相关文章

网友评论

    本文标题:自定义View 实现签到效果

    本文链接:https://www.haomeiwen.com/subject/eyntcctx.html