简介
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,就实现文章开头所展示的效果啦
代码:FlowLayout