我们知道一个自定义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() 首先,当我们要画一个图形的时候,必须知道三个数据:位置,长度,宽度   才能确定 ...

  10. Android 自定义View及其在布局文件中的使用示例

    前言: 尽管Android已经为我们提供了一套丰富的控件,如:Button,ImageView,TextView,EditText等众多控件,但是,有时候在项目开发过程中,还是需要开发者自定义一些需要 ...

随机推荐

  1. 发布网站详细步骤(.Net)

    (i)打开需要发布的网站 右键需要发布的项目 点击下拉框新建配置文件,输入配置文件名称,点击确定,下一步 发布方法选文件系统,目标位置:项目的根目录 配置选Release 点击发布 (ii) 打开ii ...

  2. Linux ARM kernel Makefile and Kconfig

    kernel build:顶层Makefile:-->1. include build/main.mk    -->2. include build/kernel.mk         k ...

  3. 关于egit的日常操作总结

    $git fetch -p --prune -p -- remove any remote tracking branches that no longer exist remotely prune的 ...

  4. 安装mysql时出错。无法初始化。 libstdc++.so.5

    ./bin/mysqld: error while loading shared libraries: libstdc++.so.5: cannot open shared object file: ...

  5. 初探Google Guava

    Guava地址:https://github.com/google/guava 第一次接触我是在16年春github上,当时在找单机查缓存方法,google guava当初取名是因为JAVA的类库不好 ...

  6. [HDU4864]Task (贪心)

    此图和上一篇博客的图一起看有奇效 题意 https://vjudge.net/problem/HDU-4864 思路 贪心 代码 by lyd 我实在是敲不来 #include <iostrea ...

  7. centos 6 与 centos 7 服务开机启动、关闭设置的方法

    简单说明下 centos 6 与 centos 7 服务开机启动.关闭设置的方法: centos 6 :使用chkconfig命令即可. 我们以apache服务为例: #chkconfig --add ...

  8. Scala中使用implict 扩展现有类的方法

    Scala中implict的一种用法就是扩展现有类的方法,有点类似于.Net中的扩展方法(MS对扩展方法的介绍:扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型.重新编译或以其他方式修改 ...

  9. 实验五 — — Java网络编程及安全

    java的第五个实验——Java网络编程及安全 北京电子科技学院 实     验    报     告 课程:Java程序设计 班级:1352 姓名:林涵锦 学号:20135213    成绩:    ...

  10. [洛谷P4091][HEOI2016/TJOI2016]求和

    题目大意:给你$n(n\leqslant10^5)$,求:$$\sum\limits_{i=0}^n\sum\limits_{j=0}^i\begin{Bmatrix}i\\j\end{Bmatrix ...