今天给大家解析一下自定义流式布局的编写,以及分析一下写代码过程遇到的难点。该布局支持水平垂直方向和子view gravity选择,先看一下运行的效果,左边是垂直布局,右边是水平布局,套一个scrollview就支持滑动了
编写过程中需要自定义一个LayoutParams,这个LayoutParams类是要继承子父类的LayoutParams,在编码过程中需要将子view的ViewGroup.LayoutParams转成自定义的LayoutParams
LayoutParams childLayoutParams = (LayoutParams) child.getLayoutParams();
这行代码如果没有进行处理就会报错,这时候就需要重写几个函数
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attributeSet) {
return new LayoutParams(getContext(), attributeSet);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
为什么需要重写这几个函数呢?我们去看看ViewGroup的源码就知道了
private void addViewInner(View child, int index, LayoutParams params,
boolean preventRequestLayout) {
....
//注意该params为child的LayoutParams
if (!checkLayoutParams(params)) {
params = generateLayoutParams(params);
}
....
}
在添加每个子view的时候都会先检测该子view的params是不是该父view的LayoutParams类型,如果不是,则会调用generateLayoutParams函数将ViewGroup.LayoutParams类型转换为自定义的LayoutParams类型。而generateDefaultLayoutParams函数则是子view无layoutparmas的时候生成一个默认layoutparams。复写这四个函数之后,上面那行代码就不会出异常了。
为了方便使用,需要自定义attr,但是attr的使用不是很熟悉,这里有个链接:
http://stackoverflow.com/questions/3441396/defining-custom-attrs
参考上面的方式,自定了自己的attr
<declare-styleable name="FlowLayout">
<attr name="orientation" format="enum">
<enum name="vertical" value="0"/>
<enum name="horizontal" value="1"/>
attr>
<attr name="childGravity">
<flag name="top" value="0x30"/>
<flag name="bottom" value="0x50"/>
<flag name="left" value="0x03"/>
<flag name="right" value="0x05"/>
<flag name="center" value="0x11"/>
attr>
<attr name="verticalSpacing" format="dimension"/>
<attr name="horizontalSpacing" format="dimension"/>
declare-styleable>
关于gravity的使用,自己看了一下源码,具体了解了一下原理,它总共用了8位二进制来表示,前面4位用来表示y轴,后4位用来表示x轴,具体可以看Gravity类,这个类中有两个掩码HORIZONTAL_GRAVITY_MASK(00000111)和VERTICAL_GRAVITY_MASK(01110000),接着如果把所有的gravity变量都用二进制来表示,就很明了地知道这些变量与和或之后的结果是另外哪个gravity变量。
最后就来直接看看代码,整个类最复杂的就是onMeasure函数了,这个函数中需要做大量的计算和处理,先贴出来代码:
FlowLayout.class类
public class FlowLayout extends ViewGroup{
public static final int VERTICAL = 0;
public static final int HORIZONTAL = 1;
private final int CENTER = 1;
private final int TOP = 2;
private final int BOTTOM =3;
private final int LEFT = 4;
private final int RIGHT = 5;
//默认间隙
private int verticalSpacing = 10;
private int horizontalSpacing = 10;
//布局方向
private int orientation = HORIZONTAL;
//子view放置gravity
private int childGravity = 1;
public FlowLayout(Context context) {
this(context, null);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
getAttrValue(attrs);
}
private void getAttrValue(AttributeSet attrs){
TypedArray typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.FlowLayout);
verticalSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_verticalSpacing, 10);
horizontalSpacing = typedArray.getDimensionPixelSize(R.styleable.FlowLayout_horizontalSpacing, 10);
orientation = typedArray.getInt(R.styleable.FlowLayout_orientation, HORIZONTAL);
int gravity = typedArray.getInt(R.styleable.FlowLayout_childGravity, Gravity.TOP);
if (orientation == HORIZONTAL) {
gravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
if (gravity == Gravity.TOP)
childGravity = TOP;
else if (gravity == Gravity.BOTTOM)
childGravity = BOTTOM;
else
childGravity = CENTER;
}else{
gravity = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
if (gravity == Gravity.LEFT)
childGravity = LEFT;
else if (gravity == Gravity.RIGHT)
childGravity = RIGHT;
else
childGravity = CENTER;
}
typedArray.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (getChildCount() <= 0){
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
int paddingTop = getPaddingTop();
int paddingLeft = getPaddingLeft();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
int width;
int height;
int childWidth;
int childHeight;
//该行最大子view大小
int maxChildSize = 0;
//剩余大小
int lastSize;
//水平布局,宽度固定,高度变化
if (orientation == HORIZONTAL) {
width = MeasureSpec.getSize(widthMeasureSpec);
height = 0;
lastSize = width - paddingLeft - paddingRight;
//如果第一个子view的大小已经超过容器大小
if (lastSize < getChildAt(0).getLayoutParams().width)
throw new ChildSizeTooLongException("the 0 child's width too long");
}
//垂直布局,高度固定,宽度变化
else{
width = 0;
height = MeasureSpec.getSize(heightMeasureSpec);
lastSize = height - paddingTop - paddingBottom;
//如果第一个子view的大小已经超过容器大小
if (lastSize < getChildAt(0).getLayoutParams().height)
throw new ChildSizeTooLongException("the 0 child's height too long");
}
//每行的第一个item的序号
int firstItemOfLine = 0;
//x,y坐标
int x = paddingLeft;
int y = paddingTop;
int childSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
childHeight = lp.height;
childWidth = lp.width;
if (childHeight <= 0 || childWidth <= 0) {
child.measure(childSpec, childSpec);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
}
child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
if (orientation == HORIZONTAL) {
lastSize = lastSize - childWidth - horizontalSpacing;
}else{
lastSize = lastSize - childHeight - verticalSpacing;
}
//需要换行
if (lastSize < 0) {
if (orientation == HORIZONTAL) {
//根据gravity将上一行的子view放置在正确的位置上
for (int j=firstItemOfLine; jif (childGravity == TOP){
//默认,无需处理
}else if (childGravity == BOTTOM){
childLayoutParams.y += maxChildSize - lineChild.getMeasuredHeight();
}else if (childGravity == CENTER){
childLayoutParams.y += (maxChildSize - lineChild.getMeasuredHeight())/2;
}
}
//将大小重置
lastSize = width - paddingLeft - paddingRight - childWidth;
//换行之后该行的第一个view大小超过整体父view大小
if (lastSize < 0)
throw new ChildSizeTooLongException("the " + i + " child's width too long");
//高换行
height += maxChildSize + verticalSpacing;
//换行之后的第一行坐标
x = paddingLeft;
y += maxChildSize + verticalSpacing;
//将最大高度值置为这第一个view的高度
maxChildSize = childHeight;
}else{
//根据gravity将上一行的子view放置在正确的位置上
for (int j=firstItemOfLine; jif (childGravity == LEFT){
//默认,无需处理
}else if (childGravity == RIGHT){
childLayoutParams.x += maxChildSize - lineChild.getMeasuredWidth();
}else if (childGravity == CENTER){
childLayoutParams.x += (maxChildSize - lineChild.getMeasuredWidth())/2;
}
}
//将大小重置
lastSize = height - paddingTop - paddingBottom - childHeight;
//换行之后该行的第一个view大小超过整体父view大小
if (lastSize < 0)
throw new ChildSizeTooLongException("the " + i + " child's height too long");
//宽换列
width += maxChildSize + horizontalSpacing;
//换列之后的第一列坐标
x += maxChildSize + horizontalSpacing;
y = paddingTop;
//将最大宽度值置为这第一个view的宽度
maxChildSize = childWidth;
}
//换行之后的第一个item序号
firstItemOfLine= i;
}
//不需要换行
else {
if (orientation == HORIZONTAL) {
//计算出这一行子view中高度最大的view
maxChildSize = maxChildSize > childHeight ? maxChildSize : childHeight;
}else{
//计算出这一列子view中宽度最大的view
maxChildSize = maxChildSize > childWidth ? maxChildSize : childWidth;
}
}
lp.setXY(x, y);
if (orientation == HORIZONTAL) {
x += childWidth + horizontalSpacing;
}else{
y += childHeight + verticalSpacing;
}
}
if (orientation == HORIZONTAL) {
height += maxChildSize;
height += + paddingBottom + paddingTop;
heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
//不要忘记最后一行
for (int i=firstItemOfLine; iif (childGravity == TOP){
//默认,无需处理
}else if (childGravity == BOTTOM){
childLayoutParams.y += maxChildSize - lineChild.getMeasuredHeight();
}else if (childGravity == CENTER){
childLayoutParams.y += (maxChildSize - lineChild.getMeasuredHeight())/2;
}
}
}else{
width += maxChildSize;
width += paddingLeft + paddingRight;
//不要忘记最后一列
for (int i=firstItemOfLine; iif (childGravity == LEFT){
//默认,无需处理
}else if (childGravity == RIGHT){
childLayoutParams.x += maxChildSize - lineChild.getMeasuredWidth();
}else if (childGravity == CENTER){
childLayoutParams.x += (maxChildSize - lineChild.getMeasuredWidth())/2;
}
}
}
setMeasuredDimension(resolveSize(width, widthMeasureSpec), resolveSize(height, heightMeasureSpec));
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
for (int i = 0; i < count; i++) {
View child = getChildAt(i);
LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.layout(lp.x, lp.y, lp.x + child.getMeasuredWidth(), lp.y + child.getMeasuredHeight());
}
}
/**
* 设置布局方向
* @param orientation {@link #HORIZONTAL}or{@link #VERTICAL}
*/
public void setOrientation(int orientation){
if (orientation!=HORIZONTAL && orientation!=VERTICAL)
throw new IllegalArgumentException("orientation error");
this.orientation = orientation;
invalidate();
}
public int getOrientation(){
return orientation;
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
return p instanceof LayoutParams;
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attributeSet) {
return new LayoutParams(getContext(), attributeSet);
}
@Override
protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
public static class LayoutParams extends ViewGroup.LayoutParams{
public int x;
public int y;
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
}
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
}
public LayoutParams(int width, int height) {
super(width, height);
}
public void setXY(int x, int y){
this.x = x;
this.y = y;
}
}
public static class ChildSizeTooLongException extends RuntimeException{
public ChildSizeTooLongException(String message){
super(message);
}
}
}
onMeasure函数第一步根据布局方向来确定整个view的宽或高,固定一个值不变,去计算另外一个值大小。第二步,循环该view所有子view,先获取子view宽和高,如果未知就让子view自己去计算宽和高;接着根据子view的大小去计算是否需要换行,垂直布局和水平布局的处理方式有些许差异;最后计算出子view的x和y轴坐标,并且赋值到该子view的layoutparams中即可。第三步,根据以上的计算结果最后统计出整个父view的大小并且调用setMeasuredDimension方法收尾即可。
onLayout函数非常简单,就是根据onMeasure函数中的计算结果x和y来布局所有子view。
最后还有一个问题就是
Method 'onMeasure' is too complex to analyze by data flow algorithm
由于在onMeasure函数里面的计算和处理代码有点多,导致在实际onMeasure操作时有些耗时,经测算,在HTC one m8t上面加入1000个子view,onMeasure函数会执行400ms左右,在500个以上就能明显感觉到卡顿,这个需要怎么处理,onMeasure函数我已经优化过了,留下的都是一些必要的操作,求指点~~
我将代码放在了自己不才写的一个框架中:
https://github.com/zhaozepeng/Android_framework
布局类FlowLayout.class代码位于com.android.libcore_ui.widget.FlowLayout中
测试类FlowLayoutActivity.class代码位于com.android.sample.test_widget.FlowLayoutActivity中。
最后可以厚颜无耻的要一颗星么~(@^_^@)~