自定义view---仪表盘--kotlin
我们知道一个自定义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的更多相关文章
- 手把手带你画一个 时尚仪表盘 Android 自定义View
拿到美工效果图,咱们程序员就得画得一模一样. 为了不被老板喷,只能多练啊. 听说你觉得前面几篇都so easy,那今天就带你做个相对比较复杂的. 转载请注明出处:http://blog.csdn.ne ...
- 简单说说Android自定义view学习推荐的方式
这几天比较受关注,挺开心的,嘿嘿. 这里给大家总结一下学习自定义view的一些技巧. 以后写自定义view可能不会写博客了,但是可以开源的我会把源码丢到github上我的地址:https://git ...
- 手把手带你做一个超炫酷loading成功动画view Android自定义view
写在前面: 本篇可能是手把手自定义view系列最后一篇了,实际上我也是一周前才开始真正接触自定义view,通过这一周的练习,基本上已经熟练自定义view,能够应对一般的view需要,那么就以本篇来结尾 ...
- Android 自定义View -- 简约的折线图
转载请注明出处:http://write.blog.csdn.net/postedit/50434634 接上篇 Android 圆形百分比(进度条) 自定义view 昨天分手了,不开心,来练练自定义 ...
- 自定义view(结合刻度盘学习)
先上效果图 一.View的测量(刻度盘的大小测量) 在现实生活中,我们如果要去画一个图形,那么便要知道它的大小和位置.所以android绘图时需要我们对view进行测量.android为我们提供了on ...
- 自定义View(二),强大的Canvas
本文转自:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2012/1212/703.html Android中使用图形处理引擎,2D部分是 ...
- Android开发自定义View
Android中View组件的作用类似于Swing变成中的JPanel,它只是一个空白的矩形区域,View组件中没有任何内容.对于Android应用的其他UI组件来说,它们都继承了View组件,然后在 ...
- android新闻项目、饮食助手、下拉刷新、自定义View进度条、ReactNative阅读器等源码
Android精选源码 Android仿照36Kr官方新闻项目课程源码 一个优雅美观的下拉刷新布局,众多样式可选 安卓版本的VegaScroll滚动布局 android物流详情的弹框 健身饮食记录助手 ...
- 自定义view(一)
最近在学习自定义view 一遍看一别学顺便记录一下 1.View的测量-------->onMeasure() 首先,当我们要画一个图形的时候,必须知道三个数据:位置,长度,宽度 才能确定 ...
随机推荐
- 前端性能优化之http缓存
前不久,公司前端开会,领导抽问了4个问题,前3个简单大家都答起来了,第4个问题关于缓存的这方面我只是了解,结果刚好问到我了(会的不问,专门挑我不熟悉的问,我这运气真是没话说),20多个前端看着我,答得 ...
- VMware workstation16 中Centos7下MySQL8.0安装过程+Navicat远程连接
1.MySQL yum源安装 2.安装后,首次登录mysql以及密码配置3.远程登录问题(Navicat15为例) 一.CentOS7+MySQL8.0,yum源安装1.安装mysql前应卸载原有my ...
- OKR工作法读后感
<OKR工作法>把管理思想融入到一则创业故事中,故事细节经过了精心的设计,融入了管理智慧和踩坑填坑经验,每个细节都以小见大,耐人寻味.一千个读者,就有一千个哈姆雷特. 所以这次我不去点评大 ...
- 常用cron表达式
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时 0 0 12 ? * WED 表示每个星期三中午12点 " ...
- Hutool中那些常用的工具类和方法
Hutool中那些常用的工具类和方法 Hutool是一个Java工具包,它帮助我们简化每一行代码,避免重复造轮子.如果你有需要用到某些工具方法的时候,不妨在Hutool里面找找,可能就有.本文将对Hu ...
- Dubbo | Dubbo快速上手笔记 - 环境与配置
目录 前言 1. Dubbo相关概念 1.1 自动服务发现工作原理 2. 启动文件 2.1 zookeeper-3.4.11\bin\zkServer.cmd 2.2 zookeeper-3.4.11 ...
- 实例_ Java中的代理模式
静态代理 我们定义一个接口,并且使用代理模式,想要做到的就是在调用这个接口的实现类时在此方法上添加功能. public interface HelloInterface { void sayHello ...
- HashSet的存储原理
HashSet的底层用哈希散列表来存储对象(默认长度为16的数组),假如: Set set=new HashSet(); set.add(obj); 内部存储过程为:定义h=obj.hashCode, ...
- linux常用查询命令
1 **系统** 2 # uname -a # 查看内核/操作系统/CPU信息 3 # head -n 1 /etc/issue # 查看操作系统版本 4 # cat /proc/cpuinfo # ...
- elementUI 表格 table 的表头错乱问题
页面中多组件开发时,如果页面中有表格的,table表格头出现表头错乱 // 全局设置1 body .el-table th.gutter{ 2 display: table-cell!importan ...