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

源码传送门

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容

  • 【Android 自定义View之绘图】 基础图形的绘制 一、Paint与Canvas 绘图需要两个工具,笔和纸。...
    Rtia阅读 11,655评论 5 34
  • 前言:自打学安卓第一天起就听过自定义View,记得当时实习的项目里要弄一个随机验证码,当时百度了一份代码,打开发...
    伦子汪不造轮子阅读 1,327评论 1 11
  • 星星山:土再挖不少,雨再大不往跟前流。 朝廷派人找王通到朝廷为官,王通跑得太快,在通化村东(现王通庙处)丢了一只靴...
    简单明了阅读 330评论 0 0
  • 我家的玩具很多,有小猫,小兔,还有玩具小白狗.我最喜欢的就是可爱的白狗了,我每天睡觉时,都会把她放在我旁边...
    王高贞十妈妈阅读 169评论 1 0
  • 当今中国社会上充斥着“人脉就是钱脉”等言论,虽然那些世俗的价值观我不置可否,但是不得不承认社交能力是决定一个人成败...
    桃子_9d34阅读 844评论 0 0