株洲新程IT 教育 李赞红老师 第七章 自定义容器
来源:互联网 发布:matlab 三维数组插值 编辑:程序博客网 时间:2024/04/29 07:01
第七章 自定义容器
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; }}
7.5、参考链接
- 视屏:hyman -> 打造Android中的流式布局和人们标签
- 博客:hyman -> Android 打造炫目的圆形菜单 秒秒钟高仿建行圆形菜单
- 博客:Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)
- 株洲新程IT 教育 李赞红老师 第七章 自定义容器
- 株洲新程IT 教育 李赞红老师 第二章 Graphics2D API
- 株洲新程IT 教育 李赞红老师 第三章 使用Graphics2D实现动态效果
- 株洲新程IT 教育 李赞红老师 第四章 双缓存技术
- 株洲新程IT 教育 李赞红老师 第五章 阴影、渐变和位图的运算
- 株洲新程IT 教育 李赞红老师 第八章 Scroller 与平滑滚动
- 洲新城 IT教育 李赞红老师 第一章节
- 宋老师,IT教育神坛级的人物
- 作IT学生不容易,当IT老师也很难(续)——软件教育随想(1)
- 作IT学生不容易,当IT老师也很难(续)——软件教育随想(2)
- 作IT学生不容易,当IT老师也很难(续)——软件教育随想(3)
- 作IT学生不容易,当IT老师也很难(续)——软件教育随想(4)
- 新株洲站2009年前建成
- it教育
- 南通青鸟 IT 教育 96 班同学在 3 教室上陆老师的 Java 课
- 第七章上机练习2 老师
- ★第七章:容器/集合
- 第七章:新的开始
- 判断一棵树是否是平衡二叉树
- 关于OpenCV的基本数据类型
- centos下安装KVM虚拟机
- WCF Web编程模型
- Android Studio中获取sha1证书的方法
- 株洲新程IT 教育 李赞红老师 第七章 自定义容器
- Storm研究综述
- 栈和函数调用
- cd
- Linux下压缩某个文件夹(文件夹打包)
- GCD 多线程——串行队列
- 测试代码执行效率的几种方法比较
- conda下载速度太慢
- POJ1724最短路