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()
方法将它们的尺寸和位置传进去
- 遍历每个子