简介
Android自定义View,ViewGroup是在开发中使用的比较多的,学习和掌握自定View,是中高级开发必须掌握的技能,本文使用一个自定义FlowLayout,来介绍自定义ViewGroup的流程与需要注意的地方
实践
- 我们先来看看最后的实现效果
- 接下来我们来实现这个效果,首先我们新建一个类,继承至ViewGroup,并且重写其onMeasure和onLayout两个方法,ViewGroup有4个构造方法,我们可以根据需要,重写其构造方法
class FlowLayout : ViewGroup {
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int)
: super(context, attributeSet, defStyleAttr)
/**
* 测量控件大小
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
}
/**
* 布局
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
}
}
onMeasure:测量控件大小的,我们定义的是ViewGroup,所以里面子view的大小都需要在这测量,测量后我们就可以拿到 控件的大小;
注意:onMeasure是有可能执行多次的,像ViewPager就调用了两次,详细可以看ViewPager的onMeasure方法源码,onMeasure调用几次,取决于它的父ViewGroup,它的父ViewGroup调用了多次measure,onMeasure就会执行多次
onLayout:布局,这个地方可以对控件进行布局,决定子view的排列方式,显示的位置;
- 测量子View的大小
怎么测量子view的大小呢?其实ViewGroup已经给我提供了3个方法来测量子view的大小
//循环遍历所有的子view,调用measureChild来测量子view大小
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
//测量单个view的大小,会考虑ViewGroup的padding
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
//测量单个view的大小,会考虑ViewGroup的padding 和子view的margin
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
因为我们需要考虑到子view的margin,所以我们可以使用第三个方法来测量子view的大小,当然我们也可以参考它的写法,自己来写
首先我们要获取到自定义ViewGroup的padding,然后在循环遍历所有子view,测量每一个view的大小
/**
* 测量
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//获取控件设置的内边距
val paddingLeft = paddingLeft
val paddingRight = paddingRight
val paddingTop = paddingTop
val paddingBottom = paddingBottom
//先测量子view的大小
for (index in 0 until childCount) {
val childView = getChildAt(index)
//获取子view的LayoutParams --> MarginLayoutParams
val childLP = childView.layoutParams as MarginLayoutParams
//将LayoutParam转变成为MeasureSpec
val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
paddingLeft + paddingRight + childLP.leftMargin + childLP.rightMargin, childLP.width)
val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
paddingTop + paddingBottom + childLP.topMargin + childLP.bottomMargin, childLP.height )
//测量子view的大小
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}
}
我们可以看到, 我们先获取到了子view的LayoutParams,然后使用getChildMeasureSpec()这个方法获取到了子view的MeasureSpec,然后调用子view的measure方法来度量子view的宽高
那么getChildMeasureSpec方法,measure方法分别干了什么事情呢?MeasureSpec又是什么呢?详情可以看我另一篇文章
自定义ViewGroup之measureSpec
这个地方有一个需要注意的地方,childView.layoutParams拿到的LayoutParams是无法直接强转MarginLayoutParams的,而只有MarginLayoutParams才能获取到margin,所以 需要我们重写ViewGroup的这几个方法
/**
* 重写generateLayoutParams方法,返回自定义的LayoutParams
* 使其可以获取Margin值
*/
override fun generateLayoutParams(p: LayoutParams?): LayoutParams? {
return MarginLayoutParams(p)
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams? {
return MarginLayoutParams(context, attrs)
}
override fun generateDefaultLayoutParams(): LayoutParams? {
return MarginLayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT
)
}
- 测量完子view后,我们就可以获取到子view的大小了,获取到子view大小后,我们还需计算出我们自定义的FlowLayout的实际大小,并且调用setMeasuredDimension()方法保存起来
在计算我们的FlowLayout的实际大小时,因为我们的子view显示不下的时候是需要换行的,所以我们高度就是所有行的高度之和,宽度就是最宽的那一行的宽度
那我们怎么知道什么时候应该换行呢?就是当前行大于我们的FlowLayout的宽度的时候,就应该换行,但是我们现在又在计算FlowLayout的大小,那我们怎么获取呢?
在onMeasure方法中有两个参数widthMeasureSpec和heightMeasureSpec,这个就是父ViewGroup给我们measureSpec,然后使用MeasureSpec.getSize()这个方法获取到父控件可以提供给你最大的布局空间,详情可以参考自定义ViewGroup之MeasureSpec,现在所有的问题都解决了,我们来看看代码:
class FlowLayout : ViewGroup {
//每一个Item横向间距
private var mHorizontalSpacing = dp2px(0f)
//每一行Item纵向间距
private var mVerticalSpeaing = dp2px(0f)
/**
* 设置每个item的横向边距
*/
fun setHorizontalSpacing(hSpacing: Float) {
mHorizontalSpacing = dp2px(hSpacing)
}
/**
* 设置每个item的纵向边距
*/
fun setVerticalSpacing(vSpacing: Float) {
mVerticalSpeaing = dp2px(vSpacing)
}
//保存每一行的view的数据
val allLineViews: MutableList<MutableList<View>> = mutableListOf()
//保存每一行的高度
val allLineHeights: MutableList<Int> = mutableListOf()
constructor(context: Context) : super(context)
constructor(context: Context, attributeSet: AttributeSet) : super(context, attributeSet)
constructor(context: Context, attributeSet: AttributeSet, defStyleAttr: Int) : super(context, attributeSet, defStyleAttr)
init {
//初始化代码
}
/**
* 测量
*/
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
//清空上次保存的数据
allLineHeights.clear()
allLineViews.clear()
//获取控件设置的内边距
val paddingLeft = paddingLeft
val paddingRight = paddingRight
val paddingTop = paddingTop
val paddingBottom = paddingBottom
//保存一行中所有的view
var lineViews: MutableList<View> = mutableListOf()
var lineWidthUsed = 0 //记录这行已经使用了多宽的size
var lineHeight = 0 //一行的行高
//解析的父ViewGroup给我的参考宽度
val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
//解析的父ViewGroup给我的参考高度
val selfHeight = MeasureSpec.getSize(heightMeasureSpec)
//所有子view中,最宽的值
var parentNeededWidth = 0
//所有子view的高度
var parentNeededHeight = 0
//记录一行,最大的topMargin和最大的bottomMargin
var maxTopMargin = 0
var maxBottomMargin = 0
//先测量子view的大小
for (index in 0 until childCount) {
val childView = getChildAt(index)
//获取子view的LayoutParams --> MarginLayoutParams
val childLP = childView.layoutParams as MarginLayoutParams
//将LayoutParam转变成为MeasureSpec
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight + childLP.leftMargin + childLP.rightMargin, childLP.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom + childLP.topMargin + childLP.bottomMargin,
childLP.height
)
//测量子view的大小
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
//获取子view测量后的宽高
val childMeasuredWidth = childView.measuredWidth
val childMeasuredHeight = childView.measuredHeight
//这一行的宽度 还需要加上当前view的margin
var lineWidth =
lineWidthUsed + childMeasuredWidth + mHorizontalSpacing + childLP.leftMargin + childLP.rightMargin
//当这一行的宽度大于父布局的宽度时,需要换行
if (lineWidth > selfWidth) {
//保存上一行的数据
allLineViews.add(lineViews)
allLineHeights.add(lineHeight)
//修改ViewGroup宽高 高度需要加上这一行最大的topMargin和最大的bottomMargin
parentNeededHeight += lineHeight + mVerticalSpeaing + maxTopMargin + maxBottomMargin
parentNeededWidth = Math.max(parentNeededWidth, lineWidth)
//初始化下一行的数据
lineWidth = childMeasuredWidth + mHorizontalSpacing + childLP.leftMargin + childLP.rightMargin
lineViews = mutableListOf()
lineHeight = 0
maxTopMargin = 0
maxBottomMargin = 0
}
//view是分行的layout,所以药记录每一行有哪些view
lineViews.add(childView)
//记录这一行,最大的topMargin和最大的bottomMargin
maxTopMargin = Math.max(maxTopMargin, childLP.topMargin)
maxBottomMargin = Math.max(maxBottomMargin, childLP.bottomMargin)
//更新每一行的宽和高
lineWidthUsed = lineWidth
lineHeight = Math.max(lineHeight, childMeasuredHeight)
//处理最后一行的数据
if (index == childCount - 1) {
allLineViews.add(lineViews)
allLineHeights.add(lineHeight)
//修改宽高
parentNeededHeight += lineHeight + mVerticalSpeaing + maxTopMargin + maxBottomMargin
parentNeededWidth = Math.max(parentNeededWidth, lineWidth)
}
}
//根据子view度量的结果,来重新度量自己ViewGroup
//作为一个ViewGroup,它自己也是一个View,它的大小也要根据它的父View给他提供的宽高来度量
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heigthMode = MeasureSpec.getMode(heightMeasureSpec)
//如果是实际大小,则直接使用设置的具体值
val realWidth = if (widthMode == MeasureSpec.EXACTLY) selfWidth else parentNeededWidth
val realHeight = if (heigthMode == MeasureSpec.EXACTLY) selfHeight else parentNeededHeight
//保存测量的大小
setMeasuredDimension(realWidth, realHeight)
}
}
这里需要特别注意的是,我们需要根据父ViewGroup给我们的widthMeasureSpec,调用MeasureSpec.getMode()方法,获取到MeasureSpec,如果MeasureSpec等于MeasureSpec.EXACTLY(确定的大小),则使用MeasureSpec.getSize()方法获取到的大小,如果是其它的,则使用我们计算出来的大小,最后调用setMeasuredDimension(realWidth, realHeight)保存我们ViewGroup的实际大小
- 测量完成后,我们就需要在onLayout里面确定子view的位置,在开始前,我们需要了解Android中的两种坐标系
一种是以Android屏幕左上角为原点,一种是根据父ViewGroup来确定位置,我们这里必须使用第二种,下面我们来看看代码
/**
* 布局
*/
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var curL = paddingLeft
var curT = paddingTop
for ((index, itemViewList) in allLineViews.withIndex()) {
//这一行的高度
val lineHeight = allLineHeights[index]
//记录这一行,最大的topMargin和最大的bottomMargin
var maxTopMargin = 0
var maxBottomMargin = 0
for (view in itemViewList) {
//获取子view的LayoutParams --> MarginLayoutParams
val childLP = view.layoutParams as MarginLayoutParams
//需要加上margin
val left = curL + childLP.leftMargin
val top = curT + childLP.topMargin
val right = left + view.measuredWidth
val bottom = top + view.measuredHeight
view.layout(left, top, right, bottom)
//下一个view的左边距离需要加上当前view的rightMargin
curL = right + mVerticalSpeaing + childLP.rightMargin
//记录这一行,最大的topMargin和最大的bottomMargin
maxTopMargin = Math.max(maxTopMargin, childLP.topMargin)
maxBottomMargin = Math.max(maxBottomMargin, childLP.bottomMargin)
}
//下一行距离父布局top的距离需要加上 上一行最大的topMargin和最大的bottomMargin
curT += lineHeight + mVerticalSpeaing + maxTopMargin + maxBottomMargin
curL = paddingLeft
}
}
好了,到这我们的自定义FlowLayout 就大功告成了,我们可以在布局中使用FlowLayout,在里面随便加一几个view,就实现文章开头所展示的效果啦