自定义容器本质上也是一个组件,常见的 LinearLayout、FrameLayout、GridLayout、ScrollView和 RelativeLayout 等等组件都是容器,容器除了有自己的外观,还能用来容纳各种组件,以一种特定的规则规定组件应该在什么位置、显示多大。
一般情况下,我们更关注自定义组件的外观及功能,但自定义容器则更关注其内的组件怎么排列和摆放,比如线性布局 LinearLayout 中的组件只能水平排列或垂直排列,帧布局 FrameLayout中的组件可以重叠,相对布局 RelativeLayout 中的组件可以以某一个组件为参照定位自身的位置……容器还关注组件与容器四个边框之间的距离(padding),或者容器内组件与组件之间的距离(margin)。
事实上,容器是可以嵌套的,一个容器中,既可以是普通的子组件,也可以是另一个子容器。
容器类一般要继承 ViewGroup 类,ViewGroup 类同时也是 View 的子类,如图所示,ViewGroup 又是一个抽象类,定义了 onLayout()等抽象方法。当然,根据需要,我们也可以让容器类继承自 FrameLayout 等 ViewGroup 的子类,比如 ListView 继承自 ViewGroup,而 ScrollView水平滚动容器类则从 FrameLayout 派生。
ViewGroup 作为容器类的父类,自然有他自己鲜明的特征,开发自定义容器必须先要了解ViewGroup。
在 ViewGroup 中,定义了一个 View[]类型的数组 mChildren,该数组保存了容器中所有的子组件,负责维护组件的添加、移除、管理组件顺序等功能,另一个成员变量 mChildrenCount 则保存了容器中子组件的数量。在布局文件(layout)中,容器中的子元素会根据顺序自动添加到 mChildren 数组中。
ViewGroup 具备了容器类的基本特征和运作流程,也定义了相关的方法用于访问容器内的
组件,主要的方法有:
public int getChildCount()
获取容器内的子组件的个数;
public View getChildAt(int index)
容器内的所有子组件都存储在名为 mChildren 的 View[]数组中,
该方法通过索引 index找到指定位置的子组件;
public void addView(View child, int index, LayoutParams params)
向容器中添加新的子组件,child 表示子组件(也可以是子容器),index 表示索引,指
定组件所在的位置,params 参数为组件指定布局参数,该方法还有两个简化的版本:
public void addView(View child, LayoutParams params):添加 child 子组件,并为该子组件指定布局参数;
public void addView(View child, int index):布局参数使用默认的 ViewGroup.LayoutParams,其中 layout_width 和 layout_height 均为 wrap_content;
public void addView(View child):布局参数同上,但 index 为-1,表示将 child 组件添加到 mChildren 数组的最后。
向容器中添加新的子组件时,子组件不能有父容器,否则会抛出“The specified child
already has a parent(该组件已有父容器)”的异常。
public void removeViewAt(int index)
移除 index 位置的子组件
类似的方法还有:
public void removeView(View view)
移除子组件 view;
public void removeViews(int start, int count)
移除从 start 开始连续的 count 个子组件。
protected void measureChild(View child,
int parentWidthMeasureSpec, int parentHeightMeasureSpec)
测量子组件的尺寸。
类似的方法还有:
protected void measureChildren(int widthMeasureSpec,
int heightMeasureSpec):
测量所有子组件的尺寸;
public final void measure(int widthMeasureSpec, int heightMeasureSpec):
该方法从 View类 中 继 承 , 用 于 测 量 组 件 或 容 器 自 己 的 尺 寸 ,
参数 widthMeasureSpec 和heightMeasureSpec 为 0 时表示按实际大小进行测量,
将 0 传入方法常常会有奇效。
ViewGroup 运行的基本流程大致为:
1) 测量容器尺寸
重写 onMeasure()方法测量容器大小,和自定义组件有所区别的是,在测量容器大小之
前,必须先调用 measureChildren()方法测量所有子组件的大小,不然结果永远为 0。
2) 确定每个子组件的位置
重写 onLayout()方法确定每个子组件的位置(这个其实挺麻烦,也是定义容器的难点部
分),在 onLayout()方法中,调用 View 的 layout()方法确定子组件的位置。
3) 绘制容器
重写 onDraw()方法,其实 ViewGroup 类并没有重写 onDraw()方法,除非有特别的要求,自定义容器也很少去重写。比如 LinearLayout 重写了该方法用于绘制水平或垂直分割条,而 FrameLayout 则是重写了 draw()方法,作用其实是一样的。
我们来看看容器类的基本结构。
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context) {
super(context);
}
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MyViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 确定每个子组件的位置
*
* @param changed 是否有新的尺寸或位置
* @param l left
* @param t top
* @param r right
* @param b bottom
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
}
/**
* 测量容器的尺寸
*
* @param widthMeasureSpec 宽度模式与大小
* @param heightMeasureSpec 高度模式与大小
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量所有子组件的大小
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
}
/**
* 绘制容器
*
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
第一节介绍过 View 的工作流程,ViewGroup 作为 View 的子类,流程基本是相同的,但
另一方面 ViewGroup 作为容器的父类,又有些差异,我们通过阅读源码来了解 ViewGroup 的工作原理。
前面说到,重写 ViewGroup 的 onMeasure()方法时,必须先调用 measureChildren()方法测量子组件的尺寸,该方法源码如下:
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);
}
}
}
measureChildren()方法中,循环遍历每一个子组件,如果当前子组件的可见性不为 GONE 也就是没有隐藏则继续调用 measureChild(child, widthMeasureSpec, heightMeasureSpec)方法测量当前子组件 child 的大小,我们继续进入 measureChild()方法。
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);
}
measureChild()方法结合父容器的 MeasureSpec、子组件的 Padding 和 LayoutParams 三个因素 利 用 getChildMeasureSpec() 计 算 出 子 组 件 的 尺 寸 模 式 和 尺 寸 大 小 ( 可 以 跟 踪 到getChildMeasureSpec()方法中查看),并调用子组件的 measure()方法进行尺寸测量。measure()方法的实现如下:
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
……
onMeasure(widthMeasureSpec, heightMeasureSpec);
……
}
真相慢慢露出水面,measure()方法调用了 onMeasure(widthMeasureSpec, heightMeasureSpec)方法,该方法正是我们重用的用来测量组件尺寸的方法,至此,测量组件尺寸的工作已掌握到开发人员手中。
根据上面的代码跟踪我们发现,从根元素出发,一步步向下递归驱动测量,每个组件又负责计算自身大小,OOP 的神奇之处就这样在实际应用中体现出来了。
接下来调用 onLayout()方法定位子组件,以确定子组件的位置和大小,在 onLayout()方法中,我们将调用子组件的 layout()方法,这里要一分为二,如果子组件是一个 View,定位流程到此结束,如果子组件又是一个容器呢?我们进入 layout()方法进行跟踪。
public void layout(int l, int t, int r, int b) {
……
int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED)
== PFLAG_LAYOUT_REQUIRED) {
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
……
}
……
}
如果子组件是一个容器,又会继续调用该容器的 onLayout()方法对孙组件进行定位,所以,onLayout()方法也是一个递归的过程。
onMeasure()方法和 onLayout()方法调用完成后,该轮到 onDraw()方法了,ViewGroup 类并没有重写该方法,但是,从第一章中我们都知道每一个组件在绘制时是会调用 View 的 draw()方法的,我们进入 draw()方法进行跟踪。
public void draw(Canvas canvas) {
……
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
// Step 6, draw decorations (scrollbars)
onDrawScrollBars(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// we're done...
return;
}
……
}
draw()方法中执行了语句 dispatchDraw(canvas),但是,当我们跟踪到 View类的 dispatchDraw()方法时发现该方法是空的,但对于 ViewGroup 来说,该方法的作用非同小可,ViewGroup 重写了dispatchDraw()方法。
protected void dispatchDraw(Canvas canvas) {
final int childrenCount = mChildrenCount;
final View[] children = mChildren;
……
for (int i = 0; i < childrenCount; i++) {
int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
final View child = (preorderedList == null)
? children[childIndex] : preorderedList.get(childIndex);
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
……
}
dispatchDraw()方法的作用是将绘制请求纷发到给子组件,并调用 drawChild()方法来完成子组件的绘制,drawChild()方法的源码如下:
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
return child.draw(canvas, this, drawingTime);
}
drawChild()方法再次调用了子组件的 boolean draw(Canvas canvas, ViewGroup parent, longdrawingTime)方法,该方法定义如下:
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
……
if (!layerRendered) {
if (!hasDisplayList) {
// Fast path for layouts with no backgrounds
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
dispatchDraw(canvas);
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().draw(canvas);
}
} else {
draw(canvas);
}
drawAccessibilityFocus(canvas);
} else {
mPrivateFlags &= ~PFLAG_DIRTY_MASK;
((HardwareCanvas) canvas).drawRenderNode(renderNode, null, flags);
}
}
……
}
上面的方法又调用了 draw(Canvas canvas)方法,如果子组件不再是一个容器,将调用 if
(!dirtyOpaque) onDraw(canvas)语句完成组件的绘制,同样地,onDraw(canvas)正是需要我们重写的方法。所以,组件的绘制同样是一个不断递归的过程。
在容器类的基本结构中,我们最陌生的是 onLayout()方法,该方法原型为: protected voidonLayout(boolean changed, int l, int t, int r, int b),其中,参数 changed 判断是否有新的大小和位置,l 表示 left,t 表示 top,r 表示 right,b 表示 bottom,后面的 4 个参数表示容器自己相对父容器的位置以及自身的大小,通常情况下,r - l 的值等同于方法 getMeasuredWidth()方法的返回值,b - t 的值等同于 getMeasuredHeight()方法的返回值。关于 l、t、r、b 参数的值的理解如图所示。
在 onLayout()方法中,需要调用 View 的 layout()方法用于定义子组件和子容器的位置,
layout()方法的原理如下:
public void layout(int l, int t, int r, int b)
参数 l、t、r、b 四个参数的作用与上面相同,通过这 4 个参数,基本可以确定子组件的位置与大小了。
下面我们来看一个案例,该案例一是为了说明自定义容器的定义方法,二是了解重写ViewGroup 方法的一些细节。
public class SizeViewGroup extends ViewGroup {
public SizeViewGroup(Context context) {
this(context, null, 0);
}
public SizeViewGroup(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SizeViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//创建一个按钮
TextView textView = new TextView(context);
ViewGroup.LayoutParams layoutParams =
new ViewGroup.LayoutParams(200, 200);
textView.setText("Android");
textView.setBackgroundColor(Color.YELLOW);
//在当前容器中添加子组件
this.addView(textView, layoutParams);
//设置容器背景颜色
this.setBackgroundColor(Color.alpha(255));
}
/**
* 确定组件位置与大小
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//设置子组件(此处为 TextView)的位置和大小
//只有一个组件,索引为 0
View textView = this.getChildAt(0);
textView.layout(50, 50, textView.getMeasuredWidth() + 50,
textView.getMeasuredHeight() + 50);
}
/**
* 测量组件
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量所有子组件的大小
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
//测量自身的大小,此处直接写死为 500 * 500
this.setMeasuredDimension(500, 500);
}
/**
* 绘制
*/
@Override
protected void onDraw(Canvas canvas) {
//为容器画一个红色边框
RectF rect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight());
rect.inset(2, 2);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(2);
paint.setColor(Color.BLACK);
Path path = new Path();
path.addRoundRect(rect, 20, 20, Path.Direction.CCW);
canvas.drawPath(path, paint);
super.onDraw(canvas);
}
}
上面的代码中,我们定义了一个容器,为了不增加难度,刻意做了简化。在 public
SizeViewGroup(Context context, AttributeSet attrs, int defStyleAttr)构造方法中,使用代码创建了一个 TextView 子组件,并将 SizeViewGroup 的背景设置成了透明(如果要绘制一个不规则的自定义组件,将背景设置成透明是个不错的办法)。onMeasure()方法用来测量容器大小,在测量容器之前,必须先调用 measureChildren()方法测量所有子组件的大小,本例中将容器的大小设置成了一个不变的值 500 * 500,所以,尽管在布局文件中将 layout_width 和 layout_height 都定义为match_parent,但事件上这个值并不起作用。onLayout()方法负责为子组件定位并确认子组件大小,因为只有一个子组件,所以先通过 getChildAt(0)获取子组件(TextView 对象)对象,再调用子组件的 layout()方法确定组件区域,子组件的 left 为 50,top 为 50,right 为 50 加上测量宽度,bottom为 50 加上测量高度。onDraw()方法为容器绘制了一个圆角矩形作为边框。运行效果如图 所示。
我们知道,一个组件多大,取决于 layout_width 和 layout_height 的大小,但真正决定组件
大小的是 layout()方法,我们将 onLayout()方法改成如下的实现:
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//设置子组件(此处为 TextView)的位置和大小
View textView = this.getChildAt(0);//只有一个组件,索引为 0
textView.layout(50, 50, 200, 100);
}
再次运行,结果如图所示。从结果中看出,此时 TextView 组件变小了,说明一个组件的真正大小不是取决于 layout_width 和 layout_height,而是取决于 layout()方法。但是很显然,通过layout()方法计算组件大小时,layout_width 和 layout_height 属性是非常重要的参考因素。
CornerLayout 布局是一个自定义容器,用于将子组件分别显示在容器的 4 个角落,不接受超过 4 个子组件的情形,默认情况下,子组件按照从左往右、从上往下的顺序放置,但可以为子组件指定位置(左上角 left_top、右上角 right_top、左下角 left_bottom、右下角 right_bottom)。如果说前面的案例为了简化形式有些偷工减料,那么本小节将以一个完整的案例来讲述自定义容器的基本实现。
先画一个草图来帮助我们分析,如图。
上图中,蓝色框表示 CornerLayout 布局的区域,A、B、C、D 是 CornerLayout 内的 4 个子组件,对于 CornerLayout 来说,首先要测量的是他的尺寸大小,当 layout_width 为 wrap_content 时,其宽度为 A、C 的最大宽度和 B、D 的最大宽度之和,当 layout_height 为 wrap_content 时,其高度为 A、B 的最大高度 C、D 的最大高度之和,这样才不至于子组件出现重叠。当然,如果layout_width 和 layout_height 指定了具体值或者屏幕不够大的情况下设置为 match_parent,子组件仍有可能会出现重叠现象。
我们先将 CornerLayout 容器的基本功能开发出来,基本功能包括:支持 0~4 个子组件;每个子组件放置在不同的角落;完美支持 layout_width 和 layout_height 属性。
public class CornerLayout extends ViewGroup {
public CornerLayout(Context context) {
super(context);
}
public CornerLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CornerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/**
* 定位子组件
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (i == 0) {
//定位到左上角
child.layout(0, 0, child.getMeasuredWidth(),
child.getMeasuredHeight());
} else if (i == 1) {
//定位到右上角
child.layout(getMeasuredWidth() - child.getMeasuredWidth(),
0, getMeasuredWidth(), child.getMeasuredHeight());
} else if (i == 2) {
//定位到左下角
child.layout(0, getMeasuredHeight() - child.getMeasuredHeight(),
child.getMeasuredWidth(), getMeasuredHeight());
} else if (i == 3) {
//定位到右下角
child.layout(getMeasuredWidth() - child.getMeasuredWidth(),
getMeasuredHeight() - child.getMeasuredHeight(),
getMeasuredWidth(), getMeasuredHeight());
}
}
}
/**
* 测量尺寸
*
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量所有子组件的大小
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
//再测量自己的大小
int width = this.measureWidth(widthMeasureSpec);
int height = this.measureHeight(heightMeasureSpec);
//应用尺寸
this.setMeasuredDimension(width, height);
}
/**
* 测量容器的宽度
*
* @param widthMeasureSpec
* @return
*/
private int measureWidth(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if (mode == MeasureSpec.EXACTLY) {
//match_parent 或具体值
width = size;
} else if (mode == MeasureSpec.AT_MOST) {
//wrap_content
int aWidth = 0;
int bWidth = 0;
int cWidth = 0;
int dWidth = 0;
for (int i = 0; i < this.getChildCount(); i++) {
if (i == 0)
aWidth = getChildAt(i).getMeasuredWidth();
else if (i == 1)
bWidth = getChildAt(i).getMeasuredWidth();
else if (i == 2)
cWidth = getChildAt(i).getMeasuredWidth();
else if (i == 3)
dWidth = getChildAt(i).getMeasuredWidth();
}
width = Math.max(aWidth, cWidth) + Math.max(bWidth, dWidth);
}
return width;
}
/**
* 测量容器的高度
*
* @param heightMeasureSpec
* @return
*/
private int measureHeight(int heightMeasureSpec) {
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if (mode == MeasureSpec.EXACTLY) {
//match_parent 或具体值
height = size;
} else if (mode == MeasureSpec.AT_MOST) {
//wrap_content
int aHeight = 0;
int bHeight = 0;
int cHeight = 0;
int dHeight = 0;
for (int i = 0; i < this.getChildCount(); i++) {
if (i == 0)
aHeight = getChildAt(i).getMeasuredHeight();
else if (i == 1)
bHeight = getChildAt(i).getMeasuredHeight();
else if (i == 2)
cHeight = getChildAt(i).getMeasuredHeight();
else if (i == 3)
dHeight = getChildAt(i).getMeasuredHeight();
}
height = Math.max(aHeight, bHeight) + Math.max(cHeight, dHeight);
}
return height;
}
}
我猜测大家可能有一个疑问,在计算每个子组件的宽度和高度时,用了 4 个 if 语句进行判断,如图所示,这样做的目的是为了在读取子组件对象时防止因子组件数量不够而出现下标越界,这个判断是必须而且有用的。
接下来对该组件进行测试,看是否达到我们预期的要求。我们通过修改 layout_width 和
layout_height 两个属性值来测试不同的运行结果。第一个测试的运行结果如图 所示,定义cornetlayout.xml 布局文件,内容如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<bczm.com.day0617.CornerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_bright" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_dark" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_red_dark" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_green_light" />
bczm.com.day0617.CornerLayout>
LinearLayout>
如图所示,将第一个子组件的宽度设置为 100dp,第四个子组件的高度设置为 100dp;
如图所示的运行结果中,我们只定义了三个子组件,效果依旧完美。
最后一种情况,就是将 CornerLayout 的 layout_width 和 layout_height 属性都设置为
match_parent,此时容器占用了整个屏幕空间,4 个子组件向四周扩散,如图 所示(横屏显示)。
我们为 CornerLayout 容器添加一个灰色的背景,因为没有设置子组件与容器边框的距离
(padding),所以子组件与容器边框是重叠的,如图所示。
如果考虑 padding 对容器带来的影响,那事情就变得复杂些了。默认情况下,容器内的
padding 会自动留出来,但如果不改子组件位置会导致子组件不能完全显示。另外,View 已经将padding 属性定义好开发人员无须自行定义,并且定义了 4 个方法分别用于读取 4 个方向的padding:
Ø public int getPaddingLeft() 离左边的 padding
Ø public int getPaddingRight() 离右边的 padding
Ø public int getPaddingTop() 离顶部的 padding
Ø public int getPaddingRight() 离底部的 padding
考虑 padding 属性之后,将给容器的宽度和高度以及子组件的定位带来影响,当 CornerLayout的 layout_width 为 wrap_content 时,其宽度 = A、C 的最大宽度 + B、D 的最大宽度 + 容器左边的 padding + 容器右边的 padding,当 layout_height 为 wrap_content 时,其高度 = A、B 的最大高度 + C、D 的最大高度 + 容器顶部的 padding + 容器底部的 padding。而在 onLayout()方法中定位子组件时,也同样需要为 padding 留出空间。
总结一句话就是容器的宽度和高度都变大了。
在 measureWidth()方法中,计算容器宽度时,加上了左边的 padding 和右边的 padding:
private int measureWidth(int widthMeasureSpec){
……
width = Math.max(aWidth, cWidth) + Math.max(bWidth, dWidth)
+ getPaddingLeft() + getPaddingRight();
……
return width;
}
在 measureHeight()方法中,计算容器高度时,加上了顶部的 padding 和底部的 padding:
private int measureHeight(int heightMeasureSpec){
……
height = Math.max(aHeight, bHeight) + Math.max(cHeight, dHeight)
+ getPaddingTop() + getPaddingBottom();
……
}
onLayout()方法的改动则比较大,大家最好是根据自己的思路和理解自行实现,这样才能真正消化。我们给大家提供的只是作为参考。
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftPadding = getPaddingLeft();
int rightPadding = getPaddingRight();
int topPadding = getPaddingTop();
int bottomPadding = getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (i == 0) {
//定位到左上角
child.layout(leftPadding, topPadding,
child.getMeasuredWidth() + leftPadding,
child.getMeasuredHeight() + topPadding);
} else if (i == 1) {
//定位到右上角
child.layout(getMeasuredWidth() - child.getMeasuredWidth()
- rightPadding,
topPadding, getMeasuredWidth() - rightPadding,
child.getMeasuredHeight() + topPadding);
} else if (i == 2) {
//定位到左下角
child.layout(leftPadding,
getMeasuredHeight() - child.getMeasuredHeight()
- bottomPadding,
child.getMeasuredWidth() + leftPadding,
getMeasuredHeight() - bottomPadding);
} else if (i == 3) {
//定位到右下角
child.layout(getMeasuredWidth() - child.getMeasuredWidth()
- rightPadding,
getMeasuredHeight() - child.getMeasuredHeight()
- bottomPadding,
getMeasuredWidth() - rightPadding,
getMeasuredHeight() - bottomPadding);
}
}
}
在布局文件中,将 CornerLayout 的 paddingLeft、paddingTop、paddingRight 和 paddingBottom四个属性分别设置为 10dp、20dp、30dp 和 40dp,运行结果如图所示。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<bczm.com.day0617.CornerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FFCCCCCC"
android:paddingBottom="40dp"
android:paddingLeft="10dp"
android:paddingRight="30dp"
android:paddingTop="20dp">
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_bright" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_dark" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_red_dark" />
<TextView
android:layout_width="150dp"
android:layout_height="150dp"
android:background="@android:color/holo_green_light" />
bczm.com.day0617.CornerLayout>
LinearLayout>
CornerLayout 并不具备实用价值,因为 FrameLayout 布局能轻易实现 CornerLayout 的功能,但是,对于理解布局容器的开发却能提供一种非常清晰的方法和思路(这个才是最重要的,不是么?)。
熟悉 css 的朋友应该知道,当 div 定义了 maring:15px;的样式时,div 各方向离相邻元素的距离都将是 15 个像素(不考虑垂直合并的情况),其实,Android 组件的外边距 margin 在理解的时候与 css 是基本相同的。我们修改 cornerlayout.xml 文件如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<bczm.com.day0617.CornerLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FFCCCCCC"
android:padding="10dp">
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/holo_blue_bright" />
bczm.com.day0617.CornerLayout>
LinearLayout>
xml 代码中我们将第一个 TextView 的 layout_margin 属性设置为 10dp(限于篇幅其他
TextView 省略了),我们希望第一个 TextView 离相邻组件的距离为 10dp,但运行后却发现没有任何效果。原因是我们在 onLayout()方法中定位子组件时没有考虑 margin 这个属性。
如果要考虑 margin,则将影响以下几个方面:
Ø 影响 onMeasure()方法测量的容器尺寸;
Ø 影响 onLayout()方法对子组件的定位;
Ø 必须为子组件提供默认的 MarginLayoutParams(或其子类)。
向容器添加子组件时,需要调用 addView()方法,该方法有几个重载版本,如果调用 publicvoid addView(View child, LayoutParams params)方法,则必须手动指定 LayoutParams,LayoutParams中定义了两个重要的属性:width 和 height,对应了 xml 中的 layout_width 和 layout_height 属性。
如果要让组件支持 margin,则必须使用 MarginLayoutParams 类,该类是 LayoutParams 的子类,下面是 MarginLayoutParams 类的源码片段:
public static class MarginLayoutParams extends ViewGroup.LayoutParams {
public int leftMargin; //对应 layout_marginLeft 属性
public int topMargin; //对应 layout_marginTop 属性
public int rightMargin; //对应 layout_marginRight 属性
public int bottomMargin; //对应 layout_marginBottom 属性
}
然而,当我们调用 public void addView(View child)方法来添加子组件时,并不需要指定
LayoutParams,此时,ViewGroup 会调用其 generateDefaultLayoutParams()方法获取默认的LayoutParams,对于支持子组件 margin 来说,这是必要的,addView()的源码如下:
public void addView(View child, int index) {
LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new llegalArgumentException("generateDefaultLayout-Params() cannot
return null");
}
}
addView(child, index, params);
}
自定义容器如果要支持 margin 特性,容器类必须重写 generateDefault-LayoutParams()方法,返回 MarginLayoutParams 对象。另外,还需要重写另外两个方法:
Ø public LayoutParams generateLayoutParams(AttributeSet attrs)
创建 LayoutParams (或子类)对象,通过 attrs 可以读取到布局文件中的自定义属性值,该方法必须重写;
Ø protected LayoutParams generateLayoutParams(LayoutParams p)
创建 LayoutParams(或子类)对象,可以重用参数 p,该方法建议重写。
为了让 CornerLayout 支持 margin 特征,需要重写 generateDefaultLayout-Params()和
generateLayoutParams()方法,代码如下(为了不影响前面的案例,将类名改名为 CornerLayout2):
public class CornerLayout2 extends ViewGroup {
……
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(this.getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new MarginLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new MarginLayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
……
}
测量容器大小时,在 layout_width 和 layout_height 皆为 wrap_content 的情况下(其他情况无需过多考虑),容器的宽度和高度分别为:
宽度 = A、C 的最大宽度 + B、D 的最大宽度 + 容器左边的 padding + 容器右边的 padding+ A、C 左、右的最大 margin + B、D 左、右的最大 margin;
高度 = A、B 的最大高度 + C、D 的最大高度 + 容器顶部的 padding + 容器底部的 padding +A、B 顶、底部的最大 margin + C、D 顶、底部的最大 margin。
修改后的 onMeasure()方法实现如下:
···
/**
* 测量尺寸
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量所有子组件的大小
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
//再测量自己的大小
int width = this.measureWidth(widthMeasureSpec);
int height = this.measureHeight(heightMeasureSpec);
//应用
this.setMeasuredDimension(width, height);
}
/**
* 测量容器的宽度
* @param widthMeasureSpec
* @return
*/
private int measureWidth(int widthMeasureSpec){
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if(mode == MeasureSpec.EXACTLY) {
//match_parent 或具体值
width = size;
}else if(mode == MeasureSpec.AT_MOST){
//wrap_content
int aWidth, bWidth, cWidth, dWidth;
aWidth = bWidth = cWidth = dWidth = 0;
int marginHa, marginHb, marginHc, marginHd;
marginHa = marginHb = marginHc = marginHd = 0;
for(int i = 0; i < this.getChildCount(); i ++){
MarginLayoutParams layoutParams = (MarginLayoutParams)
getChildAt(i).getLayoutParams();
if(i == 0) {
aWidth = getChildAt(i).getMeasuredWidth();
marginHa += layoutParams.leftMargin + layoutParams.rightMargin;
}else if(i == 1) {
bWidth = getChildAt(i).getMeasuredWidth();
marginHb += layoutParams.leftMargin + layoutParams.rightMargin;
}else if(i == 2) {
cWidth = getChildAt(i).getMeasuredWidth();
marginHc += layoutParams.leftMargin + layoutParams.rightMargin;
}else if(i == 3) {
dWidth = getChildAt(i).getMeasuredWidth();
marginHd += layoutParams.leftMargin + layoutParams.rightMargin;
}
}
width = Math.max(aWidth, cWidth) + Math.max(bWidth, dWidth)
+ getPaddingLeft() + getPaddingRight()
+ Math.max(marginHa, marginHc)
+ Math.max(marginHb, marginHd);
}
return width;
}
/**
* 测量容器的高度
* @param heightMeasureSpec
* @return
*/
private int measureHeight(int heightMeasureSpec){
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if(mode == MeasureSpec.EXACTLY) {
//match_parent 或具体值
height = size;
}else if(mode == MeasureSpec.AT_MOST){
//wrap_content
int aHeight, bHeight, cHeight, dHeight;
aHeight = bHeight = cHeight = dHeight = 0;
int marginVa, marginVb, marginVc, marginVd;
marginVa = marginVb = marginVc = marginVd = 0;
for(int i = 0; i < this.getChildCount(); i ++){
MarginLayoutParams layoutParams = (MarginLayoutParams)
getChildAt(i).getLayoutParams();
if(i == 0) {
aHeight = getChildAt(i).getMeasuredHeight();
marginVa += layoutParams.topMargin +
layoutParams.bottomMargin;
}else if(i == 1) {
bHeight = getChildAt(i).getMeasuredHeight();
marginVb += layoutParams.topMargin +
layoutParams.bottomMargin;
}else if(i == 2) {
cHeight = getChildAt(i).getMeasuredHeight();
marginVc += layoutParams.topMargin +
layoutParams.bottomMargin;k
}else if(i == 3) {
dHeight = getChildAt(i).getMeasuredHeight();
marginVd += layoutParams.topMargin +
layoutParams.bottomMargin;
}
}
height = Math.max(aHeight, bHeight) + Math.max(cHeight, dHeight)
+ getPaddingTop() + getPaddingBottom()
+ Math.max(marginVa, marginVb) + Math.max(marginVc, marginVd);
}
return height;
}
···
onLayout()方法定位子组件时也将更加复杂,考虑的因素包括子组件的尺寸、padding 和
margin,需要为 padding 和 margin 留出对应的空白空间。
/**
* 定位子组件
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftPadding = getPaddingLeft();
int rightPadding = getPaddingRight();
int topPadding = getPaddingTop();
int bottomPadding = getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
MarginLayoutParams layoutParams = (MarginLayoutParams)
child.getLayoutParams();
int leftMargin = layoutParams.leftMargin;
int rightMargin = layoutParams.rightMargin;
int topMargin = layoutParams.topMargin;
int bottomMargin = layoutParams.bottomMargin;
Log.i(TAG, "leftMargin:" + leftMargin + " rightMargin:"
+ rightMargin + " topMargin:" + topMargin
+ " bottomMargin:" + bottomMargin);
if (i == 0) {
//定位到左上角
child.layout(leftPadding + leftMargin,
topPadding + topMargin,
child.getMeasuredWidth() + leftPadding + leftMargin,
child.getMeasuredHeight() + topPadding + topMargin);
} else if (i == 1) {
//定位到右上角
child.layout(getMeasuredWidth() - child.getMeasuredWidth()
- rightPadding - rightMargin,
topPadding + rightMargin,
getMeasuredWidth() - rightPadding - rightMargin,
child.getMeasuredHeight() + topPadding + rightMargin);
} else if (i == 2) {
//定位到左下角
child.layout(leftPadding + leftMargin,
getMeasuredHeight() - child.getMeasuredHeight()
- bottomPadding - bottomMargin,
child.getMeasuredWidth() + leftPadding + leftMargin,
getMeasuredHeight() - bottomPadding - bottomMargin);
} else if (i == 3) {
//定位到右下角
child.layout(getMeasuredWidth() - child.getMeasuredWidth()
- rightPadding - rightMargin,
getMeasuredHeight() - child.getMeasuredHeight()
- bottomPadding - bottomMargin,
getMeasuredWidth() - rightPadding - rightMargin,
getMeasuredHeight() - bottomPadding bottomMargin);
}
}
}
最后,修改 cornerlayout.xml 布局文件,为第一个和第三个子组件指定 layout_margin 的值为10dp,运行效果如图所示。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:padding="10dp"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">
<bczm.com.day0617.CornerLayout2
android:background="#FFCCCCCC"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/holo_blue_bright"/>
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_dark"/>
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/holo_red_dark"/>
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_green_light"/>
bczm.com.day0617.CornerLayout2>
LinearLayout>
如果在
标记上添加 android: padding=”10dp”属性,结果如图所示。
支持 margin 的 CornerLayout2 类的结构如图所示。
我们前面接触过 LayoutParams 和 MarginLayoutParams 等布局参数类,这两个类都是
ViewGroup 的静态内部类。这也为我们自定义 LayoutParams 提供了参考依据,各位可以去阅读这两个类的源码以便有更多的了解。
到目前为止,CornerLayout 还不支持显示方位,这也是唯一尚未实现的需求。本节我们将一起来实现这个功能。方位包含 4 个方向:左上角、右上角、左下角、右下角,在 attrs.xml 文件中,定义一个名为layout_position 的属性,类型为 enum,枚举出这 4 个值。
<resources>
<declare-styleable name="CornerLayout3">
<attr name="layout_position" format="enum">
<enum name="left_top" value="0"/>
<enum name="right_top" value="1"/>
<enum name="left_bottom" value="2"/>
<enum name="right_bottom" value="3"/>
attr>
declare-styleable>
resources>
如果要从容器中读取子组件的自定义属性,需要使用布局参数,比如有如下的配置:
.com.day0617.CornerLayout3
android:layout_width="wrap_content"
android:layout_height="wrap_content" >
"100dp"
android:layout_height="100dp"
trkj:layout_position="right_bottom"/>
.com.day0617.CornerLayout3>
如果我们想在 CornerLayout3 容器类中读取 TextView 的 trkj:layout_position 属性值,使用布局参数(LayoutParams)是一个很好的解决办法。将 CornerLayout2 复制一个新类,取名为CornerLayout3,考虑到要支持 margin 特征,在该类中定义一个继承自 MarginLayoutParams的子类 PositionLayoutParams,按照惯例,PositionLayoutParams 类为 CornerLayout3 的静态内部类。
public class CornerLayout3 {
……
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new PositionLayoutParams(this.getContext(), attrs);
}
@Override
protected LayoutParams generateLayoutParams(LayoutParams p) {
return new PositionLayoutParams(p);
}
@Override
protected LayoutParams generateDefaultLayoutParams() {
return new PositionLayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT);
}
/**
* 自定义 LayoutParams
*/
public static class PositionLayoutParams extends ViewGroup.MarginLayoutParams{
public static final int LEFT_TOP = 0;
public static final int RIGHT_TOP = 1;
public static final int LEFT_BOTTOM = 2;
public static final int RIGHT_BOTTOM = 3;
public static final int NONE = -1;
public int position;
public PositionLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
//读取 layout_position 属性
TypedArray a = c.obtainStyledAttributes(
attrs, R.styleable.CornerLayout3);
position = a.getInt(R.styleable.CornerLayout3_layout_position, NONE);
a.recycle();
}
public PositionLayoutParams(int width, int height) {
super(width, height);
}
public PositionLayoutParams(MarginLayoutParams source) {
super(source);
}
public PositionLayoutParams(LayoutParams source) {
super(source);
}
}
}
上述代码中,根据父类的要求定义了 4 个构造方法,其中构造方法 publicPositionLayoutParams(Context c, AttributeSet attrs)读取了 layout_position 属性值,保存在 position成员变量中,如果未读取到该属性,则默认值为 NONE。其次定义了 4 个常量与 layout_position属性的 4 个枚举值相对应。
ViewGroup 类重写的 generateLayoutParams()和 generateDefaultLayoutParams()方法返回的LayoutParams 为 PositionLayoutParams 对 象 。 其中public LayoutParams generateLayout-Params(AttributeSet attrs)方法将attrs传入public PositionLayoutParams(Context c, AttributeSet attrs)构造方法,所以PositionLayoutParams 才能读取到 layout_position 的属性值。
在 onLayout()方法中,我们需要根据当前子组件的 PositionLayoutParams 的 position 属性来确定方位,这里有两种情况:一种是没有为组件定义方位时,依旧按照从左往右、从上往下的方式进行放置;另一种是如果组件定义了特定方位,如 right_bottom,则将该组件显示在容器的右下角。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftPadding = getPaddingLeft();
int rightPadding = getPaddingRight();
int topPadding = getPaddingTop();
int bottomPadding = getPaddingBottom();
for(int i = 0; i < getChildCount(); i ++){
View child = getChildAt(i);
PositionLayoutParams layoutParams = (PositionLayoutParams)
child.getLayoutParams();
int leftMargin = layoutParams.leftMargin;
int rightMargin = layoutParams.rightMargin;
int topMargin = layoutParams.topMargin;
int bottomMargin = layoutParams.bottomMargin;
int position = layoutParams.position;
if(i == 0 && position == PositionLayoutParams.NONE
|| position == PositionLayoutParams.LEFT_TOP){
//定位到左上角,代码不变,略
}else if(i == 1 && position == PositionLayoutParams.NONE
|| layoutParams.position ==
PositionLayoutParams.RIGHT_TOP){
//定位到右上角,代码不变,略
}else if(i == 2 && position == PositionLayoutParams.NONE
|| layoutParams.position ==
PositionLayoutParams.LEFT_BOTTOM){
//定位到左下角,代码不变,略
}else if(i == 3 && position == PositionLayoutParams.NONE
|| layoutParams.position ==
PositionLayoutParams.RIGHT_BOTTOM){
//定位到右下角,代码不变,略
}
}
}
为了更加清晰地看明白 CornerLayout3 容器内子组件的位置,我们为子组件 TextView 分别添加了 A、B、C、D 四个字符作为 text 属性的值,在没有为子组件指定方位的情况下,创建cornerlayout2.xml 布局文件,内容如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:trkj="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<bczm.com.day0617.CornerLayout3
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FFCCCCCC"
android:padding="10dp">
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/holo_blue_bright"
android:gravity="center"
android:text="A"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_dark"
android:gravity="center"
android:text="B"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/holo_red_dark"
android:gravity="center"
android:text="C"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:text="D"
android:textColor="#FFFFFFFF" />
bczm.com.day0617.CornerLayout3>
LinearLayout>
运行效果如图所示。
接下来,我们为每个子组件都指定一个不同的方位(方位相同会重叠),修改如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:trkj="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<bczm.com.day0617.CornerLayout3
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#FFCCCCCC"
android:padding="10dp">
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/holo_blue_bright"
android:gravity="center"
android:text="A"
android:textColor="#FFFFFFFF"
trkj:layout_position="right_bottom" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_dark"
android:gravity="center"
android:text="B"
android:textColor="#FFFFFFFF"
trkj:layout_position="left_bottom" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="10dp"
android:background="@android:color/holo_red_dark"
android:gravity="center"
android:text="C"
android:textColor="#FFFFFFFF"
trkj:layout_position="right_top" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:text="D"
android:textColor="#FFFFFFFF"
trkj:layout_position="left_top" />
bczm.com.day0617.CornerLayout3>
LinearLayout>
果然不出所料,组件的位置变了,效果如图所示。CornerLayout3 中的
PositionLayoutParams 自定义布局参数类只有一个属性,但足以说明问题,希望大家触类旁通,深刻理解容器通过 LayoutParams 读取子组件信息而影响容器外观与行为的意义。总之一句话,容器类能读取子组件的 LayoutParams。
因为 LayoutParams 类一般都定义为容器类的静态内部类,所以在命名时往往都统一使用
“LayoutParams”作为布局参数类的名称,不同的 LayoutParams 通过容器类来区分,比如:FrameLayout. LayoutParams、LinearLayout. LayoutParams 等等,嗯,这只是一个官方的做法而已,大家知道就好!
在 Java Swing 中,有一种布局,叫流式布局(FlowLayout),这种布局的特点是子组件按照从左往右、从上往下的顺序依次排序,如果一行放不下,自动显示到下一行,和 HTML 中的float 效果类似,但在,Android 中没有提供这样的布局,本节,我们将一起来实现这种布局。
对于 FlowLayout 来说,难点有二:一是要事先预测组件的宽度和高度,这个和 CornerLayout有明显的不同,FlowLayout 中的组件数是不固定的,而 CornerLayout 中最多只支持 4 个子组件,前者的难度系数更大,也需要更灵活的处理;二是对子组件进行定位时,也是个头痛的问题,子组件的大小不一,数量多少不一,每一个组件放在哪一行、放在一行中的什么位置都需要计算,最重要的是要找到规律,不可能一个一个去处理。
测量 FlowLayout 容器的宽度时,不允许子组件的宽度比容器的宽度还大,这是前提。当子组件个数很少总宽度比容器的 layout_width 为 match_parent 时的宽度小,那么容器的
layout_width 为 wrap_content 时就是子组件的宽度之和。但是如果子组件个数很多,总宽度超出容器的最大宽度,则就算容器的 layout_width 为 wrap_content 最终测量宽度也要采用match_parent 值,并且需要另起一行继续显示上一行余下的子组件。
//遍历所有的子组件,计算出子组件的总宽度
for(int i = 0; i < n; i ++){
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
//单个子组件的宽度不能超过容器宽度
if(childWidth > size){
throw new IllegalStateException("Sub view is too large.");
}
childrenWidth += childWidth;
}
if(childrenWidth > size){
//如果子组件的宽度之和大于容器的最大宽度,则使用容器宽度
width = size;
}else{
//如果子组件的宽度之和小于容器的最大宽度,
//容器宽度跟随子组件宽度
width = childrenWidth;
}
FlowLayout 容器高度是每一行最高的组件的高度之和。因为测量时并不需要显示子组件,所以我们采用预测的方法判断是否需要换行,换行后计算出当前行最高的组件高度并进行累加,最后算出所有行的最高高度之和。
for(int i = 0; i < n; i ++){
……
//预测是否需要换行,getChildAt(i + 1)是将下一个子组件事先考虑进来
//但不影响子组件的循环遍历
if(i < n - 1 && maxLineWidth + getChildAt(i + 1).getMeasuredWidth()
> width - getPaddingLeft() - getPaddingRight()){
//当前行的子组件宽度如果超出容器的宽度,则要换行
height += maxViewHeight;
maxLineWidth = 0;
maxViewHeight = 0;
}else if(i == n - 1){
//已经遍历到最后一个
height += maxViewHeight;
}
}
重写 onLayout()方法定位子组件时,是一个逻辑性比较强的工作。从第 0 个子组件开始,一个个进行定位,如果当前行的已占宽度加上当前子组件的宽度大于容器的宽度,则要换行,换行其实就是将当前子组件的宽度重新设置为 0、高度就是前面所有行的高度之和。每成功定位一个组件,都要算出当前行的最高高度并累计当前行的已占宽度。
需要展开想象,并亲身实践才能感同身受,对于复杂的逻辑,讲解总是苍白无力。下面是 FlowLayout 类的源码。
public class FlowLayout extends ViewGroup {
private static final String TAG = "FlowLayout";
public FlowLayout(Context context) {
super(context);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public FlowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int n = getChildCount();
int maxViewHeight = 0; //当前行的子组件的最大高度
int maxLineWidth = 0; //当前行的子组件的总宽度
int totalHeight = 0; //累计高度
int width = getMeasuredWidth(); //容器宽度
for (int i = 0; i < n; i++) {
View child = getChildAt(i);
//判断是否要换行显示(已占宽度+当前子组件的宽度是否大于容器的宽度)
if (maxLineWidth + getChildAt(i).getMeasuredWidth()
> width - getPaddingLeft() - getPaddingRight()) {
//换行后累计已显示的行的总高度
totalHeight += maxViewHeight;
Log.i(TAG, "totalHeight:" + totalHeight + " maxLineWidth:" +
maxLineWidth + " width:" + width);
//新起一行,新行的已占宽度和高度重置为 0
maxLineWidth = 0;
maxViewHeight = 0;
}
layoutChild(child, maxLineWidth, totalHeight,
maxLineWidth + child.getMeasuredWidth(),
totalHeight + child.getMeasuredHeight());
//获取当前行的最高高度
maxViewHeight = Math.max(maxViewHeight,
child.getMeasuredHeight());
//累加当前行的宽度
maxLineWidth += child.getMeasuredWidth();
}
}
/**
* 定位子组件,方法内考虑 padding
* Android 自定义组件开发详解 株洲新程 IT 教育 李赞红
* 邮箱:[email protected] 欢迎捐助,支持我分享更多技术! - 250 -
*
* @param child
* @param l
* @param t
* @param r
* @param b
*/
private void layoutChild(View child, int l, int t, int r, int b) {
Log.i(TAG, child.getTag() + ":" + " Left:" + l + " Top:"
+ t + " Right:" + r + " Bottom:" + b);
//所有子组件要统一向右和向下平移指定的 padding
child.layout(l + getPaddingLeft(), t + getPaddingTop(),
r + getPaddingLeft(), b + getPaddingTop());
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
}
/**
* 测量容器宽度
*
* @param widthMeasureSpec
* @return
*/
private int measureWidth(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
int width = 0;
if (mode == MeasureSpec.EXACTLY) {
width = size;
} else if (mode == MeasureSpec.AT_MOST) {
//计算出所有子组件占的总宽度
int n = getChildCount();
int childrenWidth = 0;
for (int i = 0; i < n; i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth();
//单个子组件的宽度不能超过容器宽度
if (childWidth > size) {
throw new IllegalStateException("Sub view is too large.");
}
childrenWidth += childWidth;
}
Log.i(TAG, "size:" + size + " viewsWidth:" + childrenWidth);
//在 wrap_content 的情况下,如果子组件占的总宽度>容器的
//最大宽度,则应该取容器最大宽度
if (childrenWidth > size) {
width = size;
} else {
width = childrenWidth;
}
//padding
width += this.getPaddingLeft() + getPaddingRight();
}
return width;
}
/**
* 测量容器高度
*
* @param heightMeasureSpec
* @return
*/
private int measureHeight(int heightMeasureSpec) {
int mode = MeasureSpec.getMode(heightMeasureSpec);
int size = MeasureSpec.getSize(heightMeasureSpec);
int height = 0;
if (mode == MeasureSpec.EXACTLY) {
height = size;
} else if (mode == MeasureSpec.AT_MOST) {
//wrap_content 容器高度跟随内容
int width = getMeasuredWidth();
int n = getChildCount();
int maxViewHeight = 0;//当前行的子组件的最大高度
int maxLineWidth = 0;//当前行的子组件的总宽度
for (int i = 0; i < n; i++) {
View child = getChildAt(i);
maxLineWidth += child.getMeasuredWidth();
maxViewHeight =
Math.max(child.getMeasuredHeight(), maxViewHeight);
//预测是否需要换行
if (i < n - 1 && maxLineWidth
+ getChildAt(i + 1).getMeasuredWidth()
> width - getPaddingLeft() - getPaddingRight()) {
//当前行的子组件宽度如果超出容器的宽度,则要换行
height += maxViewHeight;
maxLineWidth = 0;
maxViewHeight = 0;
} else if (i == n - 1) {
//已经遍历到最后一个
height += maxViewHeight;
}
}
}
//padding
height += getPaddingTop() + getPaddingBottom();
return height;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
为了观察 FlowLayout 的最终效果,定义 flowlayout.xml 布局文件,在该布局文件中定义了 9个大小不同的 TextView,每个子组件都定义了 tag 属性,用于输出 log 日志。xml 如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<bczm.com.day0617.FlowLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#99CCCCCC"
android:padding="10dp">
<TextView
android:layout_width="200dp"
android:layout_height="200dp"
android:background="@android:color/holo_blue_bright"
android:gravity="center"
android:tag="0"
android:text="0"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_dark"
android:gravity="center"
android:tag="1"
android:text="1"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:tag="2"
android:text="2"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@android:color/background_dark"
android:gravity="center"
android:tag="3"
android:text="3"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/holo_blue_dark"
android:gravity="center"
android:tag="4"
android:text="4"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:tag="5"
android:text="5"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="100dp"
android:layout_height="100dp"
android:background="@android:color/darker_gray"
android:gravity="center"
android:tag="6"
android:text="6"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="100dp"
android:layout_height="50dp"
android:background="@android:color/holo_green_light"
android:gravity="center"
android:tag="7"
android:text="7"
android:textColor="#FFFFFFFF" />
<TextView
android:layout_width="120dp"
android:layout_height="50dp"
android:background="@android:color/darker_gray"
android:gravity="center"
android:tag="8"
android:text="8"
android:textColor="#FFFFFFFF" />
bczm.com.day0617.FlowLayout>
LinearLayout>
1、请模拟 Java Swing 中的 FlowLayout 为 Android 定义一个流式布局,布局中的子组件一个挨着一个从左往右排列,如果一行显示不完继续显示在第二行,直到所有的组件都显示为止。
要求:
1)支持 margin
2)支持垂直滚动
2、请自定义一个布局容器(WaterfallLayout),用于完成子组件的瀑布流显示。
注:如果不确定请参考 http://oversea.huanqiu.com/article/的效果。
谢谢认真观读本文的每一位小伙伴,衷心欢迎小伙伴给我指出文中的错误,也欢迎小伙伴与我交流学习。
欢迎爱学习的小伙伴加群一起进步:230274309