美文网首页自定义View2021
[Android]朝花夕拾之仿MIUI时钟效果绘制

[Android]朝花夕拾之仿MIUI时钟效果绘制

作者: dafasoft | 来源:发表于2021-04-07 20:18 被阅读0次

    本文为原创文章,转载请注明出处,原创不易,且转且珍惜

    1. 前言

    几年前做过一个类似MIUI时钟的效果,逻辑比较简单,虽然MIUI经过几年的系统迭代,时钟早已不是这个效果,但当时做需求时涉及到一些canvas绘制的技巧,想来还是有些意思,并且这些思路如果最近把代码翻了出来,回顾一下当时的想法和策略。

    2.效果

    俗话说的好,没有图你说个XX,先把效果图发出来看一下:


    miui[00_00_03--00_00_23].gif

    3. 用到的知识点

    3.1. Canvas#saveLayer和Canvas#restore

    Canvas 在一般的情况下可以看作是一张画布,所有的绘图操作如drawBitmap, drawCircle都发生在这张画布上,这张画板还定义了一些属性比如Matrix,颜色等等。但是如果需要实现一些相对复杂的绘图操作,比如多层动画,地图(地图可以有多个地图层叠加而成,比如:政区层,道路层,兴趣点层)。Canvas提供了图层(Layer)支持,缺省情况可以看作是只有一个图层Layer。如果需要按层次来绘图,Android的Canvas可以使用SaveLayerXXX, Restore 来创建一些中间层,对于这些Layer是按照“栈结构“来管理的:

    1355906035_7646.png

    创建一个新的Layer到“栈”中,可以使用saveLayer, savaLayerAlpha, 从“栈”中推出一个Layer,可以使用restore,restoreToCount。但Layer入栈时,后续的DrawXXX操作都发生在这个Layer上,而Layer退栈时,就会把本层绘制的图像“绘制”到上层或是Canvas上,在复制Layer到Canvas上时,可以指定Layer的透明度(Layer),这是在创建Layer时指定的:public int saveLayerAlpha(RectF bounds, int alpha, int saveFlags)本例Layers 介绍了图层的基本用法:Canvas可以看做是由两个图层(Layer)构成的。

    3.2 Canvas转换

    Canvas的转换主要有旋转、缩放、扭曲、平移、裁剪等,本文主要用到的是旋转(rotate)

    4. 思路

    4.1 总体思路

    我们默认Canvas和系统坐标系是对应的,没有发生任何旋转缩放,因此,初始状态将每个元素绘制在View的12点钟方向,绘制每个元素时都要新建图层,待绘制完成后将图层旋转至当前时间所指示的方向即可。

    4.2 当前时间角度的计算

    一个圆的角度是360度,我们默认12点钟为0度,那么当前时针、分针、秒针所旋转的角度分别为:

    mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360 / 12);
    mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360 / 60);
    mSecondStartAngle = Math.round((Calendar.getInstance().get(Calendar.SECOND) + Calendar.getInstance().get(Calendar.MILLISECOND) / Constants.SECOND) * (360 / 60));
    

    4.3 秒针(三角)的初始化

    这里秒针的实现是使用的Canvas#drawPath,在drawPath之前,我们需要穿件一个Path使其成为一个封闭的三角形,Canvas提供了moveTo、lineTo、colse方法帮我们实现这个效果:

            //初始化三角, 该三角形为底边40, 高27的等腰三角形
            mTriangle = new Path();
            mTriangle.moveTo(mGraduationPoint.x , mGraduationPoint.y + 70);// 此点为多边形的起点
            mTriangle.lineTo(mGraduationPoint.x - 20, mGraduationPoint.y + 97);
            mTriangle.lineTo(mGraduationPoint.x + 20, mGraduationPoint.y + 97);
            mTriangle.close(); // 使这些点构成封闭的多边形
    

    初始化完成后,秒针针头指向12点钟方向

    4.4 当前时间的绘制

    这里用到的是图层的创建和保存,以及Canvs#save方法,首先绘制秒针和中间圆环:

            int layerCount = canvas.saveLayer(0 , 0 , canvas.getWidth() , canvas.getHeight() , mDefaultPaint , Canvas.ALL_SAVE_FLAG);
            Log.d("zyl", "sanjiaolayerCount = " + layerCount);
            // 将图层旋转至秒针所指的方向
            canvas.rotate(mClockAngle + mSecondStartAngle , mCenterPoint.x , mCenterPoint.y);
    
            //画三角
            canvas.drawPath(mTriangle, mPaint);
    
            //画中心的圆圈
            canvas.drawBitmap(mCircleBitmap , null , mDstCircleRect , mDefaultPaint);
    
            canvas.restoreToCount(layerCount); // 恢复图层
    

    注意,调用restoreToCount或restore后,会将图层恢复至save或saveLayer之前的状态

    秒针和中间圆环绘制完成后,绘制时针和分针,操作同上:

            //画时针
            layerCount = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mDefaultPaint, Canvas.ALL_SAVE_FLAG); //新建图层
            Log.d("zyl", "shizhenLayerCount = " + layerCount);
            canvas.rotate(mHourAngle , mCenterPoint.x , mCenterPoint.y);
            canvas.drawBitmap(mHourBitmap , null , mDstHourRect , mDefaultPaint);
    
            canvas.restoreToCount(layerCount);
    
            //画分针
            layerCount = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mDefaultPaint, Canvas.ALL_SAVE_FLAG);
            Log.d("zyl", "fenzhenlayerCount = " + layerCount);
            canvas.rotate(mMinuteAngle , mCenterPoint.x , mCenterPoint.y);
            canvas.drawBitmap(mMinuteBitmap , null , mDstMinuteRect , mDefaultPaint);
    
            canvas.restoreToCount(layerCount);
    

    4.5 周边刻度的绘制

    4.5.1 刻度绘制

    圆环周边的刻度共有180个,我们需要新建一个图层,旋转180次,每次旋转2度即可:

            Log.d("zyl", "fenzhenlayerCount = " + layerCount);
            canvas.rotate(mSecondAngle + mSecondStartAngle , mCenterPoint.x, mCenterPoint.y);
            for (int i = 0; i < GRADUATION_COUNT; i++) {
                canvas.drawLine(mGraduationPoint.x, mGraduationPoint.y + 5, mGraduationPoint.x, mGraduationPoint.y + GRADUATION_LENGTH, mGraduationPaint);
                canvas.rotate(-PER_GRADUATION_ANGLE, mCenterPoint.x, mCenterPoint.y);
            }
    
    4.5.2 拖尾效果

    刻度有一个从透明度255到透明度120的渐变拖尾效果,因此,我们需要逆时针绘制刻度,并且每次绘制时将透明度 减三,直到透明度到达120:

            //画刻度
            layerCount = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mDefaultPaint, Canvas.ALL_SAVE_FLAG);
            Log.d("zyl", "fenzhenlayerCount = " + layerCount);
            canvas.rotate(mSecondAngle + mSecondStartAngle , mCenterPoint.x, mCenterPoint.y);
            for (int i = 0; i < GRADUATION_COUNT; i++) {
                int alpha = 255 - i * 3;
                if (alpha > 120) {
                    mGraduationPaint.setAlpha(alpha);
                }
                canvas.drawLine(mGraduationPoint.x, mGraduationPoint.y + 5, mGraduationPoint.x, mGraduationPoint.y + GRADUATION_LENGTH, mGraduationPaint);
                canvas.rotate(-PER_GRADUATION_ANGLE, mCenterPoint.x, mCenterPoint.y);
            }
            canvas.restoreToCount(layerCount);
    

    5 动效

    注意看效果图,秒针的运动比较圆润丝滑,而刻度的运动时从上一个跳到下一个,有一种秒针指引刻度运动的感觉,因此我们需要定义两个动画,一个秒针动画,使用float值,一个刻度动画,使用int值,这两个动画选择其中一个监听动画变化即可

    public void startAnimation() {
            //三角刻度动画
            mClockAnimator = ValueAnimator.ofFloat(0 , GRADUATION_COUNT);
            mClockAnimator.setDuration(Constants.MINUTE);
            mClockAnimator.setInterpolator(new LinearInterpolator());
            mClockAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    mClockAngle = (float) valueAnimator.getAnimatedValue() * PER_GRADUATION_ANGLE;
    
                }
            });
            mClockAnimator.setRepeatCount(ValueAnimator.INFINITE);
    
            //圆圈刻度动画
            mSecondAnimator = ValueAnimator.ofInt(0 , GRADUATION_COUNT);
            mSecondAnimator.setDuration(Constants.MINUTE);
            mSecondAnimator.setInterpolator(new LinearInterpolator());
            mSecondAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    mSecondAngle = (int) valueAnimator.getAnimatedValue() * PER_GRADUATION_ANGLE;
                    mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360 / 12);
                    mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360 / 60);
                    Log.d("zyl", "second = " + Calendar.getInstance().get(Calendar.SECOND));
                    Log.d("zyl", "mMinuteAngle = " + mMinuteAngle);
                    invalidate();
                }
            });
            mSecondAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animator) {
                    mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360 / 12);
                    mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360 / 60);
                    mSecondStartAngle = Math.round((Calendar.getInstance().get(Calendar.SECOND) + Calendar.getInstance().get(Calendar.MILLISECOND) / Constants.SECOND) * (360 / 60));
                }
    
                @Override
                public void onAnimationEnd(Animator animator) {
    
                }
    
                @Override
                public void onAnimationCancel(Animator animator) {
    
                }
    
                @Override
                public void onAnimationRepeat(Animator animator) {
    
                }
            });
            mSecondAnimator.setRepeatCount(ValueAnimator.INFINITE);
    
            mSecondAnimator.start();
            mClockAnimator.start();
        }
    

    完整版代码:

    public class MIUIClock extends View {
    
        private Paint mPaint;
        private Context mContext;
        private Paint mDefaultPaint;
        private Paint mGraduationPaint;
        private Rect mContentRect;
        private Path mTriangle;
        private Point mGraduationPoint;
        private Point mCenterPoint;
        private Rect mDstCircleRect; //时钟中心圆圈所在位置
        private Rect mDstHourRect; //时针所在位置
        private Rect mDstMinuteRect; //分针所在位置
        private ValueAnimator mClockAnimator;
        private ValueAnimator mSecondAnimator;
        private float mSecondStartAngle; //圆环的起始角度
        private float mClockAngle; //三角指针角度
        private int mSecondAngle; //圆环角度
        private float mHourAngle; //时针角度
        private float mMinuteAngle; //分针角度
        private static final int GRADUATION_LENGTH = 50; //圆环刻度长度
        private static final int GRADUATION_COUNT = 180; //一圈圆环刻度的数量
        private static final int ROUND_ANGLE = 360; //圆一周的角度
        private static final int PER_GRADUATION_ANGLE = ROUND_ANGLE / GRADUATION_COUNT; //每个刻度的角度
        private Bitmap mCircleBitmap; //时钟中心的圆圈
        private Bitmap mHourBitmap; //时针
        private Bitmap mMinuteBitmap; //分针
    
        public MIUIClock(Context context) {
            super(context);
            init(context);
        }
    
        public MIUIClock(Context context, AttributeSet attrs) {
            super(context, attrs);
            init(context);
        }
    
        private void init(Context context) {
            mContext = context;
            mDefaultPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setColor(Color.WHITE);
            mPaint.setStrokeCap(Paint.Cap.ROUND);
            mPaint.setAlpha(120);
    
            mGraduationPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mGraduationPaint.setColor(Color.WHITE);
            mGraduationPaint.setStrokeWidth(4);
            mGraduationPaint.setStrokeCap(Paint.Cap.ROUND);
    
            mCircleBitmap = BitmapFactory.decodeResource(mContext.getResources() , R.mipmap.ic_circle);
            mHourBitmap = BitmapFactory.decodeResource(mContext.getResources() , R.mipmap.ic_hour);
            mMinuteBitmap = BitmapFactory.decodeResource(mContext.getResources() , R.mipmap.ic_minute);
        }
    
        @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            mContentRect = new Rect(0 , 0 , w, h); // 本View内容区域
            mGraduationPoint = new Point(w /2 , 0); // 圆圈刻度绘制的参照位置
            mCenterPoint = new Point(w /2 , h /2); // 本View中心点位置
            //初始化三角, 该三角形为底边40, 高27的等腰三角形
            mTriangle = new Path();
            mTriangle.moveTo(mGraduationPoint.x , mGraduationPoint.y + 70);// 此点为多边形的起点
            mTriangle.lineTo(mGraduationPoint.x - 20, mGraduationPoint.y + 97);
            mTriangle.lineTo(mGraduationPoint.x + 20, mGraduationPoint.y + 97);
            mTriangle.close(); // 使这些点构成封闭的多边形
    
            //初始化circle所在位置, 将圆圈置于View 中心
            int circleWidth = mCircleBitmap.getWidth();
            int circleHeight = mCircleBitmap.getHeight();
            mDstCircleRect = new Rect(mCenterPoint.x - circleWidth /2 , mCenterPoint.y - circleHeight/2 ,
                    mCenterPoint.x + circleWidth /2 , mCenterPoint.y  + circleHeight /2);
    
            //初始化时针所在位置
            int hourWidth = mHourBitmap.getWidth();
            int hourHeight = mHourBitmap.getHeight();
            mDstHourRect = new Rect(mCenterPoint.x - hourWidth / 2 , mCenterPoint.y - hourHeight - circleHeight / 2 - 5,
                    mCenterPoint.x + hourWidth / 2, mCenterPoint.y - circleHeight / 2 - 5);
    
            //初始化分针所在位置
            int minuteWidth = mMinuteBitmap.getWidth();
            int minuteHeight = mMinuteBitmap.getHeight();
            mDstMinuteRect = new Rect(mCenterPoint.x - minuteWidth / 2 , mCenterPoint.y - minuteHeight - circleHeight / 2 - 5,
                    mCenterPoint.x + minuteWidth / 2 , mCenterPoint.y - circleHeight / 2 - 5);
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            int layerCount = canvas.saveLayer(0 , 0 , canvas.getWidth() , canvas.getHeight() , mDefaultPaint , Canvas.ALL_SAVE_FLAG);
            Log.d("zyl", "sanjiaolayerCount = " + layerCount);
            canvas.rotate(mClockAngle + mSecondStartAngle , mCenterPoint.x , mCenterPoint.y);
    
            //画三角
            canvas.drawPath(mTriangle, mPaint);
    
            //画中心的圆圈
            canvas.drawBitmap(mCircleBitmap , null , mDstCircleRect , mDefaultPaint);
    
            canvas.restoreToCount(layerCount);
    
            //画时针
            layerCount = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mDefaultPaint, Canvas.ALL_SAVE_FLAG); //新建图层
            Log.d("zyl", "shizhenLayerCount = " + layerCount);
            canvas.rotate(mHourAngle , mCenterPoint.x , mCenterPoint.y);
            canvas.drawBitmap(mHourBitmap , null , mDstHourRect , mDefaultPaint);
    
            canvas.restoreToCount(layerCount);
    
            //画分针
            layerCount = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mDefaultPaint, Canvas.ALL_SAVE_FLAG);
            Log.d("zyl", "fenzhenlayerCount = " + layerCount);
            canvas.rotate(mMinuteAngle , mCenterPoint.x , mCenterPoint.y);
            canvas.drawBitmap(mMinuteBitmap , null , mDstMinuteRect , mDefaultPaint);
    
            canvas.restoreToCount(layerCount);
    
            //画刻度
            layerCount = canvas.saveLayer(0, 0, canvas.getWidth(), canvas.getHeight(), mDefaultPaint, Canvas.ALL_SAVE_FLAG);
            Log.d("zyl", "fenzhenlayerCount = " + layerCount);
            canvas.rotate(mSecondAngle + mSecondStartAngle , mCenterPoint.x, mCenterPoint.y);
            for (int i = 0; i < GRADUATION_COUNT; i++) {
                int alpha = 255 - i * 3;
                if (alpha > 120) {
                    mGraduationPaint.setAlpha(alpha);
                }
                canvas.drawLine(mGraduationPoint.x, mGraduationPoint.y + 5, mGraduationPoint.x, mGraduationPoint.y + GRADUATION_LENGTH, mGraduationPaint);
                canvas.rotate(-PER_GRADUATION_ANGLE, mCenterPoint.x, mCenterPoint.y);
            }
            canvas.restoreToCount(layerCount);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            setMeasuredDimension(widthMeasureSpec , heightMeasureSpec);
        }
    
        public void startAnimation() {
            //三角刻度动画
            mClockAnimator = ValueAnimator.ofFloat(0 , GRADUATION_COUNT);
            mClockAnimator.setDuration(Constants.MINUTE);
            mClockAnimator.setInterpolator(new LinearInterpolator());
            mClockAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    mClockAngle = (float) valueAnimator.getAnimatedValue() * PER_GRADUATION_ANGLE;
    
                }
            });
            mClockAnimator.setRepeatCount(ValueAnimator.INFINITE);
    
            //圆圈刻度动画
            mSecondAnimator = ValueAnimator.ofInt(0 , GRADUATION_COUNT);
            mSecondAnimator.setDuration(Constants.MINUTE);
            mSecondAnimator.setInterpolator(new LinearInterpolator());
            mSecondAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator valueAnimator) {
                    mSecondAngle = (int) valueAnimator.getAnimatedValue() * PER_GRADUATION_ANGLE;
                    mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360 / 12);
                    mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360 / 60);
                    Log.d("zyl", "second = " + Calendar.getInstance().get(Calendar.SECOND));
                    Log.d("zyl", "mMinuteAngle = " + mMinuteAngle);
                    invalidate();
                }
            });
            mSecondAnimator.addListener(new Animator.AnimatorListener() {
                @Override
                public void onAnimationStart(Animator animator) {
                    mHourAngle = (Calendar.getInstance().get(Calendar.HOUR) + ((float)Calendar.getInstance().get(Calendar.MINUTE)) / 60) * (360 / 12);
                    mMinuteAngle = (Calendar.getInstance().get(Calendar.MINUTE) + ((float)Calendar.getInstance().get(Calendar.SECOND)) / 60) * (360 / 60);
                    mSecondStartAngle = Math.round((Calendar.getInstance().get(Calendar.SECOND) + Calendar.getInstance().get(Calendar.MILLISECOND) / Constants.SECOND) * (360 / 60));
                }
    
                @Override
                public void onAnimationEnd(Animator animator) {
    
                }
    
                @Override
                public void onAnimationCancel(Animator animator) {
    
                }
    
                @Override
                public void onAnimationRepeat(Animator animator) {
    
                }
            });
            mSecondAnimator.setRepeatCount(ValueAnimator.INFINITE);
    
            mSecondAnimator.start();
            mClockAnimator.start();
        }
    
        public void cancelAnimation() {
            if (mClockAnimator != null) {
                mClockAnimator.removeAllUpdateListeners();
                mClockAnimator.removeAllListeners();
                mClockAnimator.cancel();
                mClockAnimator = null;
            }
    
            if (mSecondAnimator != null) {
                mSecondAnimator.removeAllUpdateListeners();
                mSecondAnimator.removeAllListeners();
                mSecondAnimator.cancel();
                mSecondAnimator = null;
            }
        }
    }
    

    相关文章

      网友评论

        本文标题:[Android]朝花夕拾之仿MIUI时钟效果绘制

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