Android自定义View之DashBoard(仪表盘)

前言

Android自定义View是Android初中级开发工程师向高级工程师进阶所必须掌握的一块内容,其重要性不言而喻。接下来的一段时间,我会连续出几篇跟自定义View相关的文章,从易到难,跟大家一起学习Android自定义View。本文讲一个Android很简单的View——DashBoard(仪表盘),以这个例子带大家去学习自定义View的基本绘制,让大家学会自定义View,并最终掌握。
注:本文的Demo在文章的最后

必须要掌握的几个点

在开始我们的绘制DashBoard之前,有几个点是必须要掌握的,这些是绘制的基础,也是前提。

Paint

自定义View的过程就是一个绘制的过程,而绘制就好像我们画画一样,而画画就必须要会画笔,Paint就是我们的画笔。

  • Paint 类的几个最常用的方法。具体是:
    • Paint.setStyle(Style style) 设置绘制模式
    • Paint.setColor(int color) 设置颜色
    • Paint.setStrokeWidth(float width) 设置线条宽度
    • Paint.setTextSize(float textSize) 设置文字大小
    • Paint.setAntiAlias(boolean aa) 设置抗锯齿开关

这里重点讲一下Paint.setStyle(Style style)方法,这个方法设置的是绘制的 Style 。Style 具体来说有三种: FILL, STROKE 和 FILL_AND_STROKE 。FILL 是填充模式,STROKE 是画线模式(即勾边模式),FILL_AND_STROKE 是两种模式一并使用:既画线又填充。它的默认值是 FILL,填充模式。只有当Style是STROKE 和 FILL_AND_STROKE时,Paint.setStrokeWidth(float width)才有意义,你全是填充的就不涉及什么线条宽度了。

canvas

Paint是画笔,可画画光有画笔还不行,还必须得有画布,Canvas就是画布。Canvas这个类是绘制最重要的类,没有之一,几乎所有绘制的方法都出自于这个类。

坐标系

方法先不讲,先讲一下坐标系,在Android 里,每个View 都有一个自己的坐标系,彼此之间是不影响的。这个坐标系的原点是 View 左上角的那个点;水平方向是 x 轴,右正左负;竖直方向是 y 轴,下正上负(注意,是下正上负,不是上正下负,和上学时候学的坐标系方向不一样也就是下面这个样子。

坐标系

这个坐标非常重要,因为我们所有的绘制都是在这个坐标系的基础上开展的,而关于坐标系还有这么个两个方法要特别注意:

  • Canvas.rotate(float degrees)//旋转坐标系,正角度顺时针,负角度逆时针
  • Canvas.translate(float dx, float dy)
    注意:以上两个方法的操作的对象是坐标系,跟View本身没有关系,之所以使用是为了让我们更好、更方便地绘制View。
方法

Canvas最重要也最常用的方法都是drawXXX()方法,方法太多了,我不可能一一列举,写几个最常用的,余下的请自行google

  • drawCircle(float centerX, float centerY, float radius, Paint paint) 画圆
    基本看参数名字就能猜出来是啥意思了,前面讲了坐标系的概念,前两个参数就是圆心的X、Y坐标了,第三个是半径大小,最后一个是画笔。
  • drawRect(float left, float top, float right, float bottom, Paint paint) 画矩形 (参数啥意思基本都能猜出来,不多讲,不行还是google)
  • drawOval(float left, float top, float right, float bottom, Paint paint) 画椭圆
  • drawLine(float startX, float startY, float stopX, float stopY, Paint paint) 画线
  • drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint) 绘制弧形或扇形
    left, top, right, bottom 描述的是这个弧形所在的椭圆;startAngle 是弧形的起始角度(x 轴的正向,即正右的方向,是 0 度的位置;顺时针为正角度,逆时针为负角度),sweepAngle 是弧形划过的角度;useCenter 表示是否连接到圆心,如果不连接到圆心,就是弧形,如果连接到圆心,就是扇形。
  • drawPath(Path path, Paint paint) 画自定义图形
    这里Path对象要讲一下,Path.addXxx()——添加子图形,例如Path.addCircle(float x, float y, float radius, Direction dir) 添加圆;Path.xxxTo() ——画线(直线或曲线)lineTo(float x, float y) / rLineTo(float x, float y) 画直线.

小结

以上就是我们开始绘制DashBoard之前还需要掌握的基础,因为都用得到。Paint是画笔,主要就是设置画笔相关的属性,颜色、大小、风格等等;canvas是画布,坐标系的概念必须清楚,重要的几个方法也必须知道。

DashBoard

先上个图


DashBoard

看图其实很简单,基本上就分为三步,第一份画弧;第二步画刻度;第三步画指针。

画弧线

画弧线之前,我简单讲一下自定义View的流程,创建一个DashBoard的类型继承View,重写构造方法和onDraw(Canvas canvas)

public class DashBoard extends View {
 public DashBoard(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initPaint();//初始化Paint
    }
  protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
   }
}

初始化Paint

 private void initPaint(){
        mPaint=new Paint(Paint.ANTI_ALIAS_FLAG);//抗锯齿
        mPaint.setStyle(Paint.Style.STROKE);//画线模式
        mPaint.setStrokeWidth(Utils.px2dp(2));//线宽度
        mPaint.setColor(Color.BLACK);
    }

做好以上初始化工作,我们就开始第一步画弧线。调用Canvas.drawArc(float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean useCenter, Paint paint)这个方法上面有介绍过,这里就不详细讲了,前四个参数很好设置,我们定义圆心在View的中心位置,即(getWidth()/2,geHeight()/2),半径150dp,那么前四个参数就有了,第六个参数sweepAngle是划过的度数,这个我们定义为240度,第七个是否连接中心,false,我们不需要连接中心,最后一个放自己的Paint就行了,现在的关键就是第五个参数,开始角度的计算,下面我出张图帮助大家计算一下
startAngle角度分析

图中画得应该比较清楚了,不过多解释,直接上代码

 private void drawArc(Canvas canvas){
        rectF = new RectF(getWidth() / 2 - RADIUS, getHeight() / 2 - RADIUS,
                getWidth() / 2 + RADIUS, getHeight() / 2 + RADIUS);
        canvas.drawArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE,false,mPaint);
    }

效果图


效果图

画刻度

关于画刻度,其实就是画线吗,那画线的方法拿过来看一下,Canvas.drawLine(float startX, float startY, float stopX, float stopY, Paint paint) ,需要线的起始点和结束点的坐标,如果我要画21个刻度,那需要21个点的刻度都算一遍,我去,谁可能这么干啊。放心,我们当然不会这么干了,下面提供两种方式。

第一种方式

思路:坐标系的旋转+平移
之前在讲Canvas里提过,每一个Android的View都对应着有一个坐标系,坐标原点在View的左上角(可以看一下上面的那张图),现在如果我们把坐标原点平移到圆心的位置,并且再顺时针旋转30°,那么当前的坐标系就是下面这样的

平移后的坐标系

那么在我们平移旋转以后,在画右下角第一个刻度的时候,就相当于这个刻度的起始点和结束点的纵坐标都是0,因为我们把原点移动到了圆心,而结束点的横坐标就是圆的半径(RADIUS),而起始点的横坐标就是(半径-刻度线的长度)。下面我还是用一张图来解释一下

这样第一个刻度线,我们就画出来了,现在假定我们要画21个刻度线,21刻度线对应20个间隔,总角度是240度,每个刻度线间隔角度就是240/20即12度,所以其他的刻度线就可以让坐标系每次逆时针旋转12度画一次,用代码表达一下会更清晰。

private void drawDegree(Canvas canvas){
        canvas.translate(getWidth()/2,getHeight()/2);
        canvas.rotate(30);
        for (int i=0;i<20;i++){
            //Utils.px2dp(10)是刻度线的长度,为10dp
            canvas.drawLine(RADIUS-Utils.px2dp(10),0,RADIUS,0,mPaint);
            canvas.rotate(-SWEEPANGLE/20);//逆时针选择 负值是逆时针
        }
        //最后一根线
        canvas.drawLine(RADIUS-Utils.px2dp(10),0,RADIUS,0,mPaint);
        canvas.rotate(240-30);//旋转回去的角度
        canvas.translate(-getWidth()/2,-getHeight()/2);

    }

画完以后的效果图


这个图我故意没有缩放,细心的你可能已经发现了第一个和最后一个刻度明显有点不自然,我再放大一下

这下很明显了吧,感觉好像是空了一半的宽度没有画在弧线上。这是为什么呢? 还得再上个图

看图我们可以知道,我们在画刻度的时候,Paint就是我们的画笔,默认是宽度的,我们画刻度的纵坐标是0,但是实际画的时候是把画笔的中间位置放在0的坐标上,这就导致了好像空了一半Paint出来,知道了什么原因其实解决起来也很简单,就是把起始点和结束点的纵坐标相应的提高半个画笔的高度,直接看代码吧

 private void drawDegree(Canvas canvas){
        canvas.translate(getWidth()/2,getHeight()/2);
        canvas.rotate(30);
        for (int i=0;i<20;i++){
            //纵坐标下正上负,向上提高,加负值,即-mPaint.getStrokeWidth()/2
            canvas.drawLine(RADIUS-Utils.px2dp(10),-mPaint.getStrokeWidth()/2,RADIUS,-mPaint.getStrokeWidth()/2,mPaint);
            canvas.rotate(-SWEEPANGLE/20);
        }
        //最后一个点,因坐标系已经旋转了240度,向上提高,加整值,即mPaint.getStrokeWidth()/2
        canvas.drawLine(RADIUS-Utils.px2dp(10),mPaint.getStrokeWidth()/2,RADIUS,mPaint.getStrokeWidth()/2,mPaint);
        canvas.rotate(240-30);//旋转回去的角度
        canvas.translate(-getWidth()/2,-getHeight()/2);
    }

看了注释,你会发现第一个点和最后一个点的提高方式不同,这也是为什么上面”相应的“三个字我要加粗了,再看一眼修改后的效果图
微信图片_20181024141740.jpg
第二种方式

思路:使用PathMeasure测量弧线长度,利用PathDashPathEffect来画刻度
简单讲一下PathMeasure和PathDashPathEffect

  • PathMeasure:用来测量路径的长度,public PathMeasure(Path path, boolean forceClosed),通过PathMeasure.getLength()
  • PathDashPathEffect:Paint.setPathEffect(PathEffect effect)给图形的轮廓设置效果的,PathDashPathEffect是PathEffect的一个子类,它的构造方法 PathDashPathEffect(Path shape, float advance, float phase, PathDashPathEffect.Style style) 中, shape 参数是用来绘制的 Path ; advance 是两个相邻的 shape 段之间的间隔,不过注意,这个间隔是两个 shape 段的起点的间隔,而不是前一个的终点和后一个的起点的距离; phase 和 DashPathEffect 中一样,是虚线的偏移;最后一个参数 style,是用来指定拐弯改变的时候 shape 的转换方式。style 的类型为 PathDashPathEffect.Style ,是一个 enum ,具体有三个值:TRANSLATE:位移,ROTATE:旋转,MORPH:变体
    知道了这两个方法,我们就可以先用PathMeasure拿到弧线的长度,除以20获得每个间隔的长度,然后通过Paint.setPathEffect(new PathDashPathEffect())方法来画刻度就行了,直接上代码
private void drawDegree2(Canvas canvas){
        //刻度的路径
        dash=new Path();
        //Path.Direction.CW顺时针方向 同时顺时针切线方向为X轴正向 
        dash.addRect(0,0,Utils.px2dp(2),Utils.px2dp(10), Path.Direction.CW);
        //弧线长度的路径
        Path length=new Path();
        length.addArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE);
        //测量弧线长度
        pathMeasure=new PathMeasure(length,false);
        //这里(pathMeasure.getLength()-mPaint.getStrokeWidth())/20 弧线长度之所以减去Paint的宽度跟我第一种方式去掉宽度是一个意思
        mPaint.setPathEffect(new PathDashPathEffect(dash,
                (pathMeasure.getLength()-mPaint.getStrokeWidth())/20,0, PathDashPathEffect.Style.ROTATE));
        canvas.drawArc(rectF,90+(360-SWEEPANGLE)/2,SWEEPANGLE,false,mPaint);
        mPaint.setPathEffect(null);
    }

这里我就不细讲了,注释还是比较清楚,效果图跟第一种方式是一样的就不贴图,个人还是更加推荐第一种的画刻度方式。

画指针

画指针呢,就比较简单了,其实就是调用画线的方法,先把坐标系平移动原点位置,设置一个当前的角度currentAngle还有指针长度INDICATOR,唯一有一点难度的就是计算结束点的横纵坐标,需要用到三角函数的知识

  • 横坐标:Math.cos(Math.toRadians(currentAngle))*INDICATOR
  • 纵坐标:Math.sin(Math.toRadians(currentAngle))*INDICATOR
    很简单,上代码
private void drawIndicator(Canvas canvas){
        canvas.translate(getWidth()/2,getHeight()/2);
        canvas.drawLine(0,0,
                (float) Math.cos(Math.toRadians(currentAngle))*INDICATOR,
                (float)Math.sin(Math.toRadians(currentAngle))*INDICATOR,
                mPaint);
        canvas.translate(getWidth()/2,getHeight()/2);
    }

最后

Android自定义View是Android比较难的一块内容,本文主要通过绘制DashBoard来讲基本的绘制,Paint和Canvas的基本用法,接下来的一段时间内,我会继续出自定义View相关的内容,下一篇文章会讲绘制文字。
最后放上文章的demo DashBoard,觉得还不错的请给个star哈

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

推荐阅读更多精彩内容