Android开发中,总会遇到这样和那样的需求。虽然官方已经给咱们提供了丰富的ViewGroup
和View
的实现,可是总有无法知足需求的时候。这个时候咱们该怎么办呢? 首先遇事不决能够先Google一下,看看有无现成的轮子。若是有轮子,那么恭喜,扒来改改就好啦。若是没有轮子,那能咋办,只能本身造轮子咯。其实使用轮子更多时候是追求稳定和节约时间,咱们仍是须要对轮子的原理有必定的了解的。java
流式布局
在Android开发中使用的场景应该仍是比较多的,好比标签展现、搜索历史记录展现等等。这种样式的布局Android目前是没有原生的ViewGroup
的,固然你要找轮子确定也是很容易找到的,不过今天我仍是想以自定义ViewGroup
的方式来实现这么一个容器。web
首先咱们得弄清楚ViewGroup
是什么,还有它的职责。markdown
ViewGroup
继承自View
,并实现了ViewManager
和ViewParent
接口。按照官方的定义,ViewGroup
是一个特别的View
,它能够容纳其余的View
,它实现了一系列添加和删除View
的方法。同时ViewGroup
还定义了LayoutParams
,LayoutParams
会影响View
在ViewGroup
的位置和大小相关属性。app
ViewGroup
也是个抽象类,须要咱们重写onLayout
方法,固然仅仅重写这么一个方法是不够的。ViewGroup
自己只是实现了容纳View
的能力,实现一个ViewGroup
咱们须要完成对自身的测量、对child的测量、child的布局等一系列的操做。ide
这是自定义View
实现的一个很是重要的方法,无论咱们是自定义View
也好,仍是自定义ViewGroup
都须要实现它。这个方法来自于View
,ViewGroup
自己没有去处理这个方法。这个方法会传递两个参数,分别是widthMeasureSpec
和heightMeasureSpec
。这两个数值实际上是个混合的信息,他们包含了具体的宽高数值和宽高的模式。这里须要说一下MeasureSpec
。oop
MeasureSpec
是View
的内部类,他是父容器给孩子传递的布局信息的一个压缩体。上文提到的传递的数值,实际上是经过MeasureSpec
的makeMeasureSpec
方法生成的:布局
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size, @MeasureSpecMode int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
//...
复制代码
其实MeasureSpec表明一个32位的int值,高2位表示SpecMode,低30位表示SpecSize,咱们能够分别经过getMode
和getSize
获取对应的信息。表示什么信息算是搞清楚了,那么这些信息又是如何确认的呢?this
在ViewGroup
中有个getChildMeasureSpec
方法,这个方法的实现基本能够解答咱们的疑问google
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
复制代码
代码长度仍是有点长,可是逻辑并不复杂。spec参数为ViewGroup的相关信息,padding则为ViewGroup的leftPadding+rightPadding+childLeftMargin+childRightMargin+usedWidth,childDimension为child的LayoutParams中指定的宽高信息。spa
child的具体的MeasureSpec
会受到父容器的影响,也和自身的布局信息有关,具体以下:
这个specMode,简单的来讲EXACTLY就表明宽高信息是比较确认的,AT_MOST则是会告诉你一个最大宽度,实际宽度由你本身确认,UNSPECIFIED也是会告诉你一个父容器宽度,你也能够设置为任意高度。
上面说了一堆关于MeasureSpec的,如今再来讲一下onMeasure方法里应该作什么。
若是是自定义View,咱们须要根据父容器传递的MeasureSpec来确认自身的宽高。若是是MeasureMode是EXACTLY,则这个View的宽高就是传递过来的size,若是是AT_MOST和UNSPECIFIED,则须要咱们自行处理了。在咱们计算获得了一个想要的宽高信息后,须要调用setMeasuredDimension
的方法来保存信息。
若是是自定义ViewGroup,那咱们须要作的事情可能就要多一点了,首先咱们也仍是同样,须要确认ViewGroup自身的宽高信息,若是都是EXACTLY拿很好办,直接设置对应的size便可。若是想要支持WRAP_CONTENT,这时候可能就会比较麻烦一点了。首先咱们得想好一点,这个ViewGroup是如何为child布局的。这很重要,由于不一样的布局方式,child的排布不一样,都会影响实际占用的空间。
仍是以LinearLayout
举例吧,LinearLayout
支持横向排列和纵向排列,他们须要执行的测量逻辑都是不同的。若是是纵向排列,则须要遍历child,测量child,并累加他们的高度和margin,最后还要加上自身高度,这样累加出来的数值就是WRAP_CONTENT下,自身应该占用的高度。若是是横向排列,则须要遍历和累加child,并累加他们的宽度和margin等,原理都是差很少的。
总结一下,onMeasure
方法须要ViewGroup
结合父容器传递的MeasureSpec
,测量child,配合child的排布方式,确认自身的宽高。
onLayout
方法传递了5个参数,changed表示自身的位置或大小是否发生了改变,剩下的分别为left,top,right,bottom,决定了他在父容器的位置。这是一个相对坐标,起点并非屏幕的左上角。
那在这个方法里咱们应该作什么呢?若是是自定义View
的时候,咱们能够不用管这个方法。由于View
自己没有容纳child的能力,若是是ViewGroup
,这时候咱们就须要为child执行布局操做了。咱们须要遍历child,执行它们的layout方法。经过调用layout
方法,咱们能够传递left,top,right,bottom,肯定child在ViewGroup中的位置。一样的,这也是一个相对坐标,是依赖于父容器的。
事实上,onLayout
方法是在自身的layout
方法被调用后调用的。Android总体的布局体系自上而下一层层的调用,传递布局信息,最终确认了各个View在屏幕上的位置。
一般来讲,自定义ViewGroup
并不须要重写这个方法。这个方法用来作一些绘制操做,若是是自定义View
,那咱们则须要重写这个方法,实现一些绘制逻辑。
这两个概念仍是要说一下,理解一下它们的做用和实现原理。
View
自身的属性。若是须要让这个属性生效,在绘制和布局时候,咱们须要基于这个属性的数值作必定的偏移,在测量的时候,咱们也须要考虑它的数值,为最终测量结果添加上。View
在ViewGroup
中的布局,它一般是由LayoutParams
所定义的。有这个属性的时候,咱们在测量时候须要考虑到它,而且累加上,在布局的时候,须要根据响应的属性,进行必定的偏移。 道理都理清楚了,写代码就会简单不少了。流式布局大概的效果就是添加的VIew按一行或者一列有序排列,若是一行或者一列放不下了,则换到下一行排列。下面就简单实现一个流式布局来加深一下理解。
首先须要定义一个类,继承自ViewGroup:
public class FlowLayout extends ViewGroup {
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//todo 实现测量逻辑
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//todo 实现child的布局逻辑
}
}
复制代码
由于咱们须要支持margin
属性,因此咱们还须要这样一个LayoutParams
。ViewGroup
中已经定义了这样一个MarginLayoutParams
,咱们建立一个内部类,继承此类实现:
public static class LayoutParams extends ViewGroup.MarginLayoutParams {
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
}
复制代码
LayoutParams
中还能够本身去定义一些个性化的布局参数,这里就简单处理了。同时咱们还得注意如下几个方法:
/** * 直接调用 {@link #addView(View view)}的时候 用来生成默认的LayoutParams * * @return */
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(-2, -2);
}
/** * {@link #addView(View child, ViewGroup.LayoutParams params)}时候,用来检查布局参数是否正确 * * @param p * @return */
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
/** * 若是{@link #checkLayoutParams(ViewGroup.LayoutParams p)}返回false,会调用此方法生成LayoutParams * * @param p * @return */
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
if (p == null) {
return generateDefaultLayoutParams();
}
return new LayoutParams(p);
}
/** * 若是xml中的child,会调用此方法生成布局参数 * @param attrs * @return */
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
复制代码
注释我都写了,主要是用来用户addView时候的默认布局信息生成和检测,若是没处理好,可能会引发崩溃啥的。
接下来是测量方法:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.d(TAG, "onMeasure");
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
//横向宽度固定
int lineMaxHeight = 0;//当前行最高的行高
int currentLeft = getPaddingLeft();//当前child的起点left
int currentTop = getPaddingTop();//当前child的起点top
//去除paddingLeft 和 paddingRight即为可用宽度
int availableWidth = widthSize - getPaddingLeft() - getPaddingRight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {//gone的child 不处理
continue;
}
//测量child
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
int decoratedWidth = getDecoratedWidth(child);
int decoratedHeight = getDecoratedHeight(child);
if (currentLeft + decoratedWidth > availableWidth) {
//宽度超了 换行
currentLeft = decoratedWidth + getPaddingLeft();
currentTop += lineMaxHeight;//高度加上以前的最大高度
lineMaxHeight = decoratedHeight;
} else {
//若是不须要换行 只记录当前的最大高度。
currentLeft += decoratedWidth;
lineMaxHeight = Math.max(lineMaxHeight, decoratedHeight);
}
if (i == getChildCount() - 1) {
//最后一个元素了 咱们须要累加高度
currentTop += lineMaxHeight;
}
}
//保存宽高信息
setMeasuredDimension(widthSize, currentTop + getPaddingBottom());
} else if (heightMode == MeasureSpec.EXACTLY) {
//todo 实现纵向固定的流式布局
} else {
//todo 实现宽高都固定的流式布局
}
}
复制代码
测量逻辑并不复杂,首先判断ViewGroup的宽高模式,这里实现了宽度固定的流式布局的处理逻辑。咱们须要遍历全部的child,并调用测量方法肯定他们的宽高。同时要注意的是child若是不可见则须要跳过。由于宽度是固定的,因此咱们须要计算出自身的高度。getDecoratedWidth
获取的是child自身的宽度与自身的左右的margin
的和。遍历过程当中依此排列child,若是一行排不下了,则执行换行逻辑,并累加高度,最后得出高度,保存。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
Log.d(TAG, "onLayout l :" + l + " t :" + t + " r :" + r + " b :" + b);
int lineMaxHeight = 0;
int currentLeft = getPaddingLeft();//当前child的起点left
int currentTop = getPaddingTop();//当前child的起点top
int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == GONE) {//gone的child 不处理
continue;
}
int decoratedWidth = getDecoratedWidth(child);
int decoratedHeight = getDecoratedHeight(child);
LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
int childLeft, childTop;
if (currentLeft + decoratedWidth > availableWidth) {
//宽度超了 换行
currentLeft = decoratedWidth + getPaddingLeft();
currentTop += lineMaxHeight;//高度加上以前的最大高度
lineMaxHeight = decoratedHeight;
childLeft = getPaddingLeft() + +layoutParams.leftMargin;
childTop = currentTop + layoutParams.topMargin;
} else {
//若是不须要换行 只记录当前的最大高度。
childLeft = currentLeft + layoutParams.leftMargin;
childTop = currentTop + layoutParams.topMargin;
currentLeft += decoratedWidth;
lineMaxHeight = Math.max(lineMaxHeight, decoratedHeight);
}
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(), childTop + child.getMeasuredHeight());
}
}
复制代码
onLayout方法里我也只是实现了宽度固定下的逻辑。逻辑和测量时候的思路同样,在测量的时候咱们已经为每一个child确认了自身的宽高,在这里咱们就只须要调用layout
方法为每一个child执行布局逻辑便可。
最后上运行效果,由于是demo因此样式比较随意,不要在乎这些细节(#^.^#)
自定义ViewGroup大体的流程就是这样了,若是还有什么困惑还不解能够留言,我会用心解答。