布局流程
- 测量流程:从根 View 递归调用每一级子 View 的 measure() 方法,对它们进行 测量
- 布局流程:从根 View 递归调用每一级子 View 的 layout() 方法,把测量过程得 出的子 View 的位置和尺寸传给子 View,子 View 保存
- 为什么要分两个流程?
- 因为可能会重复测量
对于每个View本身
- 运行前,开发者在 xml 文件里写入对 View 的布局要求 layout_xxx
- 父 View 在自己的 onMeasure() 中,根据开发者在 xml 中写的对子 View 的要 求,和自己的可用空间,得出对子 View 的具体尺寸要求
- 子 View 在自己的 onMeasure() 中,根据父 View 的要求和自己的特性算出自己 的期望尺寸
- 如果是 ViewGroup,还会在这里调用每个子 View 的 measure() 进行测量 父 View - 在子 View 计算出期望尺寸后,得出子 View 的实际尺寸和位置
- 子 View 在自己的 layout() 方法中,将父 View 传进来的自己的实际尺寸和位置 保存
- 如果是 ViewGroup,还会在 onLayout() 里调用每个子 View 的 layout() 把 它们的尺寸位置传给它们
对于父布局wrap-content子布局某个是match-parent
下面的代码要进行两次测量
- 测量两遍,第一遍将match-parent的宽度设置为0
- 第二遍将宽度设置为其他子布局的最大宽度
对于父布局wrap-content子布局都是match-parent
对于下面的代码要测量三遍
- 第一遍测量宽度都置为0
- 第二遍根据LinearLayout内部规则逐一设置宽度
- 第三遍重置宽度
xml与onMeasure的冲突问题
如下:
- 自定义myView重写布局宽高都为100dp
- xml中我们重写宽高都为match_parent
class MyView extends View{
onMeasure{
setMeasureDimension(100,100)
}
}
LinearLayout中Myview会正常展示100dp宽高
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Myview
android:layout_width="match_parent"
android:layout_height="match_parent">
</LinearLayout>
而在Constraint中Myview则填满屏幕
<ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<Myview
android:layout_width="match_parent"
android:layout_height="match_parent">
</ConstraintLayout>
所以在开发中我们要尽量避免这样的冲突
自定义布局
- 继承已有的View,简单改写他们的尺寸:重写onMeasure
- 如SquareImageView
- 对自定义view完全进行自定义尺寸计算:重写onMeasure
- 如CircleView
- 自定义Layout:重写onMeasure()和onLayout()
- 如TagLayout
-
onMeasure 改写已有 View 的尺寸
重写layout方法及其问题
如自定义SquareImageView
- 取短边作为边长
class SuqareImageView(context: Context?, attrs: AttributeSet?) : AppCompatImageView(context, attrs) {
override fun layout(l: Int, t: Int, r: Int, b: Int) {
//取短边为方形边长
val width = r-l
val height = b-t;
val size = min(width,height);
super.layout(l, t, l+size, t+size)
}
}
- 在布局中使用它,可以正常显示200dp的方形ImageView
<com.dsh.txlessons.viewcustomize.SuqareImageView
android:layout_width="300dp"
android:layout_height="200dp"
android:src="@mipmap/slmh"/>
<View
android:layout_width="300dp"
android:background="@color/colorPrimary"
android:layout_height="200dp"/>
- 但是当我们增加一个布局之后,就变成了如下这样,同级其他布局并没有紧贴SuqareImageView,这是因为重写layout方法之后父布局并不知道子布局尺寸发生了变化,在父布局逐级调用子View的measure方法时,依然认为SuqareImageView的宽度为300dp,所以我们需要重写onMeasure方法
- 所以不能重写layout而是要重写onMeasure
重写onMeasure
- 重写 onMeasure()
- 用 getMeasuredWidth() 和 getMeasuredHeight() 获取到测量出的尺寸
- 计算出最终要的尺寸
- 用 setMeasuredDimension(width, height) 把结果保存
- 为什么不重写 layout()?
- 上面有讲,父布局不知道子布局尺寸发生变化
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val size = min(measuredWidth,measuredHeight);
setMeasuredDimension(size,size)
}
效果正常了
getMeasureWidth与getWidth
- 测量过程中使用getMeasureWidth和getMeasureHeight
- getWidth与getHeight是在布局时才会获取到结果
- getMeasureWidth可以更早拿到尺寸
onMeasure完全自定义View的尺寸
- 重写 onMeasure()
- 计算出自己的尺寸
- 用 resolveSize() 或者 resolveSizeAndState() 修正结果 使用
- setMeasuredDimension(width, height) 保存结果
-
自定义一个带背景的圆形View
private val RADIUS = 100.dp; private val PADDING = 100.dp; class CircleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { private val paint = Paint(Paint.ANTI_ALIAS_FLAG) override fun onDraw(canvas: Canvas) { super.onDraw(canvas) canvas.drawCircle(PADDING+RADIUS,PADDING+RADIUS,RADIUS,paint) } override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val size = ((PADDING+ RADIUS)*2).toInt(); setMeasuredDimension(size,size) } }用wrap-content指定宽高,实际宽高应该是400dp
<com.dsh.txlessons.viewcustomize.CircleView android:layout_width="wrap_content" android:background="@color/colorPrimary" android:layout_height="wrap_content"/> -
使用固定宽高 宽300dp高200dp,没有效果,布局依然是400dp的宽高,这不是我们期待的结果
-
所以要继续修改onMeasure代码,通过resolveSize获取新的宽高,调用setMeasuredDimension保存结果
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val size = ((PADDING+ RADIUS)*2).toInt(); //以下被注释的代码,官方为我们提供了API->resolveSize() // val specWidthMode = MeasureSpec.getMode(widthMeasureSpec); // val specWidthSize = MeasureSpec.getSize(widthMeasureSpec); // val width = when(specWidthMode){ // MeasureSpec.EXACTLY -> specWidthSize // MeasureSpec.AT_MOST -> if (size>specWidthSize) specWidthSize else size // else -> size // } val width = View.resolveSize(size,widthMeasureSpec); val height = View.resolveSize(size,heightMeasureSpec); setMeasuredDimension(width,height) }效果如下,是我们期待的结果了
onMeasure、onLayout自定义Layout
- 重写onMeasure()
- 遍历每个子View,测量子View
- 测量完成后,得出子view的实际位置和尺寸,并暂时保存
- 有些子View可能需要重新测量
- 测量出所有子View的位置和尺寸后,计算出自己的尺寸,并用setMeasuredDimension(width,height)保存
- 遍历每个子View,测量子View
- 重写onLayout()
- 遍历每个子view,调用他们的layout()方法将位置和尺寸传给他们
- 首先写一个随机字体大小和颜色的TextView:ColoredTextView
private val COLORS = intArrayOf( Color.parseColor("#E91E63"), Color.parseColor("#673AB7"), Color.parseColor("#3F51B5"), Color.parseColor("#2196F3"), Color.parseColor("#009688"), Color.parseColor("#FF9800"), Color.parseColor("#FF5722"), Color.parseColor("#795548") ) private val TEXT_SIZES = intArrayOf(16, 22, 28) private val CORNER_RADIUS = 4.dp private val X_PADDING = 16.dp.toInt() private val Y_PADDING = 8.dp.toInt() /** * 随机字体大小和颜色的TextView */ class ColoredTextView(context: Context?, attrs: AttributeSet?) : AppCompatTextView(context, attrs) { private var paint = Paint(Paint.ANTI_ALIAS_FLAG) private val random = Random() init { setTextColor(Color.WHITE) textSize = TEXT_SIZES[random.nextInt(3)].toFloat() paint.color = COLORS[random.nextInt(COLORS.size)] setPadding(X_PADDING, Y_PADDING, X_PADDING, Y_PADDING) } @RequiresApi(VERSION_CODES.LOLLIPOP) override fun onDraw(canvas: Canvas) { canvas.drawRoundRect(0f, 0f, width.toFloat(), height.toFloat(), CORNER_RADIUS, CORNER_RADIUS, paint) super.onDraw(canvas) } } - 然后写tagLayout
- 重写onlayout:遍历每个子view,调用他们的layout()方法将位置和尺寸传给他们
- 核心是onMeasure
- 要对每一个子view进行测量,并进行折行处理
- 最后算出布局需要占用的宽度和高度
- 将总的宽度和高度通过setMeasuredDimension(width,height)的方式保存
class TagLayout(context: Context?, attrs: AttributeSet?) : ViewGroup(context, attrs) { private val childrenBounds = mutableListOf<Rect>() override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { //先遍历所有子view 计算尺寸 val count: Int = getChildCount()//子view数量 var widthUsed = 0 // var lineWidthUsed = 0 //每行已用的宽度 var heightUsed = 0 var lineMaxHeight = 0; val specWidthSize = MeasureSpec.getSize(widthMeasureSpec) val specWidthMode = MeasureSpec.getMode(widthMeasureSpec) for (index in 0 until count) { val child: View = getChildAt(index) //第一次测量(每一个子view) measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,heightUsed) //折行处理, 重置下一行参数 if (specWidthMode!=MeasureSpec.UNSPECIFIED && lineWidthUsed+child.measuredWidth>specWidthSize){ lineWidthUsed = 0 heightUsed +=lineMaxHeight lineMaxHeight = 0 measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,heightUsed) } //childrenBounds 添加元素 if (index >= childrenBounds.size){ childrenBounds.add(Rect()) } val childBounds = childrenBounds[index]; childBounds.set(lineWidthUsed,heightUsed,lineWidthUsed+child.measuredWidth,heightUsed+child.measuredHeight) lineWidthUsed += child.measuredWidth //保证tagLayout宽度为最宽那一行的宽度 widthUsed = Math.max(widthUsed,lineWidthUsed) //每一行的最大高度 lineMaxHeight = Math.max(lineMaxHeight,child.measuredHeight) } //再算出自己的尺寸 val selfWidth = widthUsed//最宽行的宽度 val selfHeight = lineMaxHeight+heightUsed//之前所有行的高度加上当前行的最大高度 setMeasuredDimension(selfWidth,selfHeight) } override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { val count: Int = getChildCount() for (index in 0 until count) { val view: View = getChildAt(index) val childBounds = childrenBounds[index] view.layout(childBounds.left,childBounds.top,childBounds.right,childBounds.bottom); } } override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams { return MarginLayoutParams(context,attrs) } } - 在xml中使用
<com.dsh.txlessons.viewcustomize.TagLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.dsh.txlessons.viewcustomize.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="北京市" />
<com.dsh.txlessons.viewcustomize.ColoredTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="天津市" />
...
</com.dsh.txlessons.viewcustomize.TagLayout>
- 效果如下