Android 自定义 View 时钟效果

1,510 阅读5分钟
原文链接: blog.csdn.net

今天继续聊自定义View,当然今天的这个比较麻烦一些,如果没有自定义View的经历,建议先看看自定义文字View与水印图片View

自定义文字View

自定义水印图片View

前面的自定义文字View,图片View都属于比较简单的自定义View,今天玩点有难度的,当然目的也是为了更加熟悉自定义View的各个步骤与坐标的计算、画笔的各种属性等。话不多聊  ,我们今天实现下如下的效果:

首先进行简单的分析:

钟表盘构成属性如下:

1、外部圆形边框(宽度、颜色)

2、内部一周的小黑点(宽度、颜色)

3、内部的1-12数字(字号、颜色)

4、时针分针和秒针(规格、颜色)

简单分析之后就能确定我们需要哪些的属性值,当然此处全部自定义属性,有些事没有必要的。

分析之后进行属性的自定义:

属性的自定义如下:

    <declare-styleable name="DemoClockView01">

        <attr name="borderwidth" format="dimension"/>
        <attr name="bordercolor" format="color"/>
        <attr name="pointcolor" format="color"/>
        <attr name="hourcolor" format="color"/>
        <attr name="minutecolor" format="color"/>
        <attr name="secondcolor" format="color"/>
        <attr name="numsize" format="dimension"/>
        <attr name="numcolor" format="color"/>

    </declare-styleable>
其中的borderwidth和bordercolor为边框的宽度和颜色,pointcolor为一圈的小黑点的颜色,hourcolor、minutecolor和secondcolor为时针、分针和秒针的颜色,numsize和numcolor为一圈数字的字号大小和颜色。

属性自定义完成之后再Java代码中拿到相关的属性值:

代码如下:

   TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.DemoClockView01, defStyleAttr, 0);
        for (int i = 0; i < array.getIndexCount(); i++) {//用getIndexCount   减少循环次数,提高性能   用.length也不能执行所有的case情况
            int attr = array.getIndex(i);
            switch (attr) {
                case R.styleable.DemoClockView01_borderwidth://边框宽度
                    mBorderWidth = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.DemoClockView01_bordercolor://边框颜色
                    mBorderColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.DemoClockView01_numcolor://数字颜色
                    mNumColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.DemoClockView01_numsize://数字字号
                    mNumSize = array.getDimensionPixelSize(attr, (int) TypedValue.applyDimension(
                            TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
                case R.styleable.DemoClockView01_pointcolor://周围小点颜色
                    mPointColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.DemoClockView01_hourcolor://时针颜色
                    mHourColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.DemoClockView01_minutecolor://分针颜色
                    mMinuteColor = array.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.DemoClockView01_secondcolor://秒针颜色
                    mSecondColor = array.getColor(attr, Color.BLACK);
                    break;
            }
        }
        array.recycle();
拿到相关的属性之后进行控件宽和高的测量:

代码如下:

   @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthMode == MeasureSpec.EXACTLY) {
            mWidth = widthSize;
        } else {
            int desire = getPaddingLeft() + getPaddingRight() + (int) TypedValue.applyDimension(
                    TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());
            mWidth = Math.min(desire, widthSize);
        }
        if (heightMode == MeasureSpec.EXACTLY) {
            mHeight = heightSize;
        } else {
            int desire = getPaddingTop() + getPaddingBottom() + (int) TypedValue.applyDimension(
                    TypedValue.COMPLEX_UNIT_DIP, 200, getResources().getDisplayMetrics());
            mHeight = Math.min(desire, heightSize);
        }
        mWidth = Math.min(mWidth, mHeight);//取最小值  防止绘制内容出错   以最小的边来为基准进行相关的绘制
        setMeasuredDimension(mWidth, mWidth);
    }
测量完成之后进行最后的绘制,从外到内一个一个来绘制:

1、首先是外部边框,圆形,计算圆心及半径,绘制如下:

 /**
         * 圆心的xy和圆环的宽度
         */
        final int cx, cy, width;
        cx = getPaddingLeft() + (getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) / 2;
        cy = getPaddingTop() + (getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) / 2;
        width = Math.min(getWidth() / 2, getHeight() / 2);//半径

        mPaint.setAntiAlias(true);//去除边缘锯齿,优化绘制效果
        mPaint.setColor(mBorderColor);
        if (mBorderColor == 0){
            mPaint.setColor(Color.BLACK);
        }
        canvas.drawCircle(cx, cy, width, mPaint);//外圆  红色

        mPaint.setColor(Color.WHITE);
        canvas.drawCircle(cx, cy, width - mBorderWidth, mPaint);//内圆 白色
此步骤完成之后在布局文件饮用控件即可看到外部的圆环效果,如下图:

2、周围小黑点的绘制:

 mPaint.setColor(mPointColor);
        if (mPointColor == 0) {
            mPaint.setColor(Color.BLACK);
        }
        canvas.save();//保存当前的状态
        for (int i = 0; i < 60; i++) {//总共60个点  所以绘制60次  //绘制一圈的小黑点
            if (i % 5 == 0) {
                canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
                        getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
                        cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
                        getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 15, getResources().getDisplayMetrics()), mPaint);
            } else {
                canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
                        getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
                        cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
                        getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10, getResources().getDisplayMetrics()), mPaint);
            }
            canvas.rotate(6, cx, cy);//360度  绘制60次   每次旋转6度
        }
        canvas.restore();//将canvas转回来
此步骤绘制完成之后即可看到圆环加上小圆点的效果,效果如下图:

3、数字的绘制(此处绘制的数字可能旋转了,因为绘制的时候是旋转画布绘制的,当然也可以计算每个数字点的坐标进行相关的绘制)

mPaint.setColor(mNumColor);
        if (mNumColor == 0) {
            mPaint.setColor(Color.BLACK);
        }
        mPaint.setTextSize(mNumSize);
        if (mNumSize == 0) {
            mPaint.setTextSize((int) TypedValue.applyDimension(
                    TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
        }
        mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 14, getResources().getDisplayMetrics()));
        String[] strs = new String[]{"12", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11",};//绘制数字1-12  (数字角度不对  可以进行相关的处理)
        Rect rect = new Rect();
        canvas.save();
        for (int i = 0; i < 12; i++) {//绘制12次  每次旋转30度
            mPaint.getTextBounds(strs[i], 0, strs[i].length(), rect);
            canvas.drawText(strs[i], cx - rect.width() / 2,
                    getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 18, getResources().getDisplayMetrics()) + rect.height(), mPaint);
            canvas.rotate(30, cx, cy);
        }
        canvas.restore();
此处绘制完成即可看到圆环、黑点和数字效果如下图:

4、时针、分针、秒针和圆心的绘制:

  mPaint.setColor(mHourColor);
        if (mHourColor == 0) {
            mPaint.setColor(Color.BLACK);
        }
        canvas.save();//绘制时针
        canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
                getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()) + rect.width(),
                cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, getResources().getDisplayMetrics()),
                cy, mPaint);
        canvas.restore();

        mPaint.setColor(mMinuteColor);
        if (mMinuteColor == 0) {
            mPaint.setColor(Color.BLACK);
        }
        canvas.save();//保存后面的状态
        canvas.rotate(60, cx, cy);
        canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
                getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, getResources().getDisplayMetrics()),
                cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics()),
                cy, mPaint);
        canvas.restore();//撤销保存的状态

        mPaint.setColor(mSecondColor);
        if (mSecondColor == 0) {
            mPaint.setColor(Color.BLACK);
        }
        canvas.save();
        mPaint.setColor(Color.RED);
        canvas.rotate(120, cx, cy);
        canvas.drawRect(cx - TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
                getPaddingTop() + mBorderWidth + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, getResources().getDisplayMetrics()),
                cx + TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()),
                cy, mPaint);
        canvas.restore();

        mPaint.setColor(Color.RED);
        canvas.drawCircle(cx, cy, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, getResources().getDisplayMetrics()), mPaint);//圆心,红色
此时完成即可看到完整的钟表盘效果。

如下图:

静态时钟功能到此实现。

下面呢的任务就是让其动起来:

           1)时间任务,每隔1s绘制界面。

在构造方法中启动时间任务,如下:

  Timer timer = new Timer("绘制线程");
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
             
            }
        }, 0, 1000);

2)计算每个时刻的对应的时针、分针、及秒针的角度。

在构造方法初始化时间如下(这里的时间类使用Calendar类):

 mCalendar = Calendar.getInstance();

在onDraw方法中进行时间及角度的计算:

   //关于当前时间的计算,默认为当前时间  当然是可以设置的

        int hour = mCalendar.get(Calendar.HOUR);//HOUR    进制为12小时   HOUR_OF_DAY  为24小时
        int minute = mCalendar.get(Calendar.MINUTE);//分钟
        int second = mCalendar.get(Calendar.SECOND) + 1;//秒数
        if (second == 60) {
            minute += 1;
            second = 0;
        }
        if (minute == 60){
            hour += 1;
            minute = 0;
        }
        if (hour == 12){
            hour = 0;
        }
        mCalendar.set(Calendar.SECOND, second);
        mCalendar.set(Calendar.MINUTE, minute);
        mCalendar.set(Calendar.HOUR, hour);
        float hourDegree = 360 * hour / 12 + 360 / 12 * minute / 60;//时针转动的角度   小时对应角度  加上  分钟对应角度   秒针忽略
        float minuteDegree = 360 * minute / 60 + 360 / 60 * second / 60;//分针转动的角度   分针对应角度  加上  秒数对应角度
        float secondDegree = 360 * second / 60;// 秒数对应角度

3)子线程、UI线程的切换绘制。

Handler进行子线程到子线程的转换。

最终的实现效果如下,也可以自己进行时间的设置:

  

到此效果实现。

Demo下载

Git地址:https://github.com/SnowJun/OwnView