株洲新程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 传入方法常常或有奇效
  ViewGroup 运行的基本流程大致为
  • 测量容器尺寸
  • 重写 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个参数,基本可以确定子组件的位置与大小
  通过下面的示例,一是了解下自定义容器的定义方法,二是了解重写 ViewGroup 方法的一些细节
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篇)
0 0
原创粉丝点击