Android 自定义View 整理

Android 自定义View 整理

前言

Android 开发必须会的一个技能点,自定义view ,也是很多Android的必经之路,工作几年,很少这样静下心来好好地去整理所学的知识,在整理 动画的时候,发现真的有很多自己以前没注意的细节,以前都是知道怎么用,或者网上找到一些介绍的文章,扫一眼,经过这一次的查漏补缺,发现基础其实并没有自己想的那么扎实,故此,想在今年年底前,将所学的知识在整理一下。

一切的开始:onDraw()

onDraw()在自定义view中是一个重要的方法,通常我们也会在这里绘制我们的UI

HenCoder Android 开发进阶: 自定义 View 1-1 绘制基础 中,扔物线大佬。这样描述:一切的开始 可见 onDraw() 方法的重要性

在 kotlin 中 继承 view 重写 onDraw() 方法 ,可以看到 一个重要的方法

1
override fun onDraw(canvas: Canvas?)

和两个重要的 类 PaintCanvas

Canvas 就是画布,Paint 就是画笔,在 画布上使用画笔绘制,就是 自定义view

1
2
3
4
5
6
7
8
9
10
class CustomView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {

private var paint: Paint = Paint()

override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
}
}

在上面的 代码中有两个很重要的东西,Paint Canvas, 分享下面的链接,看完就对Paint Canvas的使用有个很形象的了解了

Paint 和 Canvas 使用介绍

画笔和画布的一些方法,这个每一个试一下,就知道效果了。

推荐这个大佬的系列,说的很清楚

Android关于Canvas你所知道的和不知道的一切
Android关于Paint你所知道的和不知道的一切
Android关于Path你所知道的和不知道的一切

扔物线大佬的系列,也要看。会了解的更加深刻,上面大佬的,排版很清晰,每个功能是什么作用都描述的非常好

官网也有详细的使用说明介绍,但是没图没真相,不够一目了然

官方文档 Canvas
官方文档 Paint

自定义属性

含义

通过declare-styleable标签为自定义View配置自定义属性

使用

res/values/attrs下添加属性

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MeasureView">
<attr name="custom_color" format="color"/>
</declare-styleable>
</resources>

在xml 中使用定义的属性

1
2
3
<com.allens.customviewdemo.view.MeasureView
app:custom_color="@color/colorPrimaryDark"
... />

在 自定义view中获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

class MeasureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {


var color: Int

init {
//获取 自定义的属性
val ta = context.obtainStyledAttributes(attrs, R.styleable.MeasureView)
//自定义的属性获取
color = ta.getColor(R.styleable.MeasureView_custom_color, Color.RED)
//回收
ta.recycle()
}

@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (canvas == null) {
return
}
canvas.drawColor(color)
}
}

自定义的属性类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  <!--1.reference:参考某一资源ID-->
<attr name="background" format="reference" />
<!--2. color:颜色值-->
<attr name = "textColor" format = "color" />
<!--3.boolean:布尔值-->
<attr name = "focusable" format = "boolean" />
<!--4.dimension:尺寸值-->
<attr name = "layout_width" format = "dimension" />
<!--5. float:浮点值-->
<attr name = "fromAlpha" format = "float" />
<!--6.integer:整型值-->
<attr name = "lines" format="integer" />
<!--7.string:字符串-->
<attr name = "text" format = "string" />
<!--8.fraction:百分数-->
<attr name = "pivotX" format = "fraction" />
<!--9.enum:枚举值。属性值只能选择枚举值中的一个-->
<attr name="orientation">
<enum name="horizontal" value="0" />
<enum name="vertical" value="1" />
</attr>
<!--10.flag:位或运算。属性值可以选择其中多个值-->
<attr name="gravity">
<flag name="top" value="0x01" />
<flag name="bottom" value="0x02" />
<flag name="left" value="0x04" />
<flag name="right" value="0x08" />
<flag name="center_vertical" value="0x16" />
...
</attr>
<!--11.混合类型:属性定义时可以指定多种类型值-->
<attr name = "background_2" format = "reference|color" />

onMeasure() 测量过程

onMeasure方法的作用是测量控件的大小。

HenCoder Android 开发进阶: 自定义 View 2-1 布局基础
HenCoder Android 开发进阶: 自定义 View 2-2 全新定义 View 的尺寸

大佬有很详细的描述,建议看一下视屏,会加深理解

View 绘制流程图

看万视屏 对下面的流程图应该有比较深刻的印象

扔物线HenCoder截图

ViewGroup 绘制流程图

扔物线HenCoder截图

onMeasure 的3中使用场景

在视屏中有介绍 这3种不同的使用场景

  • 修改已有View的尺寸
  • 计算自定义View的尺寸
  • 自定义ViewGroup的内部布局

修改已有View的尺寸

这种是 已有 计算尺寸的逻辑 下面就是一个简单的示例 继承了 ImageView 的 限制宽高 相同 并且最大只有 200 像素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SquareImageView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : AppCompatImageView(context, attrs, defStyleAttr) {

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//之前原来的测量算法
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//获取原先的测量宽高
var width = measuredWidth
var height = measuredHeight

//利用原先的测量宽高 计算 自己需要的 尺寸
if (width > height) {
if (height > 200) {
height = 200
}
width = height
} else {
if (width > 200) {
width = 200
}
height = width
}
//保存计算之后的结果
setMeasuredDimension(width, height)
}
}

计算自定义View的尺寸

这种是直接继承View的 需要自己去绘制,因为是自己绘制,所以 super.onMeasure(widthMeasureSpec, heightMeasureSpec) 就没不要了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

class MeasureView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {


@SuppressLint("DrawAllocation")
override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas)
if (canvas == null) {
return
}
canvas.drawColor(Color.RED)
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//①没必要在调用 super.onMeasure(widthMeasureSpec, heightMeasureSpec)
// super.onMeasure(widthMeasureSpec, heightMeasureSpec)
//widthMeasureSpec 父类View 穿过来的宽度限制
//heightMeasureSpec 父类View 穿过来的高度限制

//②自己确认需要的宽高
var measureWidth = 300
var measureHeight = 300

//③将 自己计算得到的 和 父类返回的view 通过 resolveSize 传入
//返回的参数 就是 符合限制条件的 宽高尺寸
measureWidth = resolveSize(measureWidth, widthMeasureSpec)
measureHeight = resolveSize(measureHeight, heightMeasureSpec)

//④将修正过的尺寸 通过 setMeasuredDimension 保存
setMeasuredDimension(measureWidth, measureHeight)
}
}

resolveSize 核心代码分析

看到了 resolveSize就看了一下他的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
//获取父类的 限制模式
final int specMode = MeasureSpec.getMode(measureSpec);
//获取父类的 限制大小
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
//限制上限
case MeasureSpec.AT_MOST:
//如果超过了限制 就是用父类的最大size
if (specSize < size) {
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
//没有超过限制 使用传过来的大小
result = size;
}
break;
//限制固定尺寸
case MeasureSpec.EXACTLY:
result = specSize;
break;
//不限制 将传过来的参数直接拿来用
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);
}

知道了onMearse 测量的 3种模式

参数含义
AT_MOST限制上限
EXACTLY限制固定尺寸
UNSPECIFIED不限制

自定义ViewGroup的内部布局

看一下下面的这个示例 Android 进阶自定义 ViewGroup 自定义布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

package com.allens.customviewdemo.view

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup

class CustomViewGroup @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : ViewGroup(context, attrs, defStyleAttr) {


private fun getMaxWidth(): Int {
val count = childCount
var maxWidth = 0
for (i in 0 until count) {
val currentWidth = getChildAt(i).measuredWidth
if (maxWidth < currentWidth) {
maxWidth = currentWidth
}
}
return maxWidth
}

private fun getTotalHeight(): Int {
val count = childCount
var totalHeight = 0
for (i in 0 until count) {
totalHeight += getChildAt(i).measuredHeight
}
return totalHeight
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
measureChildren(widthMeasureSpec, heightMeasureSpec)

val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val width = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)

if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
val groupWidth = getMaxWidth()
val groupHeight = getTotalHeight()
setMeasuredDimension(groupWidth, groupHeight)
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(getMaxWidth(), height)
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(width, getTotalHeight())
}
}

override fun onLayout(
changed: Boolean,
l: Int,
t: Int,
r: Int,
b: Int
) {
val count = childCount
var currentHeight = 0
for (i in 0 until count) {
val view: View = getChildAt(i)
val height: Int = view.measuredHeight
val width: Int = view.measuredWidth
view.layout(l, currentHeight, l + width, currentHeight + height)
currentHeight += height
}
}
}

示例Demo

github

推荐阅读

下面的扔物线的自定义view 系列.

HenCoder Android 开发进阶: 自定义 View 1-1 绘制基础
HenCoder Android 开发进阶: 自定义 View 1-2 Paint 详解
HenCoder Android 开发进阶: 自定义 View 1-3 文字的绘制
HenCoder Android 开发进阶: 自定义 View 1-4 Canvas 对绘制的辅助
HenCoder Android 开发进阶: 自定义 View 1-5 绘制顺序
HenCoder Android 开发进阶: 自定义 View 1-6 属性动画(上手篇
HenCoder Android 开发进阶: 自定义 View 1-7 属性动画(进阶篇
HenCoder Android 开发进阶: 自定义 View 1-8 硬件加速
HenCoder Android 开发进阶: 自定义 View 2-1 布局基础
HenCoder Android 开发进阶: 自定义 View 2-2 全新定义 View 的尺寸
HenCoder Android 开发进阶: 自定义 View 2-3 定制 Layout 的内部布局