自定义钟表盘,这一篇就够了

作者: 圆号本昊 | 来源:发表于2019-06-21 16:07 被阅读8次

    关于本文:本文原先在我的 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 地址:
    https://github.com/FishInWater-1999/android_view_user_defined_first.git

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

    祝大家编程愉快!

    相关文章

      网友评论

        本文标题:自定义钟表盘,这一篇就够了

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