第七章 自定义容器
7.1、概述
自定义容器本质上讲也是一种组件,常见的 LinearLayout,FrameLayout,GradLayout,ScrollView 和 RelativeLayout 等等组件都是容器,容器除了 有自己 的外观,还能用来容纳各种组件,以一种特定的规则规定组件应该在什么位置、显示多大 一般情况下,我们更关注自定义组件的外观及功能,自定义容器则更关注其内的组件怎么排列和摆放,比如线性布局 LinearLayout 中的组件只能水平排列或者垂直排列,帧布局 FrameLayout 中的组件了可以重叠,相对布局 RelativeLayout 中的组件可以以某一个组件为参照,定位自身的位置… 容器还关注组件与容器四个边框的距离(padding),获取容器内组件与组件之间的距离(margin) 事实上,容器时可以嵌套的,一个容器中,既可以是一个普通的子组件,也可以是另一个子容器 容器类一般要继承 ViewGroup 类,ViewGroup 类同时也是 View 的子类。ViewGroup 又是一个抽象类,定义了 onLayout( )等抽象方法。当然,根据需要,我们也可以让容器类继承自 FrameLayout 等 ViewGroup 的子类,比如 ListView 继承自 ViewGroup,而 ScrollView 水平滚动容器则从 FrameLayout 派生7.2、ViewGroup类
7.2.1 ViewGroup 常用方法
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 中类继承,用于测量组件或容器自己的尺寸,参数 widthSpec 和 heightSpec 为 0 时,表示安实际大小进行测量,将 0 传入方法常常或有奇效
- 测量容器尺寸
- 重写 onMeasure( )方法测量容器大小,和自定义组件有所区别的是,在测量容器大小之前,必须调用 measureChildren( )方法测量所有子组件的大小。不然,结果永远为 0
- 确定没有子组件的位置
- 重写 onLayout( )方法确定每个子组件的位置(这个其实挺麻烦,也是自定义容器的难点部分),在 onLayout( )方法中,调用 View 的 layout( ) 方法确定子组件的位置
- 绘制容器
- 重写 onDraw( )方法,其实 ViewGroup 类并没有重写 onDraw()方法,除非有特别的要求,定义容器也很少去重写。比如 LinearLayout 则是重写了 该方法用于绘制水平 或 垂直分割线条,而 FrameLayout 则是重写了 draw( )方法,作用一样
public class MyViewGroup extends ViewGroup {
public MyViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 确定子组件的位置
* @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);
}
/**
* 绘制容器
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
}
7.2.2 ViewGroup 的工作原理
本书第一章 介绍过 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) {
boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
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,long drawingTime)方法,该方法定义如下: 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);
}
}
}
.......
}
上面的方法又调用了 daraw(Canvas canvas)方法,如果子组件不再是一个容器 。 将调用 (!dirtyOpaque) onDraw(canvas)语句完成组件的绘制。同样的,onDraw(canvas)正是需要我们重写的方法。所以,组件的绘制是一个不断的递归过程7.2.3 重写 onLayout( )方法
在容器类的基本结构中,我们最陌生的是 onLayout( )方法。该方法的原型为:protected void onLayout(boolean changed, int l, int t, int r, int b),其中,参数 changed 判断是否有新的大小和位置。l 表示 left,t 表示 top,r 表示 right,b 表示 bottom。后面的 4个参数表示容器自己相对父容器的位置及大小,通常情况下,r - l 的值等同方法 getMeasureWidth( )方法的返回值,b - t 的值等同于 getMeasureHeight( ) 方法的返回值。关于 l、t、r、b 参数的理解如图 7-2 所示 在 onLayout( )方法中,需要调用 View 的 layout( )方法用于定义子组件和子容器的位置,layout( )方法的原理如下:- public void layout(int l, int t, int r, int b)
- 参数 l、t、r、b 四个参数的作用于与上面相同,通过这 4个参数,基本可以确定子组件的位置与大小
public class SizeViewGroup extends ViewGroup {
public SizeViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
//创建一个组件
TextView textView = new TextView(context) ;
LayoutParams params =new LayoutParams(200, 200);
textView.setText("Android") ;
textView.setBackgroundColor(Color.YELLOW);
//在当前容器添加子组件
this.addView(textView,params);
//设置容器的背景颜色
this.setBackgroundColor(Color.alpha(255));
}
/**
* 测量组件
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量所有子组件的大小
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
//测量自身的大小,此处直接写死为 500*500
this.setMeasuredDimension(500, 500);
}
/**
* 确定组件的位置和大小
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//设置子组件(此处为 text)的位置和大小
View textView = this.getChildAt(0) ;
textView.layout(50, 50, textView.getMeasuredWidth()+50, textView.getMeasuredHeight()+50);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//为容器画一个红色的边框
RectF rectF = new RectF(0,0,getMeasuredWidth(),getMeasuredHeight());
rectF.inset(2, 2);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG) ;
paint.setStyle(Style.STROKE) ;
paint.setStrokeWidth(2) ;
paint.setColor(Color.RED) ;
Path path = new Path() ;
path.addRoundRect(rectF, 20, 20,Direction.CCW) ;
canvas.drawPath(path, paint);
}
}
上面的代码中,我们定义了一个容器。为了简单。在构造方法中创建了一个 TextView 组件,并将 SizeViewGroup 的背景设置成了透明(如果绘制一个不规则的自定义组件,将背景设置成透明是个不错的办法)。onMeasure( )方法用来测量容器大小,在测量容器之前,必须先调用 measureChildren( )方法测量所有的子组件的大小,本案例中将容器大小设置成了一个不变的值 500 * 500 。所以,尽管在布局文件中将 layout_width 和 layout_height 都定义为 match_parent,但实际上这个值并不起作用。onLayout( )方法负责为子组件定位并确定子组件大小,因为只有一个子组件。所以,先通过 getChildAt( 0 )获取子组件(TextView 对象)对象,再调用3子组件的 layout( )方法确定组件的区域,子组件的 left 为 50,top 为 50 ,right 为 50 加上测量宽度,bottom 为 50 加上 测量高度。onDraw( )方法为容器绘制一个圆角矩形作为边框。运行效果如下所示 : 我们知道,一个组件多大,取决于 layout_width 和 layout_height 的大小,但真正决定组件大小的是 layout( )方法。
7.3、示例 2
7.3.1 基本实现
CornerLayout 布局是一个自定义容器,用于将子组件分别显示在容器 4个角落,不接受超过 4个子组件的情形。默认情况下,子组件从左往右,从上往下的顺序放置。但可以为子组件指定位置(左上角 left_top、右上角 right_top、左下角 left_bottom、右下角 right_bottom)。本示例换一个比较完整的案例,借助于次来了解自定义容器先画一个草图来帮助我们分析,如下图 7-5 所示
上图中,蓝色框表示 CornerLayout 布局的区域,A、B、C、D 是 CornerLayout内的 4个子组件,对于 CornerLayout来说,首先要测量的是他的尺寸大小。当 layout_width 为 wrap_content时,其宽度为 A、C 的最大宽度 和 B、D 的最大宽度的和,这样才不至于组件重叠。当然,如果 layout_width 和 layout_height 指定了具体的值或者屏幕不够大的情况下设置为 match_parent,子组件仍然可能会出现重叠现象
下面的代码我们将支持 0~4 个子组件,组件的顺序为 7-5 的 A——>B——>C —— >D 的顺序依次排列
public class CornerLayout extends ViewGroup {
public CornerLayout(Context context) {
this(context,null);
}
public CornerLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CornerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 测量子组件
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量所有子组件的大小
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
//在测量自身
int widht = this.measureWidht(widthMeasureSpec) ;
int height = this.measureHeight(heightMeasureSpec) ;
this.setMeasuredDimension(widht, height);
}
/**
* 定位子组件
*/
@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){ //定位左上角 A
child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
}else if(i == 1){//定位右上角 B
child.layout(this.getMeasuredWidth() - child.getMeasuredWidth(), 0,this.getMeasuredWidth(),child.getMeasuredHeight());
}else if(i == 2){//定位左下角 C
child.layout(0, this.getMeasuredHeight() - child.getMeasuredHeight(), child.getMeasuredWidth(), this.getMeasuredHeight());
}else if(i == 3){//定位右下角 D
child.layout(this.getMeasuredWidth() - child.getMeasuredWidth(), this.getMeasuredHeight() - child.getMeasuredHeight(), this.getMeasuredWidth(), this.getMeasuredHeight());
}
}
}
private int measureWidht(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 aWidth = 0;
int bWidth = 0;
int cWidth = 0;
int dWidth = 0;
for (int i = 0; i < this.getChildCount(); i++) {
if(i == 0){
aWidth = getChildAt(0).getMeasuredWidth() ;
}else if(i == 1){
bWidth = getChildAt(1).getMeasuredWidth() ;
}else if(i == 2){
cWidth = getChildAt(2).getMeasuredWidth() ;
}else if(i == 3){
dWidth = getChildAt(3).getMeasuredWidth() ;
}
}
width = Math.max(aWidth, cWidth)+Math.max(bWidth, dWidth);
}
return width;
}
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){
int aHeight = 0;
int bHeight = 0;
int cHeight = 0;
int dHeight = 0;
for (int i = 0; i < 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 语句进行判断。这样做的目的是为了在读取子组件对象时,防止因组件数量不够而出现下标越界。这个判断是有必要的而且有用的。接下来进行测试并修改其对应的值,运行效果如下:
7.3.2 内边距 padding
我们为 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,高度类似。而在 onLayout( )方法中定位子组件时,也同样为 padding 流出空间。一句概括就是容器的宽度和高度都变大了
在 测量宽度的时候,在 measureWidth( )方法中,计算容器宽度时,加上左边的 padding 和 右边的 padding:
width = Math.max(aWidth, cWidth)+Math.max(bWidth, dWidth) +getPaddingLeft() + getPaddingRight() ;
在 测量高度的时候,在 measureHeight( )方法中,计算容器高度时,加上顶部的 padding 和 底部的 padding:
height = Math.max(aHeight, bHeight)+Math.max(cHeight, dHeight) + getPaddingTop() + getPaddingBottom();
onlayout( )方法则需熬要给每一子组件加减上对应的 padding
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftPadding = getPaddingLeft() ;
int topPadding = getPaddingTop();
int rightPadding = getPaddingRight() ;
int bottomPading = getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if(i == 0){ //定位左上角 A
child.layout(leftPadding, topPadding,
child.getMeasuredWidth()+leftPadding, child.getMeasuredHeight()+topPadding);
}else if(i == 1){//定位右上角 B
child.layout(this.getMeasuredWidth() - child.getMeasuredWidth()-rightPadding, topPadding,
this.getMeasuredWidth()-rightPadding,child.getMeasuredHeight()+topPadding);
}else if(i == 2){//定位左下角 C
child.layout(leftPadding, this.getMeasuredHeight() - child.getMeasuredHeight() - bottomPading,
child.getMeasuredWidth()+leftPadding, this.getMeasuredHeight() - bottomPading);
}else if(i == 3){//定位右下角 D
child.layout(this.getMeasuredWidth() - child.getMeasuredWidth() - rightPadding, this.getMeasuredHeight() - child.getMeasuredHeight() - bottomPading,
this.getMeasuredWidth()-rightPadding, this.getMeasuredHeight()-bottomPading);
}
}
}
在布局文件中设置 padding,其运行如下图 :
7.3.3 外边距 margin
在 Android 组件外边距 margin 在理解时候与 css 是基本相同的。把 CornerLayout 中的 TextView 设置上 margin 属性。然而在运行后没有任何的效果。这是因为我们在 定位 子组件时没有考虑 margin 这个属性如果要考虑支持 margin,则将影响一下几个方法:
- 影响 onMeasure( ) 方法测量的容器的大小
- 影响 onLayout( )方法对子组件的定位
- 必须为子组件提供默认的 MarginLayoutParams(或其子类)
向容器添加子组件时,需要调用 addView( )方法,该方法有个几个重载版本,如果调用 public
void addView(View child, LayoutParams params) 方法,则必须手动指定 LayoutParams,LayoutParams 中定义了两个重要的属性:width 和 height ,对应了 xml 中的 layout_width 和 layout_height 属性。如果要让组件支持 margin,则必须使用 MarginLayoutPamras类,该类 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 IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
addView(child, index, params);
}
自定义容器要支持 margin 特性,容器必须重写 generateDefaultLayoutParams( )方法,返回 MarginLayoutParams 对象。另外,还需要重写另外两个方法:
- public LayoutParams generateLayoutParams(AttributeSet attrs)
- 创建 LayoutParams(或子类)对象,通过 attrs 可以读取到布局文件中的自定义属性值,该方法必须重写
- protected LayoutParams generateLayoutParams(LayoutParams p)
- 创建 LayoutParams(或子类)对象,可以重用参数 p,该方法建议重写
为了让 CornerLayout 支持 margin 特征,需要重写 generateDefaultLayoutParams() 和 generateLayoutParams( )方法,代码如下:
public class CornerLayout1 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( )方法实现如下:
public class CornerLayout1 extends ViewGroup {
public CornerLayout1(Context context) {
this(context,null);
}
public CornerLayout1(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CornerLayout1(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 测量子组件
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量所有子组件的大小
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
//在测量自身
int widht = this.measureWidht(widthMeasureSpec) ;
int height = this.measureHeight(heightMeasureSpec) ;
this.setMeasuredDimension(widht, height);
}
/**
* 定位子组件
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftPadding = getPaddingLeft() ;
int topPadding = getPaddingTop();
int rightPadding = getPaddingRight() ;
int bottomPading = getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
MarginLayoutParams layoutParams = (MarginLayoutParams)getChildAt(i).getLayoutParams();
int leftMargin = layoutParams.leftMargin;
int rightMargin = layoutParams.rightMargin;
int topMargin = layoutParams.topMargin;
int bottomMargin = layoutParams.bottomMargin;
if(i == 0){ //定位左上角 A
child.layout(leftPadding + leftMargin,
topPadding + topMargin,
child.getMeasuredWidth()+leftPadding + leftMargin,
child.getMeasuredHeight()+topPadding + topMargin);
}else if(i == 1){//定位右上角 B
child.layout(this.getMeasuredWidth() - child.getMeasuredWidth()-rightPadding - rightMargin,
topPadding + topMargin,
this.getMeasuredWidth()-rightPadding - rightMargin,
child.getMeasuredHeight()+topPadding + topMargin);
}else if(i == 2){//定位左下角 C
child.layout(leftPadding + leftMargin,
this.getMeasuredHeight() - child.getMeasuredHeight() - bottomPading - bottomMargin,
child.getMeasuredWidth()+leftPadding + leftMargin,
this.getMeasuredHeight() - bottomPading - bottomMargin);
}else if(i == 3){//定位右下角 D
child.layout(this.getMeasuredWidth() - child.getMeasuredWidth() - rightPadding - rightMargin,
this.getMeasuredHeight() - child.getMeasuredHeight() - bottomPading - bottomMargin,
this.getMeasuredWidth()-rightPadding - rightMargin,
this.getMeasuredHeight()-bottomPading - bottomMargin);
}
}
}
private int measureWidht(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 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(0).getMeasuredWidth() ;
marginHa += layoutParams.leftMargin + layoutParams.rightMargin;
}else if(i == 1){
bWidth = getChildAt(1).getMeasuredWidth() ;
marginHb += layoutParams.leftMargin + layoutParams.rightMargin;
}else if(i == 2){
cWidth = getChildAt(2).getMeasuredWidth() ;
marginHc += layoutParams.leftMargin + layoutParams.rightMargin;
}else if(i == 3){
dWidth = getChildAt(3).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;
}
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){
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 < 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;
}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;
}
@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);
}
}
7.3.4 自定义 LayoutParams
我们前面接触过 LayoutParams 和 MarginLayoutParams 等布局参数,这两个类都是 ViewGroup 的静态内部类。这也为我们自定义 LayoutParams 提供了参考依据。到目前为止,CornerLayout 还不支持显示方位,这也是唯一尚未实现的需求。本节我们将一起实现这个功能方位包含 4各方向:左上角、右上角、左下角、右下角,在 attrs.xml 文件中,定义一个名为 layout_position 的属性,类型为 enum,枚举出这 4 个值
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CornerLayout2">
<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>
如果从容器中读取子组件的自定义属性,需要使用布局参数,比如有如下的配置 :
<myviewgroup.CornerLayout2
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<TextView
android:layout_width="50dp"
android:layout_height="100dp"
app:layout_position="left_top"
/>
</myviewgroup.CornerLayout2>
如果我们想在 CornerLayout2 容器中读取 TextView 的 app:layout_position 属性,使用布局参数(LayoutParams)是一个很好的解决办法,考虑使用 margin 特征,在该类中定义一个 继承自 MarginLayoutParams 的子类 PositionLayoutParams ,按照惯例,PositionLayoutParams 类为 CornerLayout2 的静态内部类
public class CornerLayout2 extends ViewGroup {
public CornerLayout2(Context context) {
this(context,null);
}
public CornerLayout2(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public CornerLayout2(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
/**
* 测量子组件
*/
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//先测量所有子组件的大小
this.measureChildren(widthMeasureSpec, heightMeasureSpec);
//在测量自身
int widht = this.measureWidht(widthMeasureSpec) ;
int height = this.measureHeight(heightMeasureSpec) ;
this.setMeasuredDimension(widht, height);
}
/**
* 定位子组件
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int leftPadding = getPaddingLeft() ;
int topPadding = getPaddingTop();
int rightPadding = getPaddingRight() ;
int bottomPading = getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
PositionLayoutParams layoutParams = (PositionLayoutParams)getChildAt(i).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){ //定位左上角 A
child.layout(leftPadding + leftMargin,
topPadding + topMargin,
child.getMeasuredWidth()+leftPadding + leftMargin,
child.getMeasuredHeight()+topPadding + topMargin);
}else if(i == 1 && position == PositionLayoutParams.NONE
|| position == PositionLayoutParams.RIGHT_TOP){//定位右上角 B
child.layout(this.getMeasuredWidth() - child.getMeasuredWidth()-rightPadding - rightMargin,
topPadding + topMargin,
this.getMeasuredWidth()-rightPadding - rightMargin,
child.getMeasuredHeight()+topPadding + topMargin);
}else if(i == 2 && position == PositionLayoutParams.NONE
|| position == PositionLayoutParams.LEFT_BOTTOM){//定位左下角 C
child.layout(leftPadding + leftMargin,
this.getMeasuredHeight() - child.getMeasuredHeight() - bottomPading - bottomMargin,
child.getMeasuredWidth()+leftPadding + leftMargin,
this.getMeasuredHeight() - bottomPading - bottomMargin);
}else if(i == 3 && position == PositionLayoutParams.NONE
|| position == PositionLayoutParams.RIGHT_BOTTOM){//定位右下角 D
child.layout(this.getMeasuredWidth() - child.getMeasuredWidth() - rightPadding - rightMargin,
this.getMeasuredHeight() - child.getMeasuredHeight() - bottomPading - bottomMargin,
this.getMeasuredWidth()-rightPadding - rightMargin,
this.getMeasuredHeight()-bottomPading - bottomMargin);
}
}
}
private int measureWidht(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 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(0).getMeasuredWidth() ;
marginHa += layoutParams.leftMargin + layoutParams.rightMargin;
}else if(i == 1){
bWidth = getChildAt(1).getMeasuredWidth() ;
marginHb += layoutParams.leftMargin + layoutParams.rightMargin;
}else if(i == 2){
cWidth = getChildAt(2).getMeasuredWidth() ;
marginHc += layoutParams.leftMargin + layoutParams.rightMargin;
}else if(i == 3){
dWidth = getChildAt(3).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;
}
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){
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 < 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;
}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;
}
@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);
}
public static class PositionLayoutParams extends 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 context, AttributeSet attrs) {
super(context, attrs);
//读取 layout_position 属性
TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CornerLayout2);
position = a.getInt(R.styleable.CornerLayout2_layout_position, NONE);
a.recycle();
}
public PositionLayoutParams(int arg0, int arg1) {
super(arg0, arg1);
}
public PositionLayoutParams(LayoutParams arg0) {
super(arg0);
}
public PositionLayoutParams(MarginLayoutParams arg0) {
super(arg0);
}
}
}
在自定义的 PositionLayoutParams 类中,根据父类的要求定义了 4 个构造方法,其中构造方法 public PositionLayoutParams(Context context, AttributeSet attrs)读取了 layout_position属性值,保存在 position 的成员变量中,如果未读取到该属性,则默认为 NONE。其次定义了 4个常量与 layout_position 属性的 4个枚举值相对应
ViewGroup 类重写了 generateLayoutParams( ) 和 generateDefaultParams( )方法返回的 LayoutParams 为 PositionLayoutParams 对象,其中 public LayoutParams generateLayout Params(AttributeSet attrs)方法将 attrs 传入了 public PositionLayoutParams(Context c, AttributeSet attrs) 构造方法,所以,PositionLayoutParams 才能读取到 layout_position 的属性值
在 onLayou( )方法中,我们需要根据当前子组件的 PositionLayoutParams 的 position 属性来确定方向,这里的有两种情况:一种是没有为组件定义方位时,依然按照 A—>—>B ——>C ——>D 的方式进行放置;另一种是如果子组件定义了 特定的方位,如 right_bottom,则将该组件显示在容器的右下角
为了更加清晰的看清 CornerLayout2 容器内子组件的位置,我们为子组件 TextView 分别添加 A、B、C、D 四个字符作为 text 的属性值。然后,分别对其在没有设置 特定方向和设置特定方向就行测试,其 layout.xml 如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
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" >
<myviewgroup.CornerLayout2
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"
app: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"
app: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"
app: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"
app:layout_position="left_top"
/>
</myviewgroup.CornerLayout2>
</LinearLayout>
运行效果如下:
7.4、案例:流式布局
在 JavaSwing中,有一种布局,叫流式布局(FlowLayout),这种布局的特点是子组件按照从左往右、从上往下的顺序依次排序,如果一行放不下,自动显示到下一行,和 HTML 中 float 效果类似,但是,在 Anroid 中没有土工这样的布局。本节,我们一起实现这种布局对于 FlowLayout 来说,难点有二:一是,要要事先预测组件的宽度和高度,这个和上面( CornerLayout)有明显的不同,FlowLayout 中的组件数是不固定的,而 CornerLayout 中最多只支持 4 个子组件;二是,对子组件进行定位,也是一个疼痛的问题,子组件的大小不一,数量不一,每一个组件放哪一行‘放在一种的什么位置都需要计算,最重要的是要找到规律,不可能一个一个去处理
测量 FlowLayout 容器的宽度时,不允许子组件的宽度比容器的宽度还要大,这是前提。当子组件个数很少,气总宽度比容器的 layout_width 为 match_parent 时的宽度小。那么,容器的 layout_width 为 wrap_content 时就是子组件的宽度之和。但是,如果子组件的个数很多,总宽度超出容器的最大宽度,则就容器的 layout_width 为 wrap_content 最终的宽度也要采用 match_parent 值,并且需要另起一行继续显示上一行余下的子组件
FlowLayout 容器高度是每一行最高的组件的高度之和,因测量时并不需要显示子组件,所以我们采用预测的方法判断是否需要换行,换行后计算出当前行的最高的组件高度进行累加,最后算出所有行的最高高度之和
重写 onLayout( ) 方法定位子组件时,是一个逻辑工作。从第 0 个子组件开始,一个个进行 定位,如果当前行的已占宽度加上当前子组件的宽度大于容器的宽度,则要换行。换行其实就是将当前子组件的宽度重新设置为 0,高度就是前有所有行的高度之和。每成功定位一个组件,都要计算出当前行行的最高高度并累计当前行已占宽度
public class FlowLayout extends ViewGroup {
private static final String TAG = "info";
public FlowLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public FlowLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public FlowLayout(Context context) {
this(context,null);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
measureChildren(widthMeasureSpec, heightMeasureSpec);
int width = measureWidth(widthMeasureSpec);
int height = measureHeight(heightMeasureSpec);
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int n = getChildCount() ;
//当前行的子组件的最大高度
int maxViewHeigth = 0 ;
//当前行的子组件的总宽度
int maxLineWidth = 0;
//累计高度
int totalHeight = 0 ;
//容器的宽度
int width = this.getMeasuredWidth();
for(int i = 0;i < n; i++){
View child = getChildAt(i) ;
//判断是否需要换行显示(已占宽度你+ 前子组件宽度是否大于容器的哭)
if(maxLineWidth + getChildAt(i).getMeasuredWidth() > width -getPaddingLeft() - getPaddingRight()){
//换行后累计已显示的行的总高度
totalHeight += maxViewHeigth ;
Log.i(TAG, "totalHeight:" + totalHeight + " maxLineWidth:" +maxLineWidth + " width:" + width);
//新起一行,新行的已占宽度和高重置 0
maxLineWidth = 0;
maxViewHeigth = 0;
}
layoutChild(child,maxLineWidth,totalHeight,
maxLineWidth + child.getMeasuredWidth(),
totalHeight + child.getMeasuredHeight());
//获取当前行的坐高高度
maxViewHeigth = Math.max(maxViewHeigth, child.getMeasuredHeight()) ;
//累加当前行的宽度
maxLineWidth += child.getMeasuredWidth();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
/**
* 定位子组件,方法内考虑 padding
* @param child 需要定位的 View
* @param maxLineWidth left
* @param totalHeight top
* @param i right
* @param j bottom
*/
private void layoutChild(View child, int maxLineWidth, int totalHeight,
int i, int j) {
Log.i(TAG, child.getTag() + ":" + " Left:" + maxLineWidth + " Top:"+ totalHeight + " Right:" + i + " Bottom:" + j);
//所有的子组件 要统一向右和向下【平移指定的 padding
child.layout(maxLineWidth + getPaddingLeft(), totalHeight + getPaddingTop(),
i + getPaddingLeft(), j + getPaddingTop()) ;
}
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;
Log.i(TAG, "childCount - width = "+n);
for (int i = 0; i <n; i++) {
View child = getChildAt(i);
int childWidth = child.getMeasuredWidth() ;
Log.i(TAG, "childMeasureWidth = "+childWidth + "childMeasureHeidght = "+child.getMeasuredHeight());
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() ;
Log.i(TAG, "width = "+ width) ;
return width;
}
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){ //高随内容
int width = getMeasuredWidth() ;
int n = getChildCount() ;
//当前行的子组件的最大高度
int maxViewHeigth = 0 ;
//当前行的子组件的总宽度
int maxLineWidth = 0;
Log.i(TAG, "childCount - height = "+n);
for(int i = 0 ;i < n ; i++){
View child = getChildAt(i) ;
maxLineWidth += child.getMeasuredWidth();
maxViewHeigth = Math.max(child.getMeasuredHeight(), maxViewHeigth) ;
//预测是否需要换行
if(i < n -1 &&
maxLineWidth + getChildAt(i+1).getMeasuredWidth() > width - getPaddingLeft() -getPaddingRight()){
//当前行的子组件宽度如果超出容器的宽度,则需要换行
height += maxViewHeigth ;
maxLineWidth = 0;
maxViewHeigth = 0;
}else if(i == n-1){
//已经是最后一个
height += maxViewHeigth ;
}
}
}
//padding
height += getPaddingTop() + getPaddingBottom() ;
Log.i(TAG, "height = "+ height) ;
return height;
}
}