Android 自定义View——表盘实例

        早之前就准备写一些自定义的玩意儿玩玩,苦于一直忙于其他的就把这事搁置起来了。最近又把这些东西翻出来了,就想着记录下来,温故而知新。

        本篇文章就说一下如何一步步的实现一个时钟表盘。在网上有好的这种表盘的例子,我也大概的翻了翻,不过总有或多或少不满意的地方,最后还是决定自己手撸一个。

首先看一下效果图:

        Android 自定义View——表盘实例_第1张图片

简单可以将整个撸的过程分成4步:

1、撸表盘;

2、撸刻度;

3、撸数字;

4、撸指针;

首先创建一个类继承View,并初始化所必须的属性,其中初始化的这些属性也不是一开始就能全部想到的,一般都是现用现声明,在这里我都给贴出来了,后面就直接用了,代码中有注释,不多说。

public class ClockView extends View {

    private Paint mCirclePaint;//表盘画笔
    private Paint mLinePaint;//刻度画笔
    private Paint mTextPaint;//文字画笔
    private Paint mPointPaint;//指针画笔
    private int mCircleWidth;//表盘宽度
    private int mWidth,mHeight;//视图宽高
    private int mLineWidth;//刻度宽度
    private int mLineLeft, mLineTop, mLineRight, mLineBottom;//刻度线左、上、右、下位置
    private Calendar mCalendar;
    private int mHour;//时
    private int mMinute;//分
    private int mSecond;//秒

    public ClockView(Context context) {
        this(context, null);
    }

    public ClockView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initPaint();//初始化画笔
    }

    public ClockView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public ClockView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}


接下来 初始化画笔:

private void initPaint(){
        //表盘宽度
        mCircleWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,8,getResources().getDisplayMetrics());
        //刻度宽度
        mLineWidth =(int)TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,3,getResources().getDisplayMetrics());

        //初始化表盘画笔
        mCirclePaint = new Paint();//初始化画笔
        mCirclePaint.setAntiAlias(true);//抗锯齿
        mCirclePaint.setStyle(Paint.Style.FILL_AND_STROKE);//空心画笔
        mCirclePaint.setStrokeWidth(10.0f);//空心画笔宽度
        mCirclePaint.setColor(Color.rgb(160,82,45));//画笔颜色

        //初始化刻度画笔
        mLinePaint = new Paint();
        mLinePaint.setAntiAlias(true);
        mLinePaint.setStyle(Paint.Style.FILL);
        mLinePaint.setColor(Color.WHITE);

        //初始化数字画笔
        mTextPaint = new Paint();
        mTextPaint.setAntiAlias(true);
        mTextPaint.setStyle(Paint.Style.FILL);
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setStrokeWidth(1.0f);
        mTextPaint.setTextSize(48);

        //初始化指针画笔
        mPointPaint = new Paint();
        mPointPaint.setAntiAlias(true);
        mPointPaint.setFilterBitmap(true);
        mPointPaint.setStyle(Paint.Style.FILL);
    }

重写onMeasure()、onSizeChanged()方法
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        mLineLeft = mWidth/2 - mLineWidth/2;
        mLineRight = mWidth/2 + mLineWidth/2;
        mLineTop = mCircleWidth;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(onMeasureSpec(widthMeasureSpec), onMeasureSpec(heightMeasureSpec));
    }

    private int onMeasureSpec(int measureSpec){
        int resultSpec = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 300, getResources().getDisplayMetrics());
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);
        if (specMode == MeasureSpec.AT_MOST) {
            resultSpec = Math.min(resultSpec, specSize);
        } else if (specMode == MeasureSpec.EXACTLY) {
            resultSpec = specSize;
        }
        return resultSpec;
    }
onSizeChanged()回调方法获取View的最终大小(宽和高);为了更好的适应各种情况,我们还需要重写onMeasure()方法测量View的大小,我们都知道View的大小不仅由其自身决定,同时还会受到父控件的影响,通常情况我们自定义这种控件都会将宽高设定相同值,如果我们将容器的宽高固定且值相等的话,不重写onMeasure()方法是没有任何问题的,但我们不能排除设置成wrap_content 和 match_parent 这些情况,这种情况下如果不重写onMeasure()方法测量View的大小的话,那我们定义的控件就会面目全非了,作为一个严谨的Android小屌丝,怎么能容忍这种情况的存在呢 。具体的onSizeChanged()、onMeasure()方法的使用讲解百度一搜一堆,不再赘述。


好了,准备工作做完了,下面开始一个一个撸那4个步骤了:

1、撸表盘

    /**
     * 绘制表盘
     * @param canvas
     */
    private void drawCircle(Canvas canvas){
        canvas.drawCircle(mWidth/2, mHeight/2, mWidth/2 - mCircleWidth, mCirclePaint);
    }
表盘,没错,就是一个圆,canvas.drawCircle(圆心横坐标, 圆心纵坐标, 半径, 画笔);如下图:

Android 自定义View——表盘实例_第2张图片

2、撸刻度

    /**
     * 绘制刻度
     * @param canvas
     */
    private void drawLines(Canvas canvas){
        for (int i = 0; i <= 360; i++) {
            if (i % 30 == 0) {
                mLineBottom = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics());
            }else{
                mLineBottom = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 12, getResources().getDisplayMetrics());
            }

            if (i % 6 == 0) {
                canvas.save();
                canvas.rotate(i, mWidth/2, mHeight/2);
                canvas.drawRect(mLineLeft, mLineTop, mLineRight, mLineBottom, mLinePaint);
                canvas.restore();
            }
        }
    }
这段代码也好理解,圆一周是360°,我们姑且将它分成360份,我们都知道,钟表每一分钟旋转的度数为6°,也就是每旋转6°画一个小刻度;每一个小时的旋转度数为30°,也就是每30°画一个大刻度,我们写了一个for循环,判断每到30的整倍数是就设置mLineBottom的值设置的大一点,每到6的整倍数时就将画布旋转对应的度数,注意,在我们旋转画布之前一定要保存画布当前的状态,也就是调用save()方法,保证之后的操作不会对之前的元素有影响,之后再恢复画布初始状态,也就是调用canvas.restore()方法。这段话看似不好理解,但只要你明白 我们所说的 平移(translate)、缩放(scale)、旋转(rotate)、错切(skew)的操作都是操作的画布(canvas)。比如我们在12点的位置画了一个刻度,然后我们进行旋转操作,以画布的中心为支点进行旋转,比如旋转了30°,这个时候,我们所画的12点的刻度就旋转到了1点的位置,这个时候我们调用save()方法,保存现在的状态,然后调用restore()方法恢复画布原来的状态,这个时候我们的画布上所画的刻度又成了12点的状态,但是我们刚才旋转30°到1点位置的那个状态还是存在的,这个时候我们就能看到在12点和1点的位置上都有了刻度,如此反复,就会将表盘刻度全部画满了,如下图:

Android 自定义View——表盘实例_第3张图片

3、撸数字

在网上看的表盘都是直接和画刻度一样,一系列的旋转,就把数字全都对应画上了,这样做确实很简单,在drawLines()这个方法中再加上几行代码就可以了

	    if(i != 0 && i % 30 == 0){
                canvas.save();
                canvas.rotate(i, mWidth/2, mHeight/2);
                canvas.drawText(i/30 + "", mLineLeft, mLineBottom + 20, mTextPaint);
                canvas.restore();
            }
但是这样做的话呈现的效果很不好,如下图:

Android 自定义View——表盘实例_第4张图片

暂且不管间距的问题,就单单看数字,下面的数字都是倒着的(从这一点也能看出我们刚才说的,旋转等操作操作的是画布),作为一个严谨的Android小屌丝来说,这种情况。。。。。。。

我们不用上面的方法,我们这样做:

    /**
     * 绘制时间数字
     * @param canvas
     */
    private void drawText(Canvas canvas){
        canvas.drawText("ⅩⅡ", mLineLeft - mLineWidth, mLineBottom + mCircleWidth, mTextPaint);
        canvas.drawText("Ⅰ", mWidth/2 + ((mWidth/2  - mCircleWidth - mLineBottom)* (float)Math.sin(Math.toRadians(30))), mHeight/2 - (mWidth/2 - mCircleWidth - mLineBottom)* (float)Math.cos(Math.toRadians(30)), mTextPaint);
        canvas.drawText("Ⅱ", mWidth/2 + ((mWidth/2  - mCircleWidth - mLineBottom)* (float)Math.sin(Math.toRadians(60))), mHeight/2 - (mWidth/2 - mCircleWidth - mLineBottom)* (float)Math.cos(Math.toRadians(60)), mTextPaint);
        canvas.drawText("Ⅲ", mWidth - mLineBottom - mCirclePaint.getStrokeWidth()- mCircleWidth, mHeight/2 + mLineWidth, mTextPaint);
        canvas.drawText("Ⅳ", mWidth/2 + ((mWidth/2  - mCircleWidth - mLineBottom)* (float)Math.sin(Math.toRadians(60))), mHeight/2 + (mWidth/2 - mCircleWidth - mLineBottom)* (float)Math.cos(Math.toRadians(60)), mTextPaint);
        canvas.drawText("Ⅴ", mWidth/2 + ((mWidth/2  - mCircleWidth - mLineBottom)* (float)Math.sin(Math.toRadians(30))), mHeight/2 + (mWidth/2 - mCircleWidth - mLineBottom)* (float)Math.cos(Math.toRadians(30)), mTextPaint);
        canvas.drawText("Ⅵ", mWidth/2, mHeight - mLineBottom - mCirclePaint.getStrokeWidth(), mTextPaint);
        canvas.drawText("Ⅶ", mWidth/2 - ((mWidth/2  - mCircleWidth - mLineBottom)* (float)Math.sin(Math.toRadians(30))), mHeight/2 + (mWidth/2 - mCircleWidth - mLineBottom)* (float)Math.cos(Math.toRadians(30)), mTextPaint);
        canvas.drawText("Ⅷ", mWidth/2 - ((mWidth/2  - mCircleWidth - mLineBottom)* (float)Math.sin(Math.toRadians(60))), mHeight/2 + (mWidth/2 - mCircleWidth - mLineBottom)* (float)Math.cos(Math.toRadians(60)), mTextPaint);
        canvas.drawText("Ⅸ", mLineBottom + mCirclePaint.getStrokeWidth(), mHeight/2 + mLineWidth, mTextPaint);
        canvas.drawText("Ⅹ", mWidth/2 - ((mWidth/2  - mCircleWidth - mLineBottom)* (float)Math.sin(Math.toRadians(60))), mHeight/2 - (mWidth/2 - mCircleWidth - mLineBottom)* (float)Math.cos(Math.toRadians(60)), mTextPaint);
        canvas.drawText("ⅩⅠ", mWidth/2 - ((mWidth/2  - mCircleWidth - mLineBottom)* (float)Math.sin(Math.toRadians(30))), mHeight/2 - (mWidth/2 - mCircleWidth - mLineBottom)* (float)Math.cos(Math.toRadians(30)), mTextPaint);
    }
数字为了好看,用的罗马数字。代码看似挺长挺乱,但是仔细一看,这不就是正余玄函数吗,没错,就是简单的正余玄函数,没啥好说的,看效果:

Android 自定义View——表盘实例_第5张图片

怎么样,B格瞬间上去了吧,哈哈
好了,最后一步:

4、撸指针

    /**
     * 绘制指针
     */
    private void drawPoint(Canvas canvas){
        mCalendar = Calendar.getInstance();
        mHour = mCalendar.get(Calendar.HOUR_OF_DAY);
        mMinute = mCalendar.get(Calendar.MINUTE);
        mSecond = mCalendar.get(Calendar.SECOND);

        //绘制时针
        mPointPaint.setColor(Color.rgb(222,184,135));
        mPointPaint.setStrokeWidth(12.0f);
        canvas.save();
        canvas.rotate(mHour * 30 + mMinute * 6 / 12, mWidth/2, mWidth/2);
        canvas.drawLine(mWidth/2, mHeight/2, mWidth/2 , mWidth/4, mPointPaint);
        canvas.drawLine(mWidth/2, mHeight/2, mWidth/2 , mWidth/2 + mWidth/10, mPointPaint);//为了美观
        canvas.restore();
        //绘制分针
        mPointPaint.setStrokeWidth(9.0f);
        canvas.save();
        canvas.rotate(mMinute * 6, mWidth/2, mWidth/2);
        canvas.drawLine(mWidth/2, mHeight/2, mWidth/2 , mWidth/6, mPointPaint);
        canvas.drawLine(mWidth/2, mHeight/2, mWidth/2 , mWidth/2 + mWidth/10, mPointPaint);//为了美观
        canvas.restore();
        //绘制秒针
        canvas.drawCircle(mWidth/2, mHeight/2, 14, mLinePaint);//绘制表针交点 为了美观
        mPointPaint.setColor(Color.WHITE);
        mPointPaint.setStrokeWidth(6.0f);
        canvas.save();
        canvas.rotate(mSecond * 6, mWidth/2, mWidth/2);
        canvas.drawLine(mWidth/2, mHeight/2, mWidth/2 , mHeight/8, mPointPaint);
        canvas.drawLine(mWidth/2, mHeight/2, mWidth/2 , mWidth/2 + mWidth/10, mPointPaint);//为了美观
        canvas.restore();

    }
如果明白了上面的那些代码的话,这段代码更没啥难度了,一系列的保存、旋转、恢复。还有就是为了美观,指针反方向也分别画了一段,指针的交点画了一个圆点,就是这样:

Android 自定义View——表盘实例_第6张图片

好了,以上就全部画完了,怎么样,挺好看的吧???但是好看不行啊,得动啊,我看网上看有好多都是通过Handler来操作的,其实大可不必,一行代码搞定:

invalidate();//重绘

或者:

postInvalidateDelayed(0);
本质都一样。

好了,搞定了,代码基本都贴上了,感兴趣的小伙伴可以自己手动撸一下,也可以下载代码





你可能感兴趣的:(Android)