拆零件,然后再把零件拼装回去,来来回回对其结构也就熟悉了
FrameLayout的特点
- 子View按照添加顺序层叠显示
- FrameLayout的尺寸与其最大子View(可见的)的尺寸相等(加上padding值)
- 如果要让GONE的子View参与计算,则需要把setMeasureAllChildren(boolean) ,setConsiderGoneChildrenWhenMeasuring()设置为true
- 支持通过layout_gravity控制子View的布局
根据上述特点,我也要实现一个简单的FrameLayout,应该怎么开始呢?
这里自然考虑继承ViewGroup,然后重写onMeasure,onLayout方法,而且onLayout方法必须实现(因为这是一个抽象方法,子类必须实现)
测量
这里测量的过程是:遍历该ViewGroup,调用子View的measure方法进行测量,同时通过子View的LayoutParams获取到子View的Margin信息,然后综合View的测量尺寸与Margin值,算出该ViewGroup的尺寸,然后设置该尺寸为ViewGroup的测量尺寸。
/*
* 尺寸测量
* */
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
var maxWidth: Int = 0
var maxHeight: Int = 0
//遍历子View进行测量
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility != View.GONE) {
//调用ViewGroup的一个实例方法(本质上是调用View的measure方法),
// 由于FrameLayout的特点,子View之间并不干扰尺寸大小,所以已经使用的空间为0
//该计算过程会计算padding和margin
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
var layoutParams = child.layoutParams as LayoutParams
//得到最大宽高,并考虑子View的Margin
maxWidth = Math.max(maxWidth, child.measuredWidth + layoutParams.leftMargin + layoutParams.rightMargin)
maxHeight = Math.max(maxHeight, child.measuredHeight + layoutParams.topMargin + layoutParams.bottomMargin)
//考虑FrameLayout本身的padding值
maxWidth += paddingLeft + paddingRight
maxHeight += paddingTop + paddingBottom
//设置尺寸
setMeasuredDimension(
resolveSize(maxWidth, widthMeasureSpec),
View.resolveSize(maxHeight, heightMeasureSpec)
)
}
}
}
注意一下,这里的LayoutParams并不是ViewGroup的LayoutParams,因为ViewGroup中的LayoutParams本身并不支持margin,而只支持宽高的属性,不过没关系,ViewGroup类中还有一个MarginLayoutParams,添加了对margin的支持。
而这里我们打算设计一个FrameLayout,那么,就得支持Margin与layout_gravity;
/*
* 子View通过LayoutParams告诉父View它想怎么布局
* {@link android.R.styleable#ViewGroup_Layout ViewGroup Layout Attributes} 包含了LayoutParams类支持的所有属性
* 包括layout_width和layout_height
* 作为基类的的LayoutParams 仅仅只描述View想要的宽高尺寸,对于每一个尺寸,可选则MATCH_PARENT和WRAP_CONTENT
* 其中MATCH_PARENT表示子View希望像父View一样大
* WRAP_CONTENT表示View需要足够大,以容纳它的内容(包括padding)
*
* 不同的ViewGroup子类具有不同的LayoutParams之类实现,用来添加各自的特性
* 如本类中的MarginLayoutParams可以用来添加对Margin和gravity的支持
* */
class LayoutParams : MarginLayoutParams {
companion object {
val UNSPECIFIED_GRAVITY = -1
}
//添加gravity支持
var gravity = UNSPECIFIED_GRAVITY
constructor(c: Context, attrs: AttributeSet?) : super(c, attrs) {
val typedArray = c.obtainStyledAttributes(R.styleable.KFrameLayout_Layout)
gravity = typedArray.getInt(R.styleable.KFrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY)
typedArray.recycle()
}
constructor(width: Int, height: Int) : super(width, height)
constructor(source: MarginLayoutParams?) : super(source)
constructor(source: ViewGroup.LayoutParams?) : super(source)
constructor(width: Int, height: Int, gravity: Int) : super(width, height) {
this.gravity = gravity
}
constructor(source: LayoutParams) : super(source) {
this.gravity = source.gravity
}
}
如果就只是这样自定义了自己的LayoutParams,然后将子View获取到的LayoutParams强制转换到自身的LayoutParams,这个自定义View并不能起作用,具体原因后面会进行说明。
布局
经过了测量过程,这里我们可以拿到测量尺寸了,然后遍历子View,进行相应的布局,主要就是定义子View的左边界,上边界的距离。
//ViewGroup的onLayout方法是抽象方法,子类必须实现
//此处根据gravity布局子View
//该过程的核心思想就是遍历子View,为子View找到四个坐标值,然后调用其子View自身的layout方法进行布局
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
layoutChildren(l, t, r, b)
}
private fun layoutChildren(l: Int, t: Int, r: Int, b: Int) {
val parentLeft = paddingLeft
val parentRight = r - l - paddingRight
val parentTop = paddingTop
val parentBottom = b - t - paddingBottom
for (i in 0 until childCount) {
val child = getChildAt(i)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
if (child.visibility != View.GONE) {
var childLeft: Int
var childTop: Int
val layoutParams: LayoutParams = child.layoutParams as LayoutParams
var gravity = layoutParams.gravity
if (gravity == -1) {
gravity = LayoutParams.UNSPECIFIED_GRAVITY
}
//从左到右布局,还是从右到左布局(国内极少使用)
val absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection)
val verticalGravity = gravity and Gravity.VERTICAL_GRAVITY_MASK
//横向
childLeft = when (absoluteGravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
Gravity.CENTER_HORIZONTAL -> {
//此处计算得理清概念
parentLeft + layoutParams.rightMargin - layoutParams.leftMargin + (parentRight - parentLeft - childWidth) / 2
}
Gravity.RIGHT -> {
parentRight - layoutParams.rightMargin - childWidth
}
else -> {
parentLeft + layoutParams.leftMargin
}
}
//纵向
childTop = when (verticalGravity) {
Gravity.CENTER_VERTICAL -> {
parentTop + layoutParams.bottomMargin - layoutParams.topMargin + (parentBottom - parentTop - childHeight) / 2
}
Gravity.BOTTOM -> {
parentBottom - layoutParams.bottomMargin
}
else -> {
parentTop + layoutParams.topMargin
}
}
child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight)
}
}
}
这个布局的过程,得根据自身的特点进行设置,例如FrameLayout由于子View之间并不互相影响,属于层叠关系,因为就不用考虑兄弟 View之间的布局关系处理了,这样就容易得多。
此处还有一个难点就是,你得理解清楚margin,padding的真实含义,以及布局中的left,right,居中布局的实际运算过程。
完善
前面说到过,LayoutParams如果直接强行转换会有问题的,至于有什么问题,就得从View的加载机制开始说起,今天暂不说明,后面有空继续,这里先写出解决方案,就是得重写几个方法:
override fun checkLayoutParams(p: ViewGroup.LayoutParams?): Boolean {
return p is LayoutParams
}
override fun generateLayoutParams(attrs: AttributeSet?): ViewGroup.LayoutParams {
return LayoutParams(context,attrs)
}
override fun generateLayoutParams(p: ViewGroup.LayoutParams?): ViewGroup.LayoutParams {
return LayoutParams(p)
}
override fun generateDefaultLayoutParams(): ViewGroup.LayoutParams {
return LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT)
}
源码
KComponent
https://github.com/jiangkang/KComponent