自定义绘制钟表控件,这一篇就够了

3,003 阅读7分钟

关于本文:本文原先在我的 CSDN 博客发布(由图片水印能发现),整理以往博客过程中,发现当时总结的很仔细,所以将其迁移到这里,希望对大家在自定义 View 方面,能有所帮助 💗

引言

Android 自定义 View 应用非常广泛,最近逛 github 是偶然发现一个 Demo 感觉写的很好,我结合着这个项目的内容,给大家讲讲如何绘制时钟表盘,也算是加深下自己对自定义 View 的理解,涉及内容比较多,大家慢慢吸收。


最后效果:

开始之前,先让大家看看最后的效果

在这里插入图片描述


现在开始

让我们先搭建这个 View

  1. 首先,我们定义一个叫做 ClockView 的自定义 View ,让它继承自 View 类。
  2. 然后在 /res/values 目录下,建立 attrs 文件,在里面定义一些属性 大致如下
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="ClockView">
        <attr name="clock_backgroundColor" format="color" />
        <attr name="clock_lightColor" format="color" />
        <attr name="clock_darkColor" format="color" />
        <attr name="clock_textSize" format="dimension" />
    </declare-styleable>

</resources>

绘制外围小时圆环的准备工作

小时圆环组成分为外围的圆弧和四个小时数字,所以我们需要的东西很明确了。

  • 我们首先需要一个 Paint 对象,用于绘制文字,
  • 还需要另一个 Paint 对象,用于绘制圆环。

重写构造方法:

    /* 暗色,圆弧、刻度线、时针、渐变起始色 */
    private int mDarkColor;
    /* 小时文本字体大小 */
    private float mTextSize;
    private Paint mTextPaint;
    private Paint mCirclePaint;

    public ClockView(Context context) {
        super(context);
    }

    public ClockView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClockView, 0, 0);
        mDarkColor = ta.getColor(R.styleable.ClockView_clock_darkColor, Color.parseColor("#80ffffff"));
        mTextSize = ta.getDimension(R.styleable.ClockView_clock_textSize, DensityUtils.sp2px(context, 14));
        ta.recycle();
        // ANTI_ALIAS_FLAG 平滑绘制 不带磕磕绊绊
        mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setColor(mDarkColor);
        // 居中绘制文字
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setTextSize(mTextSize);

        mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mCirclePaint.setColor(mDarkColor);
        // 官方:使用此样式绘制的几何和文本将被描边,尊重绘画上与笔划相关的字段。
        // 说白了就是,不要吧这块扇形都上色,只是把最外层的边描下
        mCirclePaint.setStyle(Paint.Style.STROKE);
        mCirclePaint.setStrokeWidth(mCircleStrokeWidth);// 描边宽度

    }

别忘了重写 onMeasure 方法,测量控件大小 关于具体的测量方法,请参考自定义 View 的文章,无非就是对 MeasureSpec 的三种 mode 类型进行分类处理罢了。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getMeasureResult(widthMeasureSpec), getMeasureResult(heightMeasureSpec));
    }

    private int getMeasureResult(int measureSpec){
        int defaultSize = 800;
        int size = MeasureSpec.getSize(measureSpec);
        int mode = MeasureSpec.getMode(measureSpec);
        switch (mode){
            case MeasureSpec.UNSPECIFIED:
                return defaultSize;
            case MeasureSpec.AT_MOST:
                return Math.max(defaultSize, size);
            case MeasureSpec.EXACTLY:
                return size;
            default:
                return defaultSize;
        }
    }

开始绘制外围圆环

我们知道,对于绘制圆与椭圆这类图形,经常需要先用 RectF 设置一个边界矩形再进行绘制。如果是绘制文本则是 Rect 。

所以绘制外围圆环,首先要定义一个 RectF 变量用于绘制圆环,在定义一个 Rect 变量,用于绘制文字。

注 mCanvas 绘图类是 onDraw 中的参数,我们在 onDraw 中将它保存起来

   // 测量文字大小
    private Rect mTextRect = new Rect();
    private RectF mCircleRectF = new RectF();
    /* 小时圆圈线条宽度 */
    private float mCircleStrokeWidth = 4;

    /**
     * 画最外圈的时间 12、3、6、9 文本和4段弧线
     */
    private void drawOutSideArc() {
        String[] timeList = new String[]{"12", "3", "6", "9"};
        //计算数字的高度
        mTextPaint.getTextBounds(timeList[0], 0, timeList[0].length(), mTextRect);// 计算后放回一个矩形存在 mTextRect (涉及c++原生方法,会用就行不要深究)
        mCircleRectF.set(mTextRect.width() / 2 + mCircleStrokeWidth / 2,// 画一个外界小矩形,在矩形里画圆
                mTextRect.height() / 2 + mCircleStrokeWidth / 2,
                getWidth() - mTextRect.width() / 2 - mCircleStrokeWidth / 2,
                getHeight() - mTextRect.height() / 2 - mCircleStrokeWidth / 2);
        mCanvas.drawText(timeList[0], getWidth() / 2, mCircleRectF.top + mTextRect.height() / 2, mTextPaint);// 定点写字,通过 RectF 取得边界值,由于是顶点在右上方写字,所以要向下平移
        mCanvas.drawText(timeList[1], mCircleRectF.right, getHeight() / 2 + mTextRect.height() / 2, mTextPaint);
        mCanvas.drawText(timeList[2], getWidth() / 2, mCircleRectF.bottom + mTextRect.height() / 2, mTextPaint);
        mCanvas.drawText(timeList[3], mCircleRectF.left, getHeight() / 2 + mTextRect.height() / 2, mTextPaint);
        //画连接数字的4段弧线
        for (int i = 0; i < 4; i++) {
            // 画四个弧线 sweepAngle 弧线角度(扇形角度)
            mCanvas.drawArc(mCircleRectF, 5 + 90 * i, 80, false, mCirclePaint);
        }
    }

接着,我们重写 onDraw() 方法,并在 onDraw() 方法中,调用上面这个方法绘制圆环

    private Canvas mCanvas;
    @Override
    protected void onDraw(Canvas canvas) {
        mCanvas = canvas;
        drawOutSideArc();
    }

运行一下看看效果

我们看到 圆环和时间是出来了,但是这么是个椭圆呢,在仔细检查下我们的代码,在绘制过程中,控制我们圆环的 mCircleRectF 对象,是以整个控件大小为边界的,所以原因就很明了了,那么我们只要将 mCircleRectF 对象设置成一个正方形就行。
在这里插入图片描述

重写 onSizeChanged() 方法,保证绘制的是圆

包正绘图是圆形的前提是:

  1. 保证 RectF 切割的是正方形
  2. 那么保证 RextF 围成的是正方形,就要需要知道正方形四边距离控件边界的距离
  3. 也就是我们需要计算四个整型变量 :1.mPaddingLeft | 2.mPaddingTop | 3.mPaddingRight | 4.mPaddingBottom
    private float mRadius;
    /* 加一个默认的padding值,为了防止用camera旋转时钟时造成四周超出view大小 */
    private float mDefaultPadding;
    private float mPaddingLeft;
    private float mPaddingTop;
    private float mPaddingRight;
    private float mPaddingBottom;// 以上4值 均在 onSizechanged()中测量
    
    @Override
    protected void onSizeChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        mRadius = Math.min(l - getPaddingLeft() - getPaddingRight(),
                t - getPaddingTop() - getPaddingBottom()) / 2;// 各个指针长度
        mDefaultPadding = 0.12f * mRadius;
        mPaddingLeft = mDefaultPadding + l / 2 - mRadius + getPaddingLeft();// 钟离左边界距离
        mPaddingRight = mDefaultPadding + l / 2 - mRadius + getPaddingRight();// 钟离右边界距离
        mPaddingTop = mDefaultPadding + t / 2 - mRadius + getPaddingTop();// 钟离上边界距离
        mPaddingBottom = mDefaultPadding + t / 2 - mRadius + getPaddingBottom();// 钟离下边界距离
    }

对于圆的半径 mRadius ,我们就取控件长和宽中,短的那个的一半为它的值,除此之外还有一种情况,如果控件设置了 padding 那么,如果知识取长宽中短的,那么无论 padding 的值怎么设置,控件的半径始终都是保持长宽中短的那边的一半不变,这样取值使得 padding 失去了作用,也就显得不那么人性化了,所以真正的半径应该是长宽中短的那边,再减去两个 padding 的值,如下:

mRadius = Math.min(w - getPaddingLeft() - getPaddingRight(), h - getPaddingTop() - getPaddingBottom()) / 2;

那么这个 mDefaultPadding 又是什么作用呢?不如我们将其山区看看效果:

在这里插入图片描述
试想一下如果我们,没有这个默认值,那么用户在没有设置 padding 时,画出的圆弧必然和 View 的边界相切,圆弧相切到嗨没啥,关键是圆弧上显示时间的文字也得给截去了一半,但有了这个 mDefaultPadding 就不要害怕这个问题。

在这里插入图片描述


绘制刻度线的准备

开始绘制先前,我们先要准备下一些工具,

  1. 首先一个 Paint 对象是必不可少的,
  2. 然后为了方便用户使用,我们再定义一个颜色,暴露给予设置,
  3. 最后我们还需要一个 int 型的值,用来设定刻度线的长度
    /* 刻度线长度 */
    private float mScaleLength;
    /* 刻度线画笔 */
    private Paint mScaleLinePaint;
    /* 背景色 */
    private int mBackgroundColor;

    public ClockView(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClockView, 0, 0);
        mBackgroundColor = ta.getColor(R.styleable.ClockView_clock_backgroundColor, Color.parseColor("#237EAD"));
        mDarkColor = ta.getColor(R.styleable.ClockView_clock_darkColor, Color.parseColor("#80ffffff"));
        mTextSize = ta.getDimension(R.styleable.ClockView_clock_textSize, DensityUtils.sp2px(context, 14));
        ta.recycle();
        .
        .
        .
        mScaleLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mScaleLinePaint.setStyle(Paint.Style.STROKE);
        mScaleLinePaint.setColor(mBackgroundColor);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        .
        .
        .
        mScaleLength = 0.12f * mRadius;// 根据比例确定刻度线长度
        mScaleLinePaint.setStrokeWidth(0.012f * mRadius);// 刻度圈的宽度
    }

开始绘制刻度线

绘制国晨反而很简单,对于我们来说 一小时 60min 一分钟 60s,最好的情况莫过于分为 360 份,但是这样一来,由于手机屏幕比较小会直接导致先太密集,密集到了变成圆地步:

在这里插入图片描述

所以这里,我们将 360 度,划分为 200份 ,

  1. 360/200 = 1.8f
  2. 绘制时,我们没绘制一条边 将 Canvas 角度旋转 1.8f
  3. 起点:每次我们都从画板顶部开始,下移一个 Padding 再加上 mTextRect 的高度,也就是点钟文字高度,之后再加上一个 刻度线长度由于将刻度线与圆弧分隔开来,防止它们粘在一起
  4. 终点:笔起点多一个 刻度线长度即可
    /**
     * 画一圈梯度渲染的亮暗色渐变圆弧,重绘时不断旋转,上面盖一圈背景色的刻度线
     */
    private void drawScaleLine() {
        mCanvas.save();
        // 画背景色刻度线
        for (int i = 0; i < 100; i++) {
            mCanvas.drawLine(getWidth() / 2, mPaddingTop + mScaleLength + mTextRect.height() / 2,
                    getWidth() / 2, mPaddingTop + 2 * mScaleLength + mTextRect.height() / 2, mScaleLinePaint);
            mCanvas.rotate(1.8f, getWidth() / 2, getHeight() / 2);
        }
        mCanvas.restore();
    }

大功告成

项目 Demo 地址: github.com/FishInWater…

如果有错欢迎在评论区指出,非常感谢~

祝大家编程愉快!