美文网首页
安卓自定义View,仿小米秒钟

安卓自定义View,仿小米秒钟

作者: burro630 | 来源:发表于2018-04-15 14:00 被阅读43次

    效果图:

    这里写图片描述

    前言:

    自定义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;
        }
    

    源码传送门

    相关文章

      网友评论

          本文标题:安卓自定义View,仿小米秒钟

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