美文网首页
Android自定义控件:时钟

Android自定义控件:时钟

作者: 啊杰杰杰杰 | 来源:发表于2016-12-19 15:31 被阅读160次

    下面是项目在手机上运行的效果图

    GIF演示图

    效果图

    样式效果演示图

    效果图
    效果图
    效果图

    实现原理分析

    • 刻度线绘制:画一个刻度线很简单,就是canvas.drawLine,但是根据角度每30度绘制一个刻度线怎么实现呢,我们一开始想到的可能会是根据角度,利用三角函数等去计算每个刻度线的开始坐标和结束坐标,但这种方式未免过于复杂,稍有不慎就会计算错误。但是利用画布的旋转canvas.rotate就会非常的简单,刻度线只需按照12点钟方向绘制即可,每次绘制完一个刻度线,画布旋转30度,再按照12点钟方向绘制即可。
    • 指针绘制:同样也是通过canvas.drawLine绘制3个指针,为paint设置不同的属性实现时针,分针,秒针的显示样式,同理,如果我们根据角度去计算指针的坐标,那就很复杂,这里也是通过画布的旋转,那么旋转的角度怎么确定呢,就是根据当前时间去确定(具体算法后面代码中具体分析)。
    • 动态:为了实现时钟的动态转动,我们需要在onDraw中每一秒钟获取一次当前时间,然后计算3个指针的旋转角度,再绘制就行了。

    这样一分析,其实自定义时钟很简单,就是绘制圆,然后通过画布的旋转绘制刻度线和指针。

    具体实现过程

    1. 绘制圆

       //绘制圆
       canvas.drawCircle(centerX, centerY, radius, circlePaint);
      

      其中centerX和centerY为圆心,用当前控件的中心点即可,radius为圆的半径,采用当前控件宽高的最小值/2 即可,或者自行设置。

    2. 绘制刻度线

      12个刻度线,循环12次,每3个刻度线就是一刻钟的刻度线,可以设置不同的样式区分。然后根据12点钟方向绘制刻度线。

      开始x坐标:圆心x坐标;

      开始y坐标:圆心y坐标-半径+间隙;

      结束x坐标:圆心x坐标;

      结束y坐标:开始y坐标+刻度线长度;

      每绘制完一个刻度线后,画布就在之前的基础上旋转30度,继续绘制12点钟刻度线,这样,刻度线就基于旋转后的画布绘制,也就是斜着绘制了刻度线,很方便的实现了刻度线的绘制。

      这里给出主要的绘制代码,全部代码后面贴出

       //刻度线长度
       private final static int MARK_LENGTH = 20;
      
       //刻度线与圆的间隙
       private final static int MARK_GAP = 12;
      
       //绘制刻度线
       for (int i = 0; i < 12; i++) {
           if (i % 3 == 0) {//一刻钟
               markPaint.setColor(mQuarterMarkColor);
           } else {
               markPaint.setColor(mMinuteMarkColor);
           }
           canvas.drawLine(
                   centerX,
                   centerY - radius + MARK_GAP,
                   centerX,
                   centerY - radius + MARK_GAP + MARK_LENGTH,
                   markPaint);
           canvas.rotate(30, centerX, centerY);
       }
       canvas.save();
      
    3. 绘制指针

      绘制时针,分针,秒针,我们分别用3个canvas去绘制,最后再将这3个画布的bitmap绘制到控件的canvas中,为的是单独控制每个画布的旋转角度。

      首先分析时针的指针角度,钟一圈是12个小时,360度,那么每小时就是30度,假设当前时间的小时是h(12小时制),那么时针的旋转角度就是h*30,同刻度线一样,我们也不去计算该角度的指针的各种坐标,而是直接将时针的画布旋转h*30度,然后绘制12点钟方向的时针就行了。

      接着是分针角度,钟一圈是60分钟,360度,那么每分钟就是6度,假设当前时间的分钟是m,那么分针的旋转角度就是m*6

      最后是秒针角度,钟一圈是60秒,360度,那么每秒就是6度,假设当前时间的秒数是s,那么秒针的旋转角度就是s*6

      分析完了时针,分针,秒针的角度获取,那么之后就很简单了,在onDraw中,我们每过一秒获取一次当前时间的时分秒,按照上面的算法计算角度,然后旋转相应的画布,之后绘制相应的指针(当然要注意画布的清空和还原),那么一个随着时间的流逝而旋转的时钟就出来了。

      这里给出绘制时针的主要代码,其他两个指针是类似的,具体代码后面贴出

       @Override
       protected void onDraw(Canvas canvas) {
           Calendar calendar = Calendar.getInstance();
           int hour12 = calendar.get(Calendar.HOUR);
           int minute = calendar.get(Calendar.MINUTE);
           int second = calendar.get(Calendar.SECOND);
      
           //保存画布状态
           hourCanvas.save();
           //清空画布
           hourCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
           //旋转画布
           hourCanvas.rotate(hour12 * 30, centerX, centerY);
           //绘制12点钟方向的时针
           hourCanvas.drawLine(centerX, centerY,
                   centerX, centerY - hourLineLength, hourPaint);
           //重置画布状态,即撤销之前旋转的角度,回到未旋转之前的状态
           hourCanvas.restore();
      
           canvas.drawBitmap(hourBitmap, 0, 0, null); 
      
           //每隔1s重新绘制
           postInvalidateDelayed(1000);
       }
      

      但是我们会发现有一点小小的不足,秒针是会一秒一秒的转,但是时针和分针总是在整数位置,当过了60秒,分针才会跳到下一分钟,当过了60分钟,时针才会跳到下一个小时,我们平常看的时钟都是随着秒针的转动,分针和时针都是有一定的偏移量的,当然我们的时钟也要这么炫酷,那么如何计算呢?

      时针:前面说过,每小时时针旋转30度,假设当前时间的小时是h(12小时制),那么时针的旋转角度就是h*30。那么每分钟时针旋转多少度呢,答案是30/60=0.5度(每小时60分钟,每小时30度),所以时针的偏移量就是m*0.5,那么假设当前的时间是1:30,那么时针旋转的角度就是1*30+30*0.5,就是45度,改成变量公式就是h*30+m*0.5,那么修改下上面的代码

       hourCanvas.rotate(hour12 * 30 + minute * 0.5f, centerX, centerY);
      

      分针:假设当前时间的分钟是m,那么分针的旋转角度就是m*6,每秒钟分针旋转6/60(每分钟60秒,每分钟6度),所以分针的偏移量是s*0.1,那么分针画布旋转的的代码就是

       minuteCanvas.rotate(minute * 6 + second * 0.1f, centerX, centerY);
      

      秒针:秒针就按照每秒钟6度旋转

       secondCanvas.rotate(second * 6, centerX, centerY);
      

    总结

    经过上面的3个步骤,我们就绘制出了一个会慢慢移动的时钟了。

    完整的代码和项目大家可以到我的github中查看,里面有相关的使用方法,同时这个项目上传到了maven仓库,可以通过gradle直接使用

    compile 'com.don:clockviewlibrary:1.0.1'
    

    github地址:https://github.com/zhijieeeeee/ClockView

    完整代码

    public class ClockView extends View {
    
        //使用wrap_content时默认的尺寸
        private final static int DEFAULT_SIZE = 400;
    
        //刻度线宽度
        private final static int MARK_WIDTH = 8;
    
        //刻度线长度
        private final static int MARK_LENGTH = 20;
    
        //刻度线与圆的距离
        private final static int MARK_GAP = 12;
    
        //时针宽度
        private final static int HOUR_LINE_WIDTH = 10;
    
        //分针宽度
        private final static int MINUTE_LINE_WIDTH = 6;
    
        //秒针宽度
        private final static int SECOND_LINE_WIDTH = 4;
    
        //圆心坐标
        private int centerX;
        private int centerY;
    
        //圆半径
        private int radius;
    
        //圆的画笔
        private Paint circlePaint;
    
        //刻度线画笔
        private Paint markPaint;
    
        //时针画笔
        private Paint hourPaint;
    
        //分针画笔
        private Paint minutePaint;
    
        //秒针画笔
        private Paint secondPaint;
    
        //时针长度
        private int hourLineLength;
    
        //分针长度
        private int minuteLineLength;
    
        //秒针长度
        private int secondLineLength;
    
        private Bitmap hourBitmap;
        private Bitmap minuteBitmap;
        private Bitmap secondBitmap;
    
        private Canvas hourCanvas;
        private Canvas minuteCanvas;
        private Canvas secondCanvas;
    
        //圆的颜色
        private int mCircleColor = Color.WHITE;
        //时针的颜色
        private int mHourColor = Color.BLACK;
        //分针的颜色
        private int mMinuteColor = Color.BLACK;
        //秒针的颜色
        private int mSecondColor = Color.RED;
        //一刻钟刻度线的颜色
        private int mQuarterMarkColor = Color.parseColor("#B5B5B5");
        //分钟刻度线的颜色
        private int mMinuteMarkColor = Color.parseColor("#EBEBEB");
        //是否绘制3个指针的圆心
        private boolean isDrawCenterCircle = false;
    
        //获取时间监听
        private OnCurrentTimeListener onCurrentTimeListener;
    
        public void setOnCurrentTimeListener(OnCurrentTimeListener onCurrentTimeListener) {
            this.onCurrentTimeListener = onCurrentTimeListener;
        }
    
        public ClockView(Context context) {
            super(context);
            init();
        }
    
        public ClockView(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ClockView);
            mCircleColor = a.getColor(R.styleable.ClockView_circle_color, Color.WHITE);
            mHourColor = a.getColor(R.styleable.ClockView_hour_color, Color.BLACK);
            mMinuteColor = a.getColor(R.styleable.ClockView_minute_color, Color.BLACK);
            mSecondColor = a.getColor(R.styleable.ClockView_second_color, Color.RED);
            mQuarterMarkColor = a.getColor(R.styleable.ClockView_quarter_mark_color, Color.parseColor("#B5B5B5"));
            mMinuteMarkColor = a.getColor(R.styleable.ClockView_minute_mark_color, Color.parseColor("#EBEBEB"));
            isDrawCenterCircle = a.getBoolean(R.styleable.ClockView_draw_center_circle, false);
            a.recycle();
            init();
        }
    
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            reMeasure(widthMeasureSpec, heightMeasureSpec);
    
            int width = getMeasuredWidth();
            int height = getMeasuredHeight();
            centerX = width / 2 ;
            centerY = height / 2;
            radius = Math.min(width, height) / 2;
    
            hourLineLength = radius / 2;
            minuteLineLength = radius * 3 / 4;
            secondLineLength = radius * 3 / 4;
    
            //时针
            hourBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            hourCanvas = new Canvas(hourBitmap);
    
            //分针
            minuteBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            minuteCanvas = new Canvas(minuteBitmap);
    
            //秒针
            secondBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            secondCanvas = new Canvas(secondBitmap);
    
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            //绘制圆
            canvas.drawCircle(centerX, centerY, radius, circlePaint);
            //绘制刻度线
            for (int i = 0; i < 12; i++) {
                if (i % 3 == 0) {//一刻钟
                    markPaint.setColor(mQuarterMarkColor);
                } else {
                    markPaint.setColor(mMinuteMarkColor);
                }
                canvas.drawLine(
                        centerX,
                        centerY - radius + MARK_GAP,
                        centerX,
                        centerY - radius + MARK_GAP + MARK_LENGTH,
                        markPaint);
                canvas.rotate(30, centerX, centerY);
            }
            canvas.save();
    
            Calendar calendar = Calendar.getInstance();
            int hour12 = calendar.get(Calendar.HOUR);
            int minute = calendar.get(Calendar.MINUTE);
            int second = calendar.get(Calendar.SECOND);
    
            //(方案一)每过一小时(3600秒)时针添加30度,所以每秒时针添加(1/120)度
            //(方案二)每过一小时(60分钟)时针添加30度,所以每分钟时针添加(1/2)度
            hourCanvas.save();
            //清空画布
            hourCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
            hourCanvas.rotate(hour12 * 30 + minute * 0.5f, centerX, centerY);
            hourCanvas.drawLine(centerX, centerY,
                    centerX, centerY - hourLineLength, hourPaint);
            if (isDrawCenterCircle)//根据指针的颜色绘制圆心
                hourCanvas.drawCircle(centerX, centerY, 2 * HOUR_LINE_WIDTH, hourPaint);
            hourCanvas.restore();
    
            //每过一分钟(60秒)分针添加6度,所以每秒分针添加(1/10)度;当minute加1时,正好second是0
            minuteCanvas.save();
            //清空画布
            minuteCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
            minuteCanvas.rotate(minute * 6 + second * 0.1f, centerX, centerY);
            minuteCanvas.drawLine(centerX, centerY,
                    centerX, centerY - minuteLineLength, minutePaint);
            if (isDrawCenterCircle)//根据指针的颜色绘制圆心
                minuteCanvas.drawCircle(centerX, centerY, 2 * MINUTE_LINE_WIDTH, minutePaint);
            minuteCanvas.restore();
    
            //每过一秒旋转6度
            secondCanvas.save();
            //清空画布
            secondCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
            secondCanvas.rotate(second * 6, centerX, centerY);
            secondCanvas.drawLine(centerX, centerY,
                    centerX, centerY - secondLineLength, secondPaint);
            if (isDrawCenterCircle)//根据指针的颜色绘制圆心
                secondCanvas.drawCircle(centerX, centerY, 2 * SECOND_LINE_WIDTH, secondPaint);
            secondCanvas.restore();
    
            canvas.drawBitmap(hourBitmap, 0, 0, null);
            canvas.drawBitmap(minuteBitmap, 0, 0, null);
            canvas.drawBitmap(secondBitmap, 0, 0, null);
    
            //每隔1s重新绘制
            postInvalidateDelayed(1000);
    
            if (onCurrentTimeListener != null) {
                //小时采用24小时制返回
                int h = calendar.get(Calendar.HOUR_OF_DAY);
                String currentTime = intAdd0(h) + ":" + intAdd0(minute) + ":" + intAdd0(second);
                onCurrentTimeListener.currentTime(currentTime);
            }
        }
    
        /**
         * 初始化
         */
        private void init() {
            circlePaint = new Paint();
            circlePaint.setAntiAlias(true);
            circlePaint.setStyle(Paint.Style.FILL);
            circlePaint.setColor(mCircleColor);
    
            markPaint = new Paint();
            circlePaint.setAntiAlias(true);
            markPaint.setStyle(Paint.Style.FILL);
            markPaint.setStrokeCap(Paint.Cap.ROUND);
            markPaint.setStrokeWidth(MARK_WIDTH);
    
            hourPaint = new Paint();
            hourPaint.setAntiAlias(true);
            hourPaint.setColor(mHourColor);
            hourPaint.setStyle(Paint.Style.FILL);
            hourPaint.setStrokeCap(Paint.Cap.ROUND);
            hourPaint.setStrokeWidth(HOUR_LINE_WIDTH);
    
            minutePaint = new Paint();
            minutePaint.setAntiAlias(true);
            minutePaint.setColor(mMinuteColor);
            minutePaint.setStyle(Paint.Style.FILL);
            minutePaint.setStrokeCap(Paint.Cap.ROUND);
            minutePaint.setStrokeWidth(MINUTE_LINE_WIDTH);
    
            secondPaint = new Paint();
            secondPaint.setAntiAlias(true);
            secondPaint.setColor(mSecondColor);
            secondPaint.setStyle(Paint.Style.FILL);
            secondPaint.setStrokeCap(Paint.Cap.ROUND);
            secondPaint.setStrokeWidth(SECOND_LINE_WIDTH);
    
        }
    
        /**
         * 重新设置view尺寸
         */
        private void reMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int measureWidth = MeasureSpec.getSize(widthMeasureSpec);
            int measureHeight = MeasureSpec.getSize(heightMeasureSpec);
            int measureWidthMode = MeasureSpec.getMode(widthMeasureSpec);
            int measureHeightMode = MeasureSpec.getMode(heightMeasureSpec);
            if (measureWidthMode == MeasureSpec.AT_MOST
                    && measureHeightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(DEFAULT_SIZE, DEFAULT_SIZE);
            } else if (measureWidthMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(DEFAULT_SIZE, measureHeight);
            } else if (measureHeightMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(measureWidth, DEFAULT_SIZE);
            }
        }
    
        public interface OnCurrentTimeListener {
            void currentTime(String time);
        }
    
        /**
         * int小于10的添加0
         *
         * @param i
         * @return
         */
        private String intAdd0(int i) {
            DecimalFormat df = new DecimalFormat("00");
            if (i < 10) {
                return df.format(i);
            } else {
                return i + "";
            }
        }
    }
    

    自定义属性

    <declare-styleable name="ClockView">
        <attr name="circle_color" format="color" />
        <attr name="hour_color" format="color" />
        <attr name="minute_color" format="color" />
        <attr name="second_color" format="color" />
        <attr name="quarter_mark_color" format="color" />
        <attr name="minute_mark_color" format="color" />
        <attr name="draw_center_circle" format="boolean" />
    </declare-styleable>

    相关文章

      网友评论

          本文标题:Android自定义控件:时钟

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