Android自定义View之时钟

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/z_Xiaozuo/article/details/82560260

 

效果(gif效果不是很好)

GitHub地址

https://github.com/zuohp/My_Clock.git

绘制思路

1. 绘制表盘和长短刻度

2. 绘制各个时间点的数字

3. 绘制时针,分针,秒针

4. 让各个指针转起来,根据时间计算角度

开始绘制

1. 定义参数(attrs文件)

    <declare-styleable name="Clock">
        <!--数值-->
        <attr name="mClockRingWidth" format="dimension"/>
        <attr name="mDefaultWidth" format="dimension"/>
        <attr name="mDefaultLength" format="dimension"/>
        <attr name="mSpecialWidth" format="dimension"/>
        <attr name="mSpecialLength" format="dimension"/>
        <attr name="mHWidth" format="dimension"/>
        <attr name="mMWidth" format="dimension"/>
        <attr name="mSWidth" format="dimension"/>
        <!--颜色-->
        <attr name="mCircleColor" format="color"/>
        <attr name="mHColor" format="color"/>
        <attr name="mMColor" format="color"/>
        <attr name="mSColor" format="color"/>
        <attr name="mNumColor" format="color"/>

    </declare-styleable>

2. 继承View,定义参数

//    圆形和刻度的画笔、指针的画笔、数字的画笔
    private Paint mCirclePaint,mPointerPaint,mNumPaint;

//    时钟的外环宽度、时钟的半径、默认刻度的宽度、默认刻度的长度
//    特殊刻度的宽度、特殊刻度的长度、时针的宽度、分针的宽度、秒针的宽度
    private float mClockRingWidth,mClockRadius,mDefaultWidth,mDefaultLength,
                mSpecialWidth,mSpecialLength,mHWidth,mMWidth,mSWidth;

//    圆形和刻度的颜色,时针的颜色,分针的颜色,秒针的颜色,数字的颜色
    private int mCircleColor,mHColor,mMColor,mSColor,mNumColor;

3. 初始化参数以及画笔

    public Clock(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        init(context,attrs);
        initPaint();
        Calendar mCalendar= Calendar.getInstance();
        //获取当前小时数
        int hours = mCalendar.get(Calendar.HOUR);
        //获取当前分钟数
        int minutes = mCalendar.get(Calendar.MINUTE);
        //获取当前秒数
        int seconds=mCalendar.get(Calendar.SECOND);
        setTime(hours,minutes,seconds);
        //开启定时
        start();
    }

    /**
     * 初始化自定义参数
     */
    private void init(Context context,AttributeSet attributeSet){
        TypedArray ta = context.obtainStyledAttributes(attributeSet, R.styleable.Clock);
        mClockRingWidth=ta.getDimension(R.styleable.Clock_mClockRingWidth,SizeUtils.dp2px(context,4));
        mDefaultWidth=ta.getDimension(R.styleable.Clock_mDefaultWidth,SizeUtils.dp2px(context,1));
        mDefaultLength=ta.getDimension(R.styleable.Clock_mDefaultLength,SizeUtils.dp2px(context,8));
        mSpecialWidth=ta.getDimension(R.styleable.Clock_mSpecialWidth,SizeUtils.dp2px(context,2));
        mSpecialLength=ta.getDimension(R.styleable.Clock_mSpecialLength,SizeUtils.dp2px(context,14));
        mHWidth=ta.getDimension(R.styleable.Clock_mHWidth,SizeUtils.dp2px(context,6));
        mMWidth=ta.getDimension(R.styleable.Clock_mMWidth,SizeUtils.dp2px(context,4));
        mSWidth=ta.getDimension(R.styleable.Clock_mSWidth,SizeUtils.dp2px(context,2));
        //颜色
        mCircleColor=ta.getColor(R.styleable.Clock_mCircleColor, Color.RED);
        mHColor=ta.getColor(R.styleable.Clock_mHColor, Color.BLACK);
        mMColor=ta.getColor(R.styleable.Clock_mMColor, Color.BLACK);
        mSColor=ta.getColor(R.styleable.Clock_mSColor, Color.RED);
        mNumColor=ta.getColor(R.styleable.Clock_mNumColor, Color.BLACK);
        //记得释放
        ta.recycle();

    }

    /**
     * 初始化画笔
     */
    private void initPaint() {
        //时钟的画笔
        mCirclePaint=new Paint();
        mCirclePaint.setAntiAlias(true);
        mCirclePaint.setStyle(Paint.Style.STROKE);
        //指针的画笔
        mPointerPaint=new Paint();
        mPointerPaint.setAntiAlias(true);
        mPointerPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPointerPaint.setStrokeCap(Paint.Cap.ROUND);
        //数字的画笔
        mNumPaint=new Paint();
        mNumPaint.setStyle(Paint.Style.FILL);
        mNumPaint.setTextSize(60);
        mNumPaint.setColor(mNumColor);
    }

4. 测量View并取值

在这里本想着使用 wrap_content 模式的时候抛异常提示一下呢,后来想了想没有做,因为时钟这个控件必须要给定一个数值的,哪怕是 match_parent 也行,等View测量完毕在 onSizeChanged 方法中取到view的实际宽高,然后进行一个包括半径的一些参数初始化。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = getMeasureSize(true, widthMeasureSpec);
        int height = getMeasureSize(false, heightMeasureSpec);
        setMeasuredDimension(width, height);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth=w;
        mHeight=h;
        mCenterX=w/2;
        mCenterY=h/2;
        mClockRadius= (float) ((float) (w/2)*0.8);
    }

    /**
     * 获取View尺寸
     *
     * @param isWidth 是否是width,不是的话,是height
     */
    private int getMeasureSize(boolean isWidth, int measureSpec) {

        int result = 0;

        int specSize = MeasureSpec.getSize(measureSpec);
        int specMode = MeasureSpec.getMode(measureSpec);

        switch (specMode) {
            case MeasureSpec.UNSPECIFIED:
                if (isWidth) {
                    result = getSuggestedMinimumWidth();
                } else {
                    result = getSuggestedMinimumHeight();
                }
                break;
            case MeasureSpec.AT_MOST:
                if (isWidth)
                    result = Math.min(specSize, mWidth);
                else
                    result = Math.min(specSize, mHeight);
                break;
            case MeasureSpec.EXACTLY:
                result = specSize;
                break;
        }
        return result;
    }

5.  onDraw 方法

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //先设置画布为view的中心点
        canvas.translate(mCenterX,mCenterY);
        //绘制外圆和刻度
        drawCircle(canvas);
        //绘制数字
        drawNums(canvas);
        //绘制时针,分针,秒针和中间小圆点
        drawPointer(canvas);
    }
  • 5.1  绘制外圆和刻度(分特殊刻度和普通刻度,颜色分别设置)

    /**
     * 绘制外圆和刻度
     * @param canvas
     */
    private void drawCircle(Canvas canvas) {
        mCirclePaint.setStrokeWidth(mClockRingWidth);
        mCirclePaint.setColor(mCircleColor);
        //先画出圆环
        canvas.drawCircle(0,0,mClockRadius,mCirclePaint);
        for (int i = 0; i < 60; i++) {
            if (i%5==0){//特殊的刻度
                mCirclePaint.setStrokeWidth(mSpecialWidth);
                mCirclePaint.setColor(mHColor);
                canvas.drawLine(0,-mClockRadius+mClockRingWidth/2,0,-mClockRadius+mSpecialLength,mCirclePaint);
            }else {//普通刻度
                mCirclePaint.setStrokeWidth(mDefaultWidth);
                mCirclePaint.setColor(mSColor);
                canvas.drawLine(0,-mClockRadius+mClockRingWidth/2,0,-mClockRadius+mDefaultLength,mCirclePaint);
            }
//            通过旋转画布的方式快速设置刻度
            canvas.rotate(6);
        }
    }

   此时的效果

    

  • 5.2 绘制数字

有了绘制刻度的经验,我们绘制数字也可以这么干,对,就是旋转画布,省时省力,不用计算每一个数字的位置,你会发现每个数字是从左上角开始绘制的,与我们想要的效果有些偏差,所以我们可以在文字外面包一个矩形,然后我们摆正矩形的位置即可,详情请看代码。(如有好的建议烦请告知)

从12点钟方向开始,每次旋转画布30度,绘制数字,绘制完之后你会发现有的数字是倒着的,这里需要我们特殊处理一下,详细请看代码。

    /**
     * 绘制文字
     * @param canvas
     */
    private void drawNums(Canvas canvas) {
        for (int i = 0; i < 12; i++) {
            canvas.save();
            if (i==0){ //绘制12点的数字
                Rect textBound = new Rect();
                canvas.translate(0, (-mClockRadius+mSpecialLength+mDefaultLength+mClockRingWidth));
                String text="12";
                mNumPaint.getTextBounds(text, 0, text.length(), textBound);
                canvas.drawText(text, -textBound.width()/2,
                        textBound.height() / 2, mNumPaint);
            }else { //绘制其他数字
                Rect textBound = new Rect();
                canvas.translate(0, (-mClockRadius+mSpecialLength+mDefaultLength+mClockRingWidth));
                String text=i+"";
                mNumPaint.getTextBounds(text, 0, text.length(), textBound);
                canvas.rotate(-i*30); //因画布被旋转了,所以要把画布正过来再绘制数字
                canvas.drawText(text, -textBound.width()/2,
                        textBound.height() / 2, mNumPaint);
            }
            canvas.restore();
            canvas.rotate(30);
        }
    }

  此时的效果

   

  • 5.3 绘制时针和分针和秒针

在这里我们只绘制时针分针和秒针,暂时不根据时间进行弧度计算,后期在启动定时器的时候会进行计算的,因为我们要实现手动设置时间,所以不可在绘制的时候把弧度计算死值,我们要动态更改。

    /**
     * 绘制指针,每次绘制完恢复画布状态,使用 save 和 restore 方法
     * 指针长短根据半径长度进行计算
     * @param canvas
     */
    private void drawPointer(Canvas canvas) {
        //时针
        canvas.save();
        mPointerPaint.setColor(mHColor);
        mPointerPaint.setStrokeWidth(mHWidth);
        canvas.rotate(mH, 0, 0);
        canvas.drawLine(0, -20, 0,
                (float) (mClockRadius*0.45), mPointerPaint);
        canvas.restore();

//        分针
        canvas.save();
        mPointerPaint.setColor(mMColor);
        mPointerPaint.setStrokeWidth(mMWidth);
        canvas.rotate(mM, 0, 0);
        canvas.drawLine(0, -20, 0,
                (float) (mClockRadius*0.6), mPointerPaint);
        canvas.restore();

        //秒针
        canvas.save();
        mPointerPaint.setColor(mSColor);
        mPointerPaint.setStrokeWidth(mSWidth);
        canvas.rotate(mS, 0, 0);
        canvas.drawLine(0, -40, 0,
                (float) (mClockRadius*0.75), mPointerPaint);
        canvas.restore();
        //最后绘制一个小圆点,要不然没效果
        mPointerPaint.setColor(mSColor);
        canvas.drawCircle(0,0,mHWidth/2,mPointerPaint);

    }

 此时的效果

6. 根据时间计算指针弧度,让指针动起来

我们知道秒针走一圈是60秒,而圆的一圈是360度,那么

秒针一秒钟旋转:360 / 60 = 6度

分针一秒钟旋转:360 / 60 / 60 = 0.1 度

时针一秒钟旋转:360 / (12*3600) = 1 / 120 = 0.0083度

    /**
     * 定时器
     */
    private Timer mTimer=new Timer();
    private TimerTask task = new TimerTask() {
        @Override
        public void run() {
            if (mS == 360) {
                mS = 0;
            }
            if (mM == 360){
                mM = 0;
            }
            if (mH == 360){
                mH = 0;
            }
            //具体计算
            mS = mS + 6;
            mM = mM + 0.1f;
            mH = mH + 1.0f/120;
            //子线程用postInvalidate
            postInvalidate();
        }
    };
  • 6.1 开启定时后时钟就动起来了

    /**
     *开启定时器
     */
    public void start() {
        mTimer.schedule(task,0,1000);
    }

7. 开始处理手动设置时间

(注:在此引用了 蛇发女妖 的简书里的解决方案,里面说的非常好)

蛇发女妖 的简书地址:http://www.jianshu.com/p/c2abd6226897

在这里的问题就是分针在30分的时候,时钟却还是在1点整,秒针都走了30多秒了,分针确还停在30分钟的位置上。

如图: 

蛇发女妖 的简书里的原话是这么说的:

我们知道30分30秒其实就是30.5分钟,而我们计算时仅仅只算了30分钟的角度,少了那0.5分钟。所以我们还是得把传入的秒转换为分钟,即 分钟= (分钟 + 秒 * 1.0f/60f) *6f;同理时针的角度和分针秒针都有关,我们得把传入的分和秒也都转换为小时再计算它的角度,即 小时 = (小时 + 分钟 * 1.0f/60f + 秒 * 1.0f/3600f)*30f;

8. 手动设置时间的最终解决方案

    public void setTime(int h, int m, int s) {
        if (h >= 24 || h < 0 || m >= 60 || m < 0 || s >= 60 || s < 0) {
            Toast.makeText(getContext(), "时间不正确", Toast.LENGTH_SHORT).show();
            return;
        }
        //需要以12点为准,所以统一减去180度
        if (h >= 12) {
            mH = (h + m * 1.0f/60f + s * 1.0f/3600f - 12)*30f-180;
        } else {
            mH = (h + m * 1.0f/60f + s * 1.0f/3600f)*30f-180;
        }
        mM = (m + s * 1.0f/60f) *6f-180;
        mS = s * 6f-180;
    }

9. 总结

自定义View总能让我们或多或少学到点东西,多动手多实践,共同进步!

祝:工作顺利!

猜你喜欢

转载自blog.csdn.net/z_Xiaozuo/article/details/82560260