我们知道一个自定义view一般来说需要继承view或者viewGroup并实现onMeasure, onLayout, onDraw方法。 其中onMeasure用于测量计算该控件的宽高, onLayout用来确定控件的摆放位置,onDraw执行具体的绘制动作。

今天主要学习onDraw

先看下demo效果

在正式开始之前, 我们先要了解一些基本知识

1, 坐标系

2, 像素(px)与dp

绘制过程中所有的尺寸单位都是px

通常我们在xml中用dp或者sp来表示距离或者字体大小, 这是为了自动适配各种不同的分辨率,在实际运行时, Android系统会根据不同手机的屏幕密度 帮助我们把dp转成px

但是到了绘制阶段,就已经是在和屏幕对话了,是实际执行阶段的代码,这发生在android系统帮我们转换px之后, 所以绘制过程中我们只能用px

那么用px的话,如何保证我们画出来的图形在不同分辨率的手机上都能显示大致相同的大小呢?

android为我们提供了一个方法来完成像素的转换

 1 public static float applyDimension(int unit, float value,
2 DisplayMetrics metrics)
3 {
4 switch (unit) {
5 case COMPLEX_UNIT_PX:
6 return value;
7 case COMPLEX_UNIT_DIP:
8 return value * metrics.density;
9 ......
10 }

那么我们就可以定义一个扩展函数来完成这个转换,如

1 val Float.toPx
2 get() = TypedValue.applyDimension(
3 TypedValue.COMPLEX_UNIT_DIP,
4 this,
5 Resources.getSystem().displayMetrics)

这里的Resources.getSystem().displayMetrics获取的就是当前手机系统的displayMetrics

1 /**
2 * Return the current display metrics that are in effect for this resource object.
3 * The returned object should be treated as read-only.
4 */
5 public DisplayMetrics getDisplayMetrics() {
6 return mResourcesImpl.getDisplayMetrics();
7 }

3,paint 油漆

在Kotlin中, 我们可以通过 val paint = Paint()来获取一个paint对象

1     /**
2 * Create a new paint with default settings.
3 */
4 public Paint() {
5 this(0);
6 }

但是实际应用中, 我们通常会传入一个flag叫做ANTI_ALIAS_FLAG  , 它的作用是允许抗锯齿, 让我们画出来的图形更加圆滑

 1     /**
2 * Paint flag that enables antialiasing when drawing.
3 *
4 * <p>Enabling this flag will cause all draw operations that support
5 * antialiasing to use it.</p>
6 *
7 * @see #Paint(int)
8 * @see #setFlags(int)
9 */
10 public static final int ANTI_ALIAS_FLAG = 0x01;

4, canvas 画布

我们知道在onDraw方法中,会传入一个canvas对象, canvas有很多方法可以帮我们进行绘制的动作

如 drawLine, drawArc, drawCircle, drwaRect, drawText, drawPoint等等

如我们要画一条直线

class DashBoardView(context: Context, attributes: AttributeSet) : View(context,attributes){

    private val paint = Paint(ANTI_ALIAS_FLAG)

    override fun onDraw(canvas: Canvas) {
canvas.drawLine(100f.toPx, 100f.toPx,200f.toPx,200f.toPx, paint)
}
}

5, path 路径

比如我们想画一个圆, 除了直接调用canvas.drawCircle()方法之外,还有一种方法是

先调用path.addCircle()定义一个圆的路径, 然后再调用canvas.drawPath()方法来完成绘制,如:

 1 class DashBoardView(context: Context, attributes: AttributeSet) : View(context,attributes){
2
3 private val paint = Paint(ANTI_ALIAS_FLAG)
4 private val path = Path()
5
6 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
7 path.reset()
8 path.addCircle(width/2f, height/2f, 100f.toPx, Path.Direction.CCW)
9 }
10
11 override fun onDraw(canvas: Canvas) {
12 canvas.drawPath(path, paint)
13 }
14 }

注意, 不要在onDraw方法里执行对象创建的工作,因为onDraw会被频繁调用

对path的初始化应该放在onSizeChanged方法里, 当size改变时(比如父容器发生变化),应该对path进行reset

另外我们看到path方法里传入了一个direction参数,表示绘制的方向。 该参数有两种取值 Path.Direction.CW表示顺时针(clockwise) , Path.Direction.CCW表示逆时针(counter-clockwise) , 其作用是当绘制多个图形时,与fillType一起决定图形相交的部分是填充还是缕空。

我们再画一个和圆相交的矩形来演示一下

 1 //定义圆的半径
2 val RADIUS = 100f.toPx
3 class DashBoardView(context: Context, attributes: AttributeSet) : View(context,attributes){
4
5 private val paint = Paint(ANTI_ALIAS_FLAG)
6 private val path = Path()
7
8 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
9 path.reset()
10 path.addCircle(width/2f, height/2f, RADIUS, Path.Direction.CCW)
11 path.addRect(width/2f- RADIUS, height/2f, width/2f+ RADIUS, height/2f+2* RADIUS, Path.Direction.CCW)
12 }
13
14 override fun onDraw(canvas: Canvas) {
15 canvas.drawPath(path, paint)
16 }
17 }

当圆和矩形都是逆时针来画时,我们看到相交的部分被填充了

现在我们把矩形的path方向改为顺时针

1         path.addRect(width/2f- RADIUS, height/2f, width/2f+ RADIUS, height/2f+2* RADIUS, Path.Direction.CW)

可以看到相交的部分被缕空。 上文中我们说方向是和fillType一起决定是否缕空相交部分, 当我们没有去设置fillType时,path的默认fillType是 FillType.WINDING,

path里定义了四种fillType,

1 static final FillType[] sFillTypeArray = {
2 FillType.WINDING,
3 FillType.EVEN_ODD,
4 FillType.INVERSE_WINDING,
5 FillType.INVERSE_EVEN_ODD
6 };

WINDING模式会根据direction来判断是否填充,方向相同则填充,不同则缕空 。  EVEN_ODD则是不考虑方向,相交部分一律缕空。 另外两种分别是这两种的反向填充情况,如下图

好,啰嗦完了,我们进入正题

一个简单的仪表盘包括弧, 刻度, 指针,

1) 那么第一步我们先来画狐

1 canvas.drawArc(width/2f- RADIUS,
2 height/2f- RADIUS,
3 width/2f+ RADIUS,
4 height/2f + RADIUS,
5 ?,
6 ?,
7 false,
8 paint)

该方法传入的前四个值分别为left, top, right, bottom, 就是根据这些来确定圆(这里也可以理解为矩形)的位置

useCenter 的意思就是是否要让你画出来的弧闭合

startAngle和sweepAngle表示该弧的起始角度和扫描角度, 这个角度怎么计算呢?

画上坐标系,看图就明白了, 假设弧的开口角度是120, 那么起始角度就是90+120/2,

扫描角度是指弧形扫过的角度,显然,它等于360-开口角度

传入角度之后我们得到这样的效果

我们看到,现在画出来的弧内部都被填充了, 我们修改下paint, 让它画线条

这里就显示了useCenter的作用, 为true时它自动以圆心为中点帮我们加了两条线,把弧闭合了

我们把它改成false, 现在就得到了想要的弧

2) 第二步, 我们开始画刻度

这里我们需要了解另一个方法

paint.pathEffect = PathDashPathEffect()
 1     /**
2 * Dash the drawn path by stamping it with the specified shape. This only
3 * applies to drawings when the paint's style is STROKE or STROKE_AND_FILL.
4 * If the paint's style is FILL, then this effect is ignored. The paint's
5 * strokeWidth does not affect the results.
6 * @param shape The path to stamp along
7 * @param advance spacing between each stamp of shape
8 * @param phase amount to offset before the first shape is stamped
9 * @param style how to transform the shape at each position as it is stamped
10 */
11 public PathDashPathEffect(Path shape, float advance, float phase,
12 Style style) {
13 native_instance = nativeCreate(shape.readOnlyNI(), advance, phase,
14 style.native_style);
15 }
paint.pathEffect就是设置path的效果,
PathDashPathEffect就是我们用path来画虚线, 上面方法中的参数 advance表示虚线每个点之间的距离,表示一共要画多少个点phase

了解上面方法之后,我们就能想到,可以把每个刻度当成一个小矩形, 然后沿着第一步得到的弧, 用小矩形来画一条虚线

那么每个矩形的位置如何确定呢?

我们先确定矩形的长宽,如

1 val DASH_WIDTH = 3f.toPx
2 val DASH_HEIGHT = 10f.toPx

因为画矩形的Path每次的起点都在弧上,所以我们以该起点为坐标原点,画上坐标系

结合坐标系,我们现在就很容易得到:

        dashPath.addRect(0f, 0f, DASH_WIDTH, DASH_HEIGHT, Path.Direction.CCW )

有了小矩形, 我们再来看PathDashPathEffect(Path shape, float advance, float phase, Style style) 的第二个参数,间隔

间隔是需要计算的, 比如我们要画20个刻度, 那么间隔就是弧的总长度除以20, 那么弧的总长度怎么得到呢?

android为我们提供了pathMeasure

所以现在我们改用path来画弧

1 //画弧的path
2 private val arcPath = Path()
3
4 arcPath.addArc(width/2f- RADIUS,
5 height/2f- RADIUS,
6 width/2f+ RADIUS,
7 height/2f + RADIUS,
8 90f+ OPEN_ANGLE/2f,
9 360f- OPEN_ANGLE)

那么就可以得到弧的长度

val pathMeasure = PathMeasure(arcPath, false)
val length = pathMeasure.length

那么(length-DASH_WIDTH)/20 就等于刻度间距    这里减去DASH_WIDTH是因为: 20个间隔其实是21个刻度

所以完整代码如下

 1 //定义圆的半径
2 val RADIUS = 150f.toPx
3 //定义仪表盘的开口角度
4 const val OPEN_ANGLE = 120
5 //定义矩形的宽高
6 val DASH_WIDTH = 2f.toPx
7 val DASH_HEIGHT = 10f.toPx
8 class DashBoardView(context: Context, attributes: AttributeSet) : View(context,attributes){
9
10 private val paint = Paint(ANTI_ALIAS_FLAG)
11 //小矩形的path
12 private val dashPath = Path()
13 //画弧的path
14 private val arcPath = Path()
15 //
16 lateinit var pathEffect: PathDashPathEffect
17
18 init {
19 paint.strokeWidth = 3f.toPx
20 paint.style = Paint.Style.STROKE
21 dashPath.addRect(0f, 0f, DASH_WIDTH, DASH_HEIGHT, Path.Direction.CCW )
22 }
23
24 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
25 arcPath.reset()
26 arcPath.addArc(width/2f- RADIUS,
27 height/2f- RADIUS,
28 width/2f+ RADIUS,
29 height/2f + RADIUS,
30 90f+ OPEN_ANGLE/2f,
31 360f- OPEN_ANGLE)
32 val pathMeasure = PathMeasure(arcPath, false)
33 val length = pathMeasure.length
34 pathEffect = PathDashPathEffect(dashPath, (pathMeasure.length - DASH_WIDTH)/20f, 0f,PathDashPathEffect.Style.ROTATE)
35 }
36
37 override fun onDraw(canvas: Canvas) {
38 //先画一条弧
39 canvas.drawPath(arcPath, paint)
40 //再画虚线(刻度)
41 paint.pathEffect = pathEffect
42 canvas.drawPath(arcPath, paint)
43 paint.pathEffect = null
44 }
45 }

运行结果:

3)现在进行第三步, 画仪表指针

仪表指针好像很简单, 画一条线就行

嗯。。。。线的起点我们是知道的, 可是。。。终点怎么算呢

如图, 指针长度是已定的, 角度也可以得到, 那么根据三角定理就可以算出a和b的值, 即终点位置

上面看到是锐角的情况, 事实上同样的公式也适用于钝角。这里不明白的可以复习下数学啊

所以对长度为length,角度为angle的仪表指针, 它的终点坐标就是 (length*cos(angle), length*sin(angle))

那么下一个问题,角度怎么计算呢?

如图, 第三个刻度的角度就等于(360-OPEN_ANGLE)*20/3 + 90+ OPEN_ANGLE/2

 1 //画指针
2 canvas.drawLine(width/2f, height/2f,
3 (width/2f+ LENGTH* cos(markToRadians(3))).toFloat(),
4 (height/2f + LENGTH* sin(markToRadians(3))).toFloat(),
5 paint)
6
7
8 private fun markToRadians(mark: Int): Double {
9 return Math.toRadians(((360f-OPEN_ANGLE)/20*mark + 90f+ OPEN_ANGLE/2f).toDouble())
10 }

注意这里的cos(), sin()以及toRadians()方法

1 /** Computes the cosine of the angle [x] given in radians.
2 *
3 * Special cases:
4 * - `cos(NaN|+Inf|-Inf)` is `NaN`
5 */
6 @SinceKotlin("1.2")
7 @InlineOnly
8 public actual inline fun cos(x: Double): Double = nativeMath.cos(x)

cos()/sin()方法接收的角度参数是 given in radians--- 弧度

所以我们需要调用 Math.toRadians方法将角度转换为弧度

看下运行结果

自定义view---仪表盘--kotlin的更多相关文章

  1. 手把手带你画一个 时尚仪表盘 Android 自定义View

    拿到美工效果图,咱们程序员就得画得一模一样. 为了不被老板喷,只能多练啊. 听说你觉得前面几篇都so easy,那今天就带你做个相对比较复杂的. 转载请注明出处:http://blog.csdn.ne ...

  2. 简单说说Android自定义view学习推荐的方式

    这几天比较受关注,挺开心的,嘿嘿. 这里给大家总结一下学习自定义view的一些技巧.  以后写自定义view可能不会写博客了,但是可以开源的我会把源码丢到github上我的地址:https://git ...

  3. 手把手带你做一个超炫酷loading成功动画view Android自定义view

    写在前面: 本篇可能是手把手自定义view系列最后一篇了,实际上我也是一周前才开始真正接触自定义view,通过这一周的练习,基本上已经熟练自定义view,能够应对一般的view需要,那么就以本篇来结尾 ...

  4. Android 自定义View -- 简约的折线图

    转载请注明出处:http://write.blog.csdn.net/postedit/50434634 接上篇 Android 圆形百分比(进度条) 自定义view 昨天分手了,不开心,来练练自定义 ...

  5. 自定义view(结合刻度盘学习)

    先上效果图 一.View的测量(刻度盘的大小测量) 在现实生活中,我们如果要去画一个图形,那么便要知道它的大小和位置.所以android绘图时需要我们对view进行测量.android为我们提供了on ...

  6. 自定义View(二),强大的Canvas

    本文转自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2012/1212/703.html Android中使用图形处理引擎,2D部分是 ...

  7. Android开发自定义View

    Android中View组件的作用类似于Swing变成中的JPanel,它只是一个空白的矩形区域,View组件中没有任何内容.对于Android应用的其他UI组件来说,它们都继承了View组件,然后在 ...

  8. android新闻项目、饮食助手、下拉刷新、自定义View进度条、ReactNative阅读器等源码

    Android精选源码 Android仿照36Kr官方新闻项目课程源码 一个优雅美观的下拉刷新布局,众多样式可选 安卓版本的VegaScroll滚动布局 android物流详情的弹框 健身饮食记录助手 ...

  9. 自定义view(一)

    最近在学习自定义view  一遍看一别学顺便记录一下 1.View的测量-------->onMeasure() 首先,当我们要画一个图形的时候,必须知道三个数据:位置,长度,宽度   才能确定 ...

随机推荐

  1. 老鼠走迷宫I

    转自:http://blog.csdn.net/holymaple/article/details/8582517 说明:老鼠走迷宫是递回求解的基本提醒,我们在二维阵列中使用2来表示迷宫墙壁,使用1来 ...

  2. 真实机中安装CentOS

    前言 最近在b站上看了兄弟连老师的Linux教程,非常适合入门:https://www.bilibili.com/video/BV1mW411i7Qf 看完后就自己来试着玩下,正好手上有台空闲的电脑就 ...

  3. 程序解决十苹果问题 Java

    程序解决十苹果问题 Java 题目:10个苹果,其中有9个重量相同,剩余1个相比其它重量不同(或重或轻,不得而知),使用天平比较三次,找出重量特殊的那一个 import org.junit.Test; ...

  4. 紫色飞猪的研发之旅--06go自定义状态码

    在实际开发中,需要前后端需要协商状态码,状态码用于后端返前端时使用.在一个团队中,定义的状态码讲道理应该是一致的,项目开始的起始阶段状态码应该是定义了个七七八八的,随着功能的叠加而不断增加.此系列将围 ...

  5. git02

    Git Gui的使用 Ssh key 介绍及使用 Ssh key介绍 我理解的就是每台电脑上会产生出一个ssh key,然后自己有一个远程账户,但是自己有可能有很多台电脑, 包括家里的电脑还有公司的电 ...

  6. 前端搭建Linux云服务器,Nginx配置详解及部署自己项目到服务器上

    目录 搭建Linux云服务器 购买与基本配置 链接linux服务器 目录结构 基本命令 软件安装 Linux 系统启动 启动过程 运行级别 Nginx详解 1.安装 方式一:yum安装 方式二:自定义 ...

  7. Python 高级特性(3)- 列表生成式

    range() 函数 日常工作中,range() 应该非常熟悉了,它可以生成一个迭代对象,然后可以使用 list() 将它转成一个 list # 判断是不是迭代对象 print(isinstance( ...

  8. GoLang设计模式05 - 原型模式

    原型模式也是一种创建型模式,它可以帮助我们优雅地创建对象的拷贝.在这种设计模式里面,将克隆某个对象的职责交给了要被克隆的这个对象.被克隆的对象需要提供一个clone()方法.通过这个方法可以返回该对象 ...

  9. java代码覆盖实战

    Jacoco原理 代码插桩 On-the-fly插桩: JVM中通过-javaagent参数指定特定的jar文件启动Instrumentation的代理程序,代理程序在通过Class Loader装载 ...

  10. ABP 极简入门教程(一)

    本示例不讲概念,只讲怎么用,概念性的内容没有比官方文档更清楚的了,我也正在学习,可能理解的地方有不对的欢迎一起交流,但需要您了解以下内容才能看明白 asp.net core Entity Framew ...