Android
自定义控件的 measure
, layout
Android
自定义 View
一般都要写 测量
, 摆放
在 onMeasure 里面测量出自己的宽高, 然后父控件会根据自己测量出来的宽高来进行摆放(layout)
如果不按照父容器的约束来, 就会出问题
重写了 layout
方法, 摆放的很大, 但是父容器那里的尺寸并没有改, 所以其他的 view 就会有重叠
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.example.lsn13_layout.views.OneHundredView
android:layout_width="10dp"
android:layout_height="10dp"
android:background="#FF0000" />
<View
android:layout_width="10dp"
android:layout_height="10dp"
android:background="#00FF00" />
</LinearLayout>
class OneHundredView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
override fun layout(l: Int, t: Int, r: Int, b: Int) {
super.layout(l, t, r + dp2px(100F).toInt(), b + dp2px(100F).toInt())
}
}
流程
- 从整体看:
- 测量流程: 从根
View
递归调用每一级子View
的measure()
方法, 对它们进行测量 - 布局流程: 从根
View
递归调用每一级子View
的layout()
方法, 把测量过程得出的子View
的位置和尺寸传给子View
, 子View
保存 - 为什么要分两个流程?
- 从个体看, 对于每个
View
:
- 1、运行前, 开发者在
xml
文件里写入对View
的布局要求layout_xxx
- 2、父
View
在自己的onMeasure()
中, 根据开发者在xml
中写的对子View
的要求, 和自己的可用空间, 得出对子View
的具体尺寸要求 - 3、子
View
在自己的onMeasure()
中, 根据自己的特性算出自己的期望尺寸
- 如果是
ViewGroup
, 还会在这里调用每个子View
的measure()
进行测量
- 4、父
View
在子View
计算出期望尺寸后, 得出子View
的实际尺寸和位置 - 5、子
View
在自己的layout()
方法中, 将父View
传进来的自己的实际尺寸和位置保存
- 如果是
ViewGroup
, 还会在onLayout()
里调用每个子View
的layout()
把它们的尺寸位置传给它们
自定义控件一般有三种情况
1、集成已有的 View
, 简单改写它们的尺寸: 重写 onMeasure()
- 重写
onMeasure()
- 用
getMeasuredWidth()
和getMeasuredHeight()
获取到测量出的尺寸 - 计算出最终要的尺寸
- 用
setMeasureDimension(width, height)
把结果保存
2、对自定义 View
完全进行自定义尺寸计算: 重写 onMeasure()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val size = (PADDING + RADIUS) * 2
val measuredWidth = resolveSizeAndState(size.toInt(), widthMeasureSpec, 0)
val measuredHeight = resolveSizeAndState(size.toInt(), heightMeasureSpec, 0)
setMeasuredDimension(measuredWidth, measuredHeight)
}
- 1、重写
onMeasure
- 2、计算出自己的尺寸
- 3、用
resolveSize()
或者resolveSizeAndState()
修正结果
resolveSize()
内部实现
- 首先用
MeasureSpec.getMode(measureSpec)
和MeasureSpec.getSize(measureSpec)
取出父容器对自己的尺寸限制类型和具体限制尺寸; - 如果
measureSpec
的mode
是MeasureSpec.EXACTLY
, 表示父View
对子View
的尺寸做出了精确限制, 所以就放弃计算出的size
, 直接选用measureSpec
的size
; - 如果
measureSpec
的mode
是MeasureSpec.AT_MOST
, 表示父View
对子View
的尺寸只限制了上限, 需要看情况:
- 如果计算出的
size
不大于measureSpec
中显示的size
, 表示尺寸没有超出限制, 所以选用计算出的size
; - 而如果计算出的
size
大于measureSpec
中限制的size
, 表示尺寸超限了, 所以选用measureSpec
的size
, 并且在resolveSizeAndState()
中会添加标志MEASURED_STATE_TOO_SMALL
(这个表示可以辅助父View做测量和布局的计算);
- 如果
measureSpec
的mode
是MeasureSpec.UNSPECIFIED
, 表示父View
对子View
没有任何尺寸限制, 所以直接选用计算出的size
, 忽略measureSpec
中的size
.
- 4、用
setMeaduredDimension(width, height)
来保存结果
3、自定义 Layout
: 重写 onMeasure()
和 onLayout()
- 首先用
measureChildWithMargins(child, widthMeasureSpec, paddingStart + paddingEnd, heightMeasureSpec, heightUsed + paddingTop + paddingBottom)
测量子控件的宽高 - 如果已经用掉的宽度加上子控件的宽度超过了父容器给的宽度, 就要换行, 然后重新测量
- 最后用
resolveSizeAndState()
来纠正宽高 - 1、重写
onMeasure()
- 1、遍历每个子
View
, 用measureChildWithMargins()
测量
- 需要重写
generateLayoutParams()
并返回MarginLayoutParams
- 换行处的
View
需要重新测量 - 测量完成后, 得出子
View
的尺寸和位置, 并保存
- 2、测量出所有的子
View
的尺寸后, 计算出自己的尺寸, 最后用setMeasuredDimension(measuredWidth, meaduredHeight)
保存
- 2、重写
onLayout()
- 遍历每个子
View
, 调用它们的layout()
方法将它们的尺寸和位置传进去