自定义View-仿小米秒钟

效果图:

这里写图片描述

前言:

自定义view,是开发者必备的技能之一,也是找工作时面试官必问的题目。
有文章把自定义控件归纳为三种:
一、自绘控件,即继承View,在onDraw()内使用canvas绘制;
二、组合控件,即把常用的控件组合在一起,变成新的控件;
三、继承控件,即继承一个常用的View,修改、增加某个方法等。

组合控件最常用,自绘控件最体现水平。网上很多入门教程也很详细,本篇也会通过实例细讲绘制过程。总结下来就是更多的:“计算”(计算位置、计算距离等等),所以打开AndroidStudio的同时,也请准备好计算器。

正文

新建StopwatchView 继承View ,除了构造方法外,有两个方法必须得重写:测量尺寸onMeasure(xxx)和绘制图形onDraw(xxx)

public class StopwatchView extends View {

    public StopwatchView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
    }
}
一、onMeasure方法:

系统在绘制图形前,会先测量图形尺寸等相关参数,然后根据尺寸进行绘制。
在Demo中,我们的秒表始终保持圆形,但View的宽高设定可以有三种情况:match_parent、wrap_content、定值,所以我们重写onMeasure()来适配这三种情况

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //重新定义尺寸,保证为正方形
        int width = measuredDimension(widthMeasureSpec);
        int height = measuredDimension(heightMeasureSpec);
        mLen = Math.min(width, height);
         //小三角形指针端点到外圆之间的距离,用于计算三角形坐标[这里取整体宽度的1/16]
        mTriangleLen = (float) mLen / 16.0f;
        //提交设置 新的值
        setMeasuredDimension(mLen, mLen);
    }
    //适配不同尺寸
    private int measuredDimension(int measureSpec) {
        int defaultSize = 800; //默认大小
        int mode = MeasureSpec.getMode(measureSpec); //宽高度设定方式
        int size = MeasureSpec.getSize(measureSpec); //宽高度测量大小
        switch (mode) {
            case MeasureSpec.EXACTLY: //尺寸指定
                return size;
            case MeasureSpec.AT_MOST: //match_parent
                return size;
            case MeasureSpec.UNSPECIFIED: //wrap_content
                return defaultSize;
            default:
                return defaultSize;
        }
    }

说明:1、mLen 是最终外围宽高度。内部其他各元素的宽高、大小等都要以此为基准。简单来说,就是其他各元素都要按照mLen的值进行比例分配,不能设定死。否则可能出现不同尺寸下,内部元素比例不协调的情况
2、MeasureSpec 看起来比较陌生,其实内部只有三个常量、三个方法,如上面的代码所写,重写目的一是保证宽、高相同,二是在wrap_content时给一个默认值

二、StopwatchView构造方法:

在写onDraw()前,先提一下画笔。因为本例是一个动画效果,需要不停的重复执行ondraw(),所以一些不变的对象,如画笔等应该放在构造方法里。分析全局,需要四个画笔:三角形画笔指针(mTrianglePaint)、mLinePaint(mLinePaint)、文字画笔(mTextPaint)、内部圆形画笔(mInnerCirclePaint)

    public StopwatchView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        //三角形指针画笔
        mTrianglePaint = new Paint();
        mTrianglePaint.setColor(Color.WHITE);
        mTrianglePaint.setAntiAlias(true); //抗锯齿
        //刻度线的画笔
        mLinePaint = new Paint();
        mLinePaint.setAntiAlias(true);
        mLinePaint.setStrokeWidth(2); //设线宽
        //文字画笔
        mTextPaint = new Paint();
        mTextPaint.setTextAlign(Paint.Align.CENTER); //文字居中
        mTextPaint.setColor(Color.WHITE);
        mTextPaint.setAntiAlias(true);
        mTextPaint.setStrokeWidth(2);
        //内部圆形画笔
        mInnerCirclePaint = new Paint();
        mInnerCirclePaint.setColor(Color.WHITE);
        mInnerCirclePaint.setStyle(Paint.Style.STROKE); //无填充
        mInnerCirclePaint.setAntiAlias(true);
    }
三、onDraw方法:

本例主要的变量为秒表计时的毫秒值mMilliseconds
再根据mMilliseconds值计算出外圆三角形指针的角度outerAngle内部小圆的角度innerAngle,其他图形的绘制是根据这三个参数来进行;
另一个需要强调的是,参考小米秒钟,共设定240条刻度线,并预先设定好每个角度的值:

 float eachLineAngle = 360f / 240f; //两个刻度线之间的角度1.5° 共240条线 240间隔
1、calculateValue() 计算相关值
    //计算相关值【根据当前毫秒值,计算外指针角度和内圆指针角度】
    private void calculateValue() {
        //显示文字
        int hours = mMilliseconds / (1000 * 60 * 60);
        int minutes = (mMilliseconds % (1000 * 60 * 60)) / (1000 * 60);
        int seconds = (mMilliseconds - hours * (1000 * 60 * 60) - minutes * (1000 * 60)) / 1000;
        int milliSec = mMilliseconds % 1000 / 100;
        if (hours == 0) {
            mShowContent = toDoubleDigit(minutes) + ":" + toDoubleDigit(seconds) + "." + milliSec;
        } else {
            mShowContent = toDoubleDigit(hours) + ":" + toDoubleDigit(minutes) + ":" + toDoubleDigit(seconds) + "." + milliSec;
        }

        //外角度
        outerAngle = 360 * (mMilliseconds % 60000) / 60000;
        //内角度
        innerAngle = 360 * (mMilliseconds % 1000) / 1000;
    }
2、drawTriangle(Canvas canvas) 根据角度绘制三角形
    //根据角度绘制三角形
    private void drawTriangle(Canvas canvas) {
        canvas.save();
        //确定坐标
        canvas.translate(mLen / 2, mLen / 2);
        canvas.rotate(outerAngle);
        //画三角形
        Path p = new Path();
        //指针点
        p.moveTo(0, mLen / 2 - mTriangleLen);
        //左右侧点
        p.lineTo(0.5f * mTriangleLen, mLen / 2 - 0.134f * mTriangleLen);
        p.lineTo(-0.5f * mTriangleLen, mLen / 2 - 0.134f * mTriangleLen);
        p.close();
        canvas.drawPath(p, mTrianglePaint);
        canvas.restore();
    }

说明:mTriangleLen是之前计算的指针顶点到外边缘的距离。因为没有三角形的api,所以根据路径来绘制。其中:0.5f * mTriangleLen 和 mLen / 2 - 0.134f * mTriangleLen 分别表示以三角形指针另两点的x和y的距离[0.5=sin30°,0.134=(1-cos30°)]

3、drawLine(Canvas canvas) 绘制外部刻度线
 //绘制外部刻度线
    private void drawLine(Canvas canvas) {
        canvas.save();
        canvas.translate(mLen / 2, mLen / 2);
        int totalLines = (int) (360f / eachLineAngle); //240条线
        int lastLine = (int) (outerAngle / eachLineAngle);  //最亮的线条
        int firstLine = lastLine - ((int) (90 / eachLineAngle)); //最暗的一条
        boolean negativeFlag = false; //负数标志【即表示跨过了0起始坐标】
        if (firstLine < 0) {
            negativeFlag = true;
            firstLine = totalLines - Math.abs(firstLine);
        }
        int count = 0;
        for (int i = 0; i < totalLines; i++) {
            canvas.rotate(eachLineAngle);
            int color = 0;
            if (!negativeFlag) {
                //没有跨过起始点标志
                if (i >= firstLine && i <= lastLine && count < (totalLines / 4)) {
                    count++;
                    color = Color.argb(255 - ((totalLines / 4 - count) * 3), 255, 255, 255);
                } else {
                    color = Color.argb(255 - (int) (360f * 3 / (eachLineAngle * 4)), 255, 255, 255);
                }
            } else {
                //跨过起始点
                if (i >= 0 && i < lastLine) {
                    if (count == 0) {
                        count = totalLines / 4 - lastLine;
                    } else {
                        count++;
                    }
                    color = Color.argb(255 - ((totalLines / 4 - count) * 3), 255, 255, 255);
                } else if (mMilliseconds!=0&&i < totalLines && i >= firstLine) {  //mMilliseconds!=0 条件限制,目的是初始化时 都是灰色线条
                    Log.i("TAG6", "firstLine" + firstLine + " lastLine" + lastLine);
                    count++;
                    color = Color.argb(255 - ((totalLines / 4 - (i - firstLine)) * 3), 255, 255, 255);
                } else {
                    color = Color.argb(255 - (int) (360f * 3 / (eachLineAngle * 4)), 255, 255, 255);
                }
            }
            mLinePaint.setColor(color);
            //mTriangleLen/5距离 目的是为了三角形到线条之间保留的距离
            canvas.drawLine(0, (float) (mLen / 2 - (mTriangleLen+mTriangleLen/5)), 0, (float) (mLen / 2 - (2 * mTriangleLen+mTriangleLen/5)), mLinePaint);

        }
        canvas.restore();
    }

说明:绘制线条,先要计算总的线条数,然后for循环,循环中每次旋转eachLineAngle角度。同时要根据当前角度来设定画笔的颜色来达到渐变效果。因为有跨过0°和未跨过0°的情况,所以代码中分别对此做了处理。当然也可能有其它更好的计算方法。其中的有判断 mMilliseconds!=0情况,表示初始情况或重置情况下,颜色不做改变

4、drawText(Canvas canvas) 绘制文字
//绘制文字
    private void drawText(Canvas canvas) {
        canvas.save();
        canvas.translate(mLen / 2, mLen / 2);
        mTextPaint.setTextSize(mLen / 10);
        canvas.drawText(mShowContent, 0, 0, mTextPaint);
        canvas.restore();
    }
5、drawSecondHand(Canvas canvas) 根据角度绘制内部秒针
    //根据角度绘制内部秒针
    private void drawSecondHand(Canvas canvas) {
        canvas.save();
        canvas.translate(mLen / 2, (float) mLen * 3 / 4.0f - mLen / 16);
        canvas.drawCircle(0, 0, mLen / 12, mInnerCirclePaint);
        canvas.drawCircle(0, 0, mLen / 80, mInnerCirclePaint);
        canvas.rotate(innerAngle);
        canvas.drawLine(0, mLen / 80, 0, mLen / 14, mInnerCirclePaint);
        canvas.restore();
    }
四、增加对外交互的方法
    //开始
    public void start() {
        if (mTimer == null) {
            mTimer = new Timer();
            mTimer.schedule(new TimerTask() {
                @Override
                public void run() {
                    if (!isPause) {![这里写图片描述](https://img-blog.csdn.net/20180415133612672?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2Fyc29uNjYzMzAw/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
                        mMilliseconds += 50;
                        //工作线程中用postInvalidate(); UI线程用invalidate()
                        postInvalidate();
                    }
                }
            }, 50, 50);
        } else {
            resume();
        }
    }

    //暂停
    public void pause() {
        isPause = true;
    }

    //继续
    private void resume() {
        isPause = false;
    }

    //重置
    public void reset() {
        if (mTimer != null) {
            mTimer.cancel();
            mTimer = null;
        }
        isPause = false;
        mMilliseconds = 0;
        invalidate();
    }
    //记录
    public int record() {
       return mMilliseconds;
    }

源码传送门

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值