我们知道一个自定义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. RabbitMQ(二):交换机

    前言 学习自bili尚硅谷-RabbitMQ 发布确认 之前的消息应答,队列持久化是为了保证 -> 消息从rabbitmq队列到消费者的过程中不会丢失:消息持久化则是为了保证 -> 消息从 ...

  2. 移动端常用单位——rem

    移动端常用单位: ①px:像素大小,固定值 ②%:百分比 ③em(不常用,但是在首行缩进时可以使用):相对自身的font大小(当自身的字体大小也是em做单位时,才会以父元素的字体大小为基准单位) ④r ...

  3. delta源码阅读

    阅读思路: 1.源码编译 2.功能如何使用 3.实现原理 4.源码阅读(通读+记录+分析) 源码结构 源码分析 元数据 位置:org.apache.spark.sql.delta.actions下的a ...

  4. C# 简单粗暴的毫秒转换成 分秒的格式

    C# 简单粗暴的毫秒转换成 分秒的格式 1:code(网络上很多存在拷贝或者存在bug的或者不满足自己的要求) 1 public static string RevertToTime(double m ...

  5. vue 接入 vod-js-sdk-v6.js 完成视频上传

    东西有点多,耐心看完.按照操作一步一步来,绝对能成功 首先:npm 引入 npm install vod-js-sdk-v6 mian.js  全局引入  //腾讯云点播 import TcVod f ...

  6. ubuntu下使用minicom

    环境 宿主机平台:Ubuntu 16.04.6 目标机:iMX6ULL 安装及使用 首先时在Ubuntu里安装minicom sudo apt-get install minicom 接下来可以使用 ...

  7. 288 day05_异常,线程

    day05 [异常.线程] 主要内容 异常.线程 教学目标 [ ] 能够辨别程序中异常和错误的区别 [ ] 说出异常的分类 [ ] 说出虚拟机处理异常的方式 [ ] 列举出常见的三个运行期异常 [ ] ...

  8. Python新手的奇技淫巧,掌握在手的充实感

    以下是我长久以来收集的一些Python实用技巧和工具,希望能对刚学习Python的新手有所帮助.  1.交换变量 x = 6 y = 5 x, y = y, x print x >>> ...

  9. 一起学习PHP的runkit扩展如何使用

    这次又为大家带来一个好玩的扩展.我们知道,在 PHP 运行的时候,也就是部署完成后,我们是不能修改常量的值,也不能修改方法体内部的实现的.也就是说,我们编码完成后,将代码上传到服务器,这时候,我们想在 ...

  10. javascript,jquery在父窗口触发子窗口(iframe)某按钮的click事件

    $('iframe').contents().find(".btn").click(); 其中 contents(): 查找匹配元素内部所有的子节点(包括文本节点).如果元素是一个 ...