简介
???????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的大小
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);
}
}
}
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);
}
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
for (index in 0 until childCount) {
val childView = getChildAt(index)
val childLP = childView.layoutParams as MarginLayoutParams
val childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
paddingLeft + paddingRight + childLP.leftMargin + childLP.rightMargin, childLP.width)
val childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
paddingTop + paddingBottom + childLP.topMargin + childLP.bottomMargin, childLP.height )
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
}
}
我们可以看到, 我们先获取到了子view的LayoutParams,然后使用getChildMeasureSpec()这个方法获取到了子view的MeasureSpec,然后调用子view的measure方法来度量子view的宽高 那么getChildMeasureSpec方法,measure方法分别干了什么事情呢?MeasureSpec又是什么呢?详情可以看我另一篇文章 自定义ViewGroup之measureSpec 这个地方有一个需要注意的地方,childView.layoutParams拿到的LayoutParams是无法直接强转MarginLayoutParams的,而只有MarginLayoutParams才能获取到margin,所以 需要我们重写ViewGroup的这几个方法
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 {
private var mHorizontalSpacing = dp2px(0f)
private var mVerticalSpeaing = dp2px(0f)
fun setHorizontalSpacing(hSpacing: Float) {
mHorizontalSpacing = dp2px(hSpacing)
}
fun setVerticalSpacing(vSpacing: Float) {
mVerticalSpeaing = dp2px(vSpacing)
}
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
var lineViews: MutableList<View> = mutableListOf()
var lineWidthUsed = 0
var lineHeight = 0
val selfWidth = MeasureSpec.getSize(widthMeasureSpec)
val selfHeight = MeasureSpec.getSize(heightMeasureSpec)
var parentNeededWidth = 0
var parentNeededHeight = 0
var maxTopMargin = 0
var maxBottomMargin = 0
for (index in 0 until childCount) {
val childView = getChildAt(index)
val childLP = childView.layoutParams as MarginLayoutParams
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight + childLP.leftMargin + childLP.rightMargin, childLP.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom + childLP.topMargin + childLP.bottomMargin,
childLP.height
)
childView.measure(childWidthMeasureSpec, childHeightMeasureSpec)
val childMeasuredWidth = childView.measuredWidth
val childMeasuredHeight = childView.measuredHeight
var lineWidth =
lineWidthUsed + childMeasuredWidth + mHorizontalSpacing + childLP.leftMargin + childLP.rightMargin
if (lineWidth > selfWidth) {
allLineViews.add(lineViews)
allLineHeights.add(lineHeight)
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
}
lineViews.add(childView)
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)
}
}
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]
var maxTopMargin = 0
var maxBottomMargin = 0
for (view in itemViewList) {
val childLP = view.layoutParams as MarginLayoutParams
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)
curL = right + mVerticalSpeaing + childLP.rightMargin
maxTopMargin = Math.max(maxTopMargin, childLP.topMargin)
maxBottomMargin = Math.max(maxBottomMargin, childLP.bottomMargin)
}
curT += lineHeight + mVerticalSpeaing + maxTopMargin + maxBottomMargin
curL = paddingLeft
}
}
好了,到这我们的自定义FlowLayout 就大功告成了,我们可以在布局中使用FlowLayout,在里面随便加一几个view,就实现文章开头所展示的效果啦 代码:FlowLayout
|