一个仿时钟自定义View例子

原项目地址: https://github.com/chenzongwen/MiClockView
本篇主要是参照原项目记录自己动手实践的过程,需要看原作者项目的朋友请移步原项目地址
原项目的效果图:
在这里插入图片描述
自己实现的效果图:
在这里插入图片描述
开始编码流程。首先分析一下这个view的组成部分。有外圈时针圈,文字,内圈的秒针刻度,分针,时针,秒针,动画效果,秒针移动时刻度盘的渐变效果。所以暂定这个view需要完成的步骤如下:

  • 初始化;
  • 外圈时针圈,包括文字;
  • 秒针圈(刻度圈);
  • 秒针,分针,时针
  • 动画效果
  • 秒针移动的颜色渐变效果
  • 其他优化等

1.初始化:每个自定义都是要继承view或者viewGroup,实现它的基本方法。

public class ClockView extends View {
    public ClockView(Context context) {
        this(context, null, 0);
    }
    public ClockView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }
    public ClockView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }    
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //super.onMeasure(widthMeasureSpec, heightMeasureSpec);
         setMeasuredDimension(measureDimension(widthMeasureSpec), measureDimension(heightMeasureSpec));
    } 
     @Override
    protected void onDraw(Canvas canvas) {
        
    }
    private int measureDimension(int measureSpec) {
        int defaultSize = 800;
        int model = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        switch (model) {
            case MeasureSpec.EXACTLY:
                return size;
            case MeasureSpec.AT_MOST:
                return Math.min(size, defaultSize);
            case MeasureSpec.UNSPECIFIED:
            default:
                return defaultSize;
        }
    }
}

关于view测量方法的重写大多数情况下都是可以使用这套写法。先判断当前view的测量模式,
根据模式重新定义view的大小。只是为了简便使用完全可以固定一个大小设置。

 setMeasuredDimension(500,500);

当然使用默认实现也是没有任何问题。
2.绘制外圈,文字
时针圈其实就是四条弧度,每条弧度中间有一定的间隔,刚好容下一个文字。为了美观一点,这里定义每条弧跨越度数为80度,每条弧度留出10个弧度来放下文字。在onDraw 方法里:

    private void init() {
        //圆弧大小
        mRectf = new RectF();
        mRectf.set(200, 200, 500, 500);
        //画笔初始化
        mPaint = new Paint();
        mPaint.setColor(Color.parseColor("#ff22ff"));
        mPaint.setAntiAlias(true);
        mPaint.setStrokeWidth(4); 
        mPaint.setStyle(Paint.Style.STROKE);//设置绘制类型:填充/描边
    }
     @Override
    protected void onDraw(Canvas canvas) {
        //画弧     
        for (int i = 0; i < 4; i++) {
            canvas.drawArc(mRectf, 5 + i * 90, 80, false, mPaint);
        }
    }

上面的 mRectf就是一个RectF 对象,圈定了绘制弧度的大小,一个半径为150像素的圆。这里我先是简单的定义成一个固定值。可根据实际情况设置动态或者外部设置。之后运行查看一下效果先:(ps:记得在xml文件中引用先,不然什么也看不到)。
在这里插入图片描述
绘制文字同样是onDraw方法里面,但是换一只画笔(别忘记对画笔初始化):

    private void init() {
       ....
       //文字
        mTextPaint = new Paint();
        mTextPaint.setColor(Color.parseColor("#ff22ff"));
        mTextPaint.setAntiAlias(true);
        mTextPaint.setTextSize(24);
        mTextPaint.setTextAlign(Paint.Align.CENTER);
        mTextPaint.setStyle(Paint.Style.STROKE);//设置绘制类型:填充/描边
    }
     protected void onDraw(Canvas canvas) {
        //画弧     
        ....
        //绘制文字
        mTextPaint.getTextBounds("12", 0, 2, boundRf);
        int textH = boundRf.height() / 2;
        canvas.drawText("3", 500, 200 + 300 / 2, mTextPaint);
        canvas.drawText("6", 200 + 300 / 2, 500, mTextPaint);
        canvas.drawText("9", 200, 200 + 300 / 2, mTextPaint);
        canvas.drawText("12", 200 + 300 / 2, 200, mTextPaint);
    }

展示效果:
在这里插入图片描述
从效果上看:这个字没有在中间的位置,很影响美观,要分别对文字做偏移处理,往下边移动一点(画布中以左上角为坐标原点,所以在Y轴方向偏移都是作加法计算),具体就是这个文字高度的一半。可以通过使用画笔的APIgetTextBounds来获取绘制文字的边界值。具体代码:

        //绘制文字
        Rect boundRf = new Rect();
        mTextPaint.getTextBounds("12", 0, 2, boundRf);
        int textH = boundRf.height() / 2;
        canvas.drawText("3", 500, 200 + 300 / 2 + textH, mTextPaint);
        canvas.drawText("6", 200 + 300 / 2, 500 + textH, mTextPaint);
        canvas.drawText("9", 200, 200 + 300 / 2 + textH, mTextPaint);
        canvas.drawText("12", 200 + 300 / 2, 200 + textH, mTextPaint);

调整后的效果:
在这里插入图片描述
3.内圈(刻度圈)
由众多的线条围成的一个圆环。假设有60条弧度(对应60秒),那么一条弧度跨越的度数为6度。做法之一是根据弧度起始位置,移动画笔画弧。比如如0 到 6,7 到 12…,一直到360这样。另一种做法就是固定画笔位置,移动画布。每次画弧的位置都是同一个,但是每画完一次就旋转画布,最后恢复画布开始状态。

//刻度线
canvas.save();
 for (int i = 0; i < 60; i++) {
    //刻度线长度为20
    canvas.drawLine(200 + 300 / 2, 200 + 20, 200 + 300 / 2, 200 + 40, mRulingPaint);
    //圆心为中心点,旋转画布
    canvas.rotate(6f, 200 + 300 / 2, 200 + 300 / 2);
 }
 canvas.restore();

使用不一样的画笔别忘了初始化,在init方法添加:

        //刻度线
        mRulingPaint = new Paint();
        mRulingPaint.setColor(Color.parseColor("#FFA500"));
        mRulingPaint.setAntiAlias(true);
        mRulingPaint.setStrokeWidth(2);
        mRulingPaint.setStyle(Paint.Style.FILL);//设置绘制类型:填充/描边

注意使用画布旋转时,需要先保存当前画布状态,调用canvas.save();api,结束时恢复之前保存的状态调用canvas.restore();,并且这俩个api是成对出现的,如果缺少了则会抛出异常。
时针,分针,秒针
先把秒针绘制,先绘制在12点钟方向。在onDraw()方法添加:

        canvas.drawLine(200 + 300 / 2, 200 + 55, 200 + 300 / 2, 200 + 300 / 2, mSecondPaint);

秒针画笔初始化,init()添加:

        //秒针
        mSecondPaint = new Paint();
        mSecondPaint.setColor(Color.parseColor("#ff22ff"));
        mSecondPaint.setAntiAlias(true);
        mSecondPaint.setStrokeWidth(3);
        mSecondPaint.setStyle(Paint.Style.FILL);//设置绘制类型:填充/描边

添加了刻度以及秒针的效果:
在这里插入图片描述
4.动画效果
在没有看到源码之前,我猜想实现动画效果的方案可以是在内部维护一个Handler计时器(不一定适用Handler),每秒执行一次,每次重绘指针位置,重绘可以旋转画布或者改变指针的终点位置(起点在圆心)。这个可以根据上一次时间计算出下一秒的度数(位置),而且开始绘制时可以获取到当前的时间,计算出指针的位置并且绘制上去。看完源码后发现原作者并没有使用任何定时器实现,而是通过不断重新绘制(在onDraw结束调用invalidate()达到死循环的目的)实现的动画效果。实际上使用Handler也是要达到无限循环的效果,通过重绘还减少了维护Handler的过程。好东西当然要借鉴。

    /**
     * 获取当前时间
     */
    private void getCurrentTime() {
        Calendar calendar = Calendar.getInstance();
        float milliSecond = calendar.get(Calendar.MILLISECOND);
        float second = calendar.get(Calendar.SECOND) + milliSecond / 1000;
        float minute = calendar.get(Calendar.MINUTE) + second / 60;
        float hour = calendar.get(Calendar.HOUR) + minute / 60;
        // 当前时间,秒针所在的位置(以12点钟方向为起始,划过的角度)
        mSecondDegree = second / 60 * 360;
        // 分针划过角度
        mMinuteDegree = minute / 60 * 360;
        //时针划过角度
        mHourDegree = hour / 12 * 360;
    }
     @Override
    protected void onDraw(Canvas canvas) {
        //获取时间。
        getCurrentTime();
        .....
        //秒针
        canvas.save();
        canvas.rotate(mSecondDegree, 200 + 300 / 2, 200 + 300 / 2);
        canvas.drawLine(200 + 300 / 2, 200 + 55, 200 + 300 / 2, 200 + 300 / 2, mSecondPaint);
        canvas.restore();
        invalidate();
    }   

如果没有意外的话,此时秒针已经能够动起来了,而且每次开始运行定位都是当前时间。
5.渐变效果
理解了源码的实现后,我才能明白这个渐变效果是怎么实现的。首先使用到了SweepGradient。翻译大概就是扫描渐变,梯度渐变,呈圆环形状类似雷达那种。相同的渐变效果类还有LinearGradient – 线性渐变。顾名思义就是渐变呈线性,水平方向。不明白SweepGradient可以先看看这里面的介绍:
https://www.cnblogs.com/androidsuperman/p/5021872.html
https://www.jianshu.com/p/b517cb479b24
会对梯度渐变有一定了解。主要是这个例子能够很好的解释这个梯度渐变的效果。实际代码:

    //梯度扫描渐变
    private SweepGradient mSweepGradient;
    // 渐变矩阵,作用在SweepGradient
    private Matrix mGradientMatrix;
    //渐变的画笔
    private Paint mGradientPaint;
    private void init() {
         ....
        //渐变画笔。
        mGradientPaint = new Paint();
        mGradientPaint.setAntiAlias(true);
        mGradientPaint.setStrokeWidth(20);
        mGradientPaint.setStyle(Paint.Style.STROKE); //描边
        mGradientPaint.setColor(Color.parseColor("#ff22ff"));
        //颜色渐变,颜色选择使用白色加透明效果。(会产生一圈白色到白色半透明的效果)
        mGradientMatrix = new Matrix();
        mSweepGradient = new SweepGradient(200 + 300 / 2, 200 + 300 / 2,
                new int[]{Color.parseColor("#ffffff"), Color.parseColor("#80ffffff")}
                , new float[]{0.75f, 1f});
    }

SweepGradient的构造函数:

  • float cx :渐变中心x坐标
  • float cy :y坐标
  • int colors[] : 渐变颜色组
  • float positions[] : 表示对应颜色的相对位置。以0开始,1结束。可以为null,表示各种颜色均匀分布。

需要注意的是颜色数组长度必须>2,最后一个参数float数组的长度必须等于颜色数组的长度。
渐变的效果已经设置好,把它应用到画笔上。注意这个画笔一定要设置为描边,就是Paint.Style.STROKE,原因在下面代码注释有说明,自己在实践的过程中也被这个困扰了好久。应用渐变效果的代码(要放在invalidate()之前):

     protected void onDraw(Canvas canvas) {
        ....
        //渐变效果。 matrix 默认会从0度(对应到时钟就是三点钟方向)顺时针旋转开始颜色的渐变
        mSweepGradient.setLocalMatrix(mGradientMatrix);
        //渐变内容与设置了渐变的画笔有关,如果画出的是一个圆,那么作用范围就是一个圆;如果是一个正方形,那么作用范围就一个正方形。
        mGradientPaint.setShader(mSweepGradient);
        RectF gradiendRectf = new RectF();
        gradiendRectf.set(200 + 30, 200 + 30, 500 - 30, 500 - 30);
        canvas.drawArc(gradiendRectf, 0, 360, false, mGradientPaint);//这里是环状的弧
    }

以上效果图:
在这里插入图片描述
这时的运行效果肯定会让人惊讶:之前的秒针盘几乎都看不到了,外圈还有一圈白色。冷静分析一下,我这个绘制渐变的颜色是白色不透明到白色半透明,所以漏出那一点点的秒针刻度圈应该透明效果导致的。最终原因就是后绘制的白色渐变覆盖了先绘制的秒针刻度,只要调整一些位置就可以了。还有画布本身也是白色的,渐变效果不明显,还需要给画布添加一个背景颜色。在xml添加背景颜色或者使用画笔添加颜色都可以。这里也选择跟demo一样的颜色:
在这里插入图片描述
让渐变视觉效果更好一点,可以将秒针圈刻度的间隔密切一点。目前数量设置是60,调整到240,新的效果展示:
在这里插入图片描述
到这里这个时钟view已经成型了,剩下分针跟时针参考秒针的做法就好。最后的优化完善最后总结下这个view涉及到的知识点:

  • View的测量方法的重写;
  • 画笔,画布,弧,线,文字的基本使用,包括获取到被绘制文字的宽高等;
  • SweepGradient颜色渐变(最开始吸引我拿来练习的地方),这里有2点注意:1.渐变画笔style要设置为描边。2.渐变画笔宽度=刻度线宽度(为了美观);

以上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值