Android -- ViewGroup源码分析+自定义

1,我们前三篇博客了解了一下自定义View的基本方法和流程

     从源码的角度一步步打造自己的TextView

     深入了解自定义属性

     onMeasure()源码分析

  之前,我们只是学习过自定义View,其实自定义ViewGroup和自定义View的步骤差不了多少,他们的的区别主要来自各自的作用不同,ViewGroup是容器,用来包含其他控件,而View是真正意义上看得见摸得着的,它需要将自己画出来。ViewGroup需要重写onMeasure方法测量子控件的宽高和自己的宽高,然后实现onLayout方法摆放子控件。而 View则是需要重写onMeasure根据测量模式和父控件给出的建议的宽高值计算自己的宽高,然后再父控件为其指定的区域绘制自己的图形。

  但是仅仅是了解自定义view还是不够的,我们还要学习一下我们的ViewGroup,例如SlideMenu、CardLayout、 CustomLayout等。先看一下我们的官方文档来怎么描述我们的

ViewGroup是一种可以包含其他视图的特殊视图,他是各种布局和所有容器的基类,这些类也定义了ViewGroup.LayoutParams类作为类的布局参数。

  所以我们现在可以自定义ViewGroup分为下面这几步:

  1,继承自ViewGroup,重写构造方法
  2,重写OnMeasure()方法,丈量子控件和自身宽高
  3,重写OnLayout()方法,摆放子控件位置

  

2,实现简单的水平排列结果

  先创建自定义ViewGroup,实现从左到右,排满换行的的功能

package com.qianmo.activitydetail.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * Created by wangjitao on 2017/3/23 0023.
 * E-Mail:543441727@qq.com
 */

public class MyLayout extends ViewGroup {
    private static String TAG = "MyLayout";

    public MyLayout(Context context) {
        this(context, null);
    }

    public MyLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }

    /**
     * 所有子view自己测量大小,然后根据自孩子的大小完成自己的尺寸测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //首先计算所有子view的宽高
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //保留测量的宽高(这里使用wrap_content和match_parent都是填充屏幕)
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

    }

    /**
     * 为所有的子控件摆放位置
     *
     * @param changed
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        //获取子控件数量
        final int count = getChildCount();
        int childMeasureWidth = 0;
        int childMeasureHeight = 0;

        //容器已经占据的宽高度
        int layoutWidth = 0;
        int layoutHeight = 0;

        //每一行的高度是这一行中最高控件的高度
        int maxChildHeight = 0;

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            //注意此处不能使用getWidth和getHeight,这两个方法必须在onLayout执行完,才能正确获取宽高
            childMeasureHeight = child.getMeasuredHeight();
            childMeasureWidth = child.getMeasuredWidth();

            Log.i(TAG, "getWidth():" + getWidth());
            Log.i(TAG, "childMeasureHeight:" + childMeasureHeight);
            Log.i(TAG, "childMeasureWidth:" + childMeasureWidth);
            getWidth();
            if (layoutWidth < getWidth()) {
                //如果一行没有排满,继续往右排列
                left = layoutWidth;
                right = left + childMeasureWidth;
                top = layoutHeight;
                bottom = top + childMeasureHeight;
            } else {
                //排满后就换行
                layoutWidth = 0;
                layoutHeight += maxChildHeight;
                left = layoutWidth;
                right = left + childMeasureWidth;
                top = layoutHeight;
                bottom = top + childMeasureHeight;
            }
            //宽度累加
            layoutWidth += childMeasureWidth;
            //记录本次最高宽度
            if (childMeasureHeight > maxChildHeight) {
                maxChildHeight = childMeasureHeight;
            }

            //确定子控件的位置,四个参数分别代表上下左右的坐标值
            child.layout(left, top, right, bottom);
        }
    }
}

  布局文件

<?xml version="1.0" encoding="utf-8"?>
<com.qianmo.activitydetail.view.MyLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:myview="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FF8247"
        android:padding="20dip"
        android:text="按钮1"
        android:textColor="#ffffff"
        android:textSize="20dip"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#8B0A50"
        android:padding="10dip"
        android:text="按钮2222222222222"
        android:textColor="#ffffff"
        android:textSize="20dip"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#7CFC00"
        android:padding="15dip"
        android:text="按钮333333"
        android:textColor="#ffffff"
        android:textSize="20dip"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#1E90FF"
        android:padding="10dip"
        android:text="按钮4"
        android:textColor="#ffffff"
        android:textSize="10dip"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#191970"
        android:padding="15dip"
        android:text="按钮5"
        android:textColor="#ffffff"
        android:textSize="20dip"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#7A67EE"
        android:padding="20dip"
        android:text="按钮6"
        android:textColor="#ffffff"
        android:textSize="20dip"/>

</com.qianmo.activitydetail.view.MyLayout>

  看一下运行效果

3,自定义LayoutParams,实现RelativeLayout的layout_alignLeft、layout_alignRight、layout_alignTop、layout_alignBottom功能

  回想一下我们平时使用RelativeLayout的时候,在布局文件中使用android:layout_alignParentRight="true"、android:layout_centerInParent="true"等各种属性,就能控制子控件显示在父控件的上下左右、居中等效果。 在上一篇讲onMeasure的博客中,我们有了解过ViewGroup.LayoutParams类,ViewGroup中有两个内部类ViewGroup.LayoutParams和ViewGroup.MarginLayoutParams,MarginLayoutParams继承自LayoutParams,这两个内部类就是ViewGroup的布局参数类,比如我们在LinearLayout等布局中使用的layout_width\layout_hight等以“layout_ ”开头的属性都是布局属性。
  在View中有一个mLayoutParams的变量用来保存这个View的所有布局属性。ViewGroup.LayoutParams有两个属性layout_width和layout_height,因为所有的容器都需要设置子控件的宽高,所以这个LayoutParams是所有布局参数的基类,如果需要扩展其他属性,都应该继承自它。比如RelativeLayout中就提供了它自己的布局参数类RelativeLayout.LayoutParams,并扩展了很多布局参数。

  • 大致明确布局容器的需求,初步定义布局属性

  在定义属性之前要弄清楚,我们自定义的布局容器需要满足那些需求,需要哪些属性,比如,我们现在要实现像相对布局一样,为子控件设置一个位置属性layout_position=”“,来控制子控件在布局中显示的位置。暂定位置有五种:左上、左下、右上、右下、居中。有了需求,我们就在attr.xml定义自己的布局属性

 <declare-styleable name="MyLayout2">
        <attr name="layout_position">
            <enum name="left" value="1"/>
            <enum name="top" value="2"/>
            <enum name="right" value="3"/>
            <enum name="bottom" value="4"/>
            <enum name="center" value="5"/>
        </attr>
    </declare-styleable>
  • 继承LayoutParams,定义布局参数类

  我们可以选择继承ViewGroup.LayoutParams,覆盖构造方法,然后在有AttributeSet参数的构造方法中初始化参数值,这个构造方法才是布局文件被映射为对象的时候被调用的。

package com.qianmo.activitydetail.java;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.ViewGroup;

import com.qianmo.activitydetail.R;

/**
 * Created by wangjitao on 2017/3/23 0023.
 * E-Mail:543441727@qq.com
 */

public class MyLayoutParams extends ViewGroup.LayoutParams {

    public static final int POSITION_LEFT = 1;
    public static final int POSITION_TOP = 2;
    public static final int POSITION_RIGHT = 3;
    public static final int POSITION_BOTTOM = 4;
    public static final int POSITION_CENTER = 5;

    public int position = POSITION_LEFT;

    public MyLayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.MyLayout2);
        position = a.getInt(R.styleable.MyLayout2_layout_position, POSITION_LEFT);
    }

    public MyLayoutParams(int width, int height) {
        super(width, height);
    }

    public MyLayoutParams(ViewGroup.LayoutParams source) {
        super(source);
    }
}
  • 重写generateLayoutParams()

  在ViewGroup中有下面几个关于LayoutParams的方法,generateLayoutParams (AttributeSet attrs)是在布局文件被填充为对象的时候调用的,这个方法是下面几个方法中最重要的,如果不重写它,我们布局文件中设置的布局参数都不能拿到。

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new MyLayoutParams(p);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MyLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof MyLayoutParams;
    }
  • 在布局文件中使用布局属性
<?xml version="1.0" encoding= "utf-8"?>
<com.qianmo.activitydetail.view.MyLayout2
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:myview="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#FF8247"
        android:padding="20dip"
        android:text="按钮1"
        android:textColor="#ffffff"
        android:textSize="20dip"
        myview:layout_position="left"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#8B0A50"
        android:padding="10dip"
        android:text="按钮2222222222222"
        android:textColor="#ffffff"
        android:textSize="18dip"
        myview:layout_position="right"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#7CFC00"
        android:padding="15dip"
        android:text="按钮333333"
        android:textColor="#ffffff"
        android:textSize="20dip"
        myview:layout_position="bottom"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#1E90FF"
        android:padding="10dip"
        android:text="按钮4"
        android:textColor="#ffffff"
        android:textSize="15dip"
        myview:layout_position="top"/>

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#191970"
        android:padding="15dip"
        android:text="按钮5"
        android:textColor="#ffffff"
        android:textSize="20dip"
        myview:layout_position="center"/>
</com.qianmo.activitydetail.view.MyLayout2>
  • 在onMeasure和onLayout中使用布局参数

  经过上面几步之后,我们运行程序,就能获取子控件的布局参数了,在onMeasure方法和onLayout方法中,我们按照自定义布局容器的特殊需求,对宽度和位置坐特殊处理。这里我们需要注意一下,如果布局容器被设置为包裹类容,我们只需要保证能将最大的子控件包裹住就ok,代码注释比较详细,就不多说了。

package com.qianmo.activitydetail.view;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

import com.qianmo.activitydetail.java.MyLayoutParams;

/**
 * Created by wangjitao on 2017/3/23 0023.
 * E-Mail:543441727@qq.com
 * 通过自定义LayoutParams设置特殊的属性
 */

public class MyLayout2 extends ViewGroup {
    private static String TAG = "MyLayout";

    public MyLayout2(Context context) {
        this(context, null);
    }

    public MyLayout2(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyLayout2(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }

    /**
     * 在ViewGroup中有下面几个关于LayoutParams的方法,generateLayoutParams (AttributeSet attrs)是在布局文件被填充为对象的时候调用的
     * 如果不重写它,我么布局文件中设置的布局参数都不能拿到。
     *
     * @param attrs
     * @return
     */

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new MyLayoutParams(p);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MyLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof MyLayoutParams;
    }

    /**
     * 所有子view自己测量大小,然后根据自孩子的大小完成自己的尺寸测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取本ViewGroup上机容器为其推荐的款和高,以及计算模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //计算出所有子控件的宽和高
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int childWidth = 0;
        int childHeight = 0;
        int chileCount = getChildCount();

        //测量的父控件的宽高
        int layoutHeight = 0;
        int layoutWidth = 0;

        //进行宽度模式的判断
        if (widthMode == MeasureSpec.EXACTLY) {
            //这时不具容器的宽度模式是确定的(具体的size或者match_patent,直接使用父窗体建议的宽度)
            layoutWidth = widthSize;
        } else {
            //如果是未指定的活wrap_content,我们一般按照包裹内容来处理,宽度就拿所有控件的宽度和为宽度
            for (int i = 0; i < chileCount; i++) {
                View child = getChildAt(i);
                childWidth = child.getMeasuredWidth();
                //获取子控件最大宽度
                layoutWidth = childWidth > layoutWidth ? childWidth : layoutWidth;
            }
        }

        //高度模式一样
        if (heightMode == MeasureSpec.EXACTLY) {
            //这时不具容器的宽度模式是确定的(具体的size或者match_patent,直接使用父窗体建议的宽度)
            layoutHeight = heightSize;
        } else {
            //如果是未指定的活wrap_content,我们一般按照包裹内容来处理,宽度就拿所有控件的宽度和为宽度
            for (int i = 0; i < chileCount; i++) {
                View child = getChildAt(i);
                childHeight = child.getMeasuredHeight();
                //获取子控件最大高度
                layoutHeight = childHeight > layoutHeight ? childHeight : layoutHeight;
            }
        }

        //保存测量宽高数据
        setMeasuredDimension(layoutWidth, layoutHeight);
    }

    /**
     * 为所有的子控件摆放位置
     *
     * @param changed
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int childWidth = 0;
        int childHeight = 0;
        int chileCount = getChildCount();
        MyLayoutParams params = null;

        for (int i = 0; i < chileCount; i++) {
            childWidth = getChildAt(i).getMeasuredWidth();
            childHeight = getChildAt(i).getMeasuredHeight();

            params = (MyLayoutParams) getChildAt(i).getLayoutParams();
            switch (params.position) {
                case MyLayoutParams.POSITION_LEFT:
                    //左上方
                    left = 0;
                    top = 0;
                    break;
                case MyLayoutParams.POSITION_TOP:
                    //右上方
                    left = getWidth() - childWidth;
                    top = 0;
                    break;
                case MyLayoutParams.POSITION_RIGHT:
                    //右下方
                    left = 0;
                    top = getHeight() - childHeight;
                    break;
                case MyLayoutParams.POSITION_BOTTOM:
                    left = getWidth() - childWidth;
                    top = getHeight() - childHeight;
                    break;
                case MyLayoutParams.POSITION_CENTER:
                    left = (getWidth() - childWidth) / 2;
                    top = (getHeight() - childHeight) / 2;
                    break;
                default:
                    break;
            }
            // 确定子控件的位置,四个参数分别代表(左上右下)点的坐标值
            getChildAt(i).layout(left, top, left + childWidth, top + childHeight);

        }
    }
}

  看一下运行效果

4,支持layout_margin属性

  如果我们自定义的布局参数类继承自MarginLayoutParams,就自动支持了layout_margin属性了,我们需要做的就是直接在布局文件中使用layout_margin属性,然后再onMeasure和onLayout中使用margin属性值测量和摆放子控件。需要注意的是我们测量子控件的时候应该调用measureChildWithMargin()方法。

<?xml version="1.0" encoding= "utf-8"?>
<com.qianmo.activitydetail.view.MyLayout3
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:myview="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        myview:layout_position= "left"
        android:layout_marginLeft = "20dip"
        android:background= "#FF8247"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "20dip"
        android:text="按钮1" />

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:layout_marginTop = "30dip"
        myview:layout_position= "top"
        android:background= "#8B0A50"
        android:textColor= "#ffffff"
        android:textSize="18dip"
        android:padding= "10dip"
        android:text="按钮2222222222222" />

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        android:layout_marginLeft = "30dip"
        android:layout_marginBottom = "10dip"
        myview:layout_position= "bottom"
        android:background= "#7CFC00"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "15dip"
        android:text="按钮333333" />

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        myview:layout_position= "right"
        android:layout_marginBottom = "30dip"
        android:background= "#1E90FF"
        android:textColor= "#ffffff"
        android:textSize="15dip"
        android:padding= "10dip"
        android:text="按钮4" />

    <Button
        android:layout_width= "wrap_content"
        android:layout_height= "wrap_content"
        myview:layout_position= "center"
        android:layout_marginBottom = "30dip"
        android:layout_marginRight = "30dip"
        android:background= "#191970"
        android:textColor= "#ffffff"
        android:textSize="20dip"
        android:padding= "15dip"
        android:text="按钮5" />

</com.qianmo.activitydetail.view.MyLayout3>

  我们创建类继承自MarginParams类

package com.qianmo.activitydetail.java;

import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.ViewGroup;

import com.qianmo.activitydetail.R;

/**
 * Created by wangjitao on 2017/3/23 0023.
 * E-Mail:543441727@qq.com
 * 添加外边框参数
 */

public class MyLayoutParamsWithMargin extends ViewGroup.MarginLayoutParams {

    public static final int POSITION_LEFT = 1;
    public static final int POSITION_TOP = 2;
    public static final int POSITION_RIGHT = 3;
    public static final int POSITION_BOTTOM = 4;
    public static final int POSITION_CENTER = 5;

    public int position = POSITION_LEFT;

    public MyLayoutParamsWithMargin(Context c, AttributeSet attrs) {
        super(c, attrs);
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.MyLayout2);
        position = a.getInt(R.styleable.MyLayout2_layout_position, POSITION_LEFT);
    }

    public MyLayoutParamsWithMargin(int width, int height) {
        super(width, height);
    }

    public MyLayoutParamsWithMargin(ViewGroup.LayoutParams source) {
        super(source);
    }
}

  在generateLayoutParams()方法中替换类

 @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParamsWithMargin(getContext(), attrs);
    }

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MyLayoutParamsWithMargin(p);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MyLayoutParamsWithMargin(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected boolean checkLayoutParams(LayoutParams p) {
        return p instanceof MyLayoutParamsWithMargin;
    }

  onMeasure和onLayout:

 /**
     * 所有子view自己测量大小,然后根据自孩子的大小完成自己的尺寸测量
     *
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取本ViewGroup上机容器为其推荐的款和高,以及计算模式
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        //计算出所有子控件的宽和高
//        measureChildren(widthMeasureSpec, heightMeasureSpec);
        int childWidth = 0;
        int childHeight = 0;
        int chileCount = getChildCount();

        //测量的父控件的宽高
        int layoutHeight = 0;
        int layoutWidth = 0;

        // 计算出所有的childView的宽和高
        for (int i = 0; i < chileCount; i++) {
            View child = getChildAt(i);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
        MyLayoutParamsWithMargin params = null;

        //进行宽度模式的判断
        if (widthMode == MeasureSpec.EXACTLY) {
            //这时不具容器的宽度模式是确定的(具体的size或者match_patent,直接使用父窗体建议的宽度)
            layoutWidth = widthSize;
        } else {
            //如果是未指定的活wrap_content,我们一般按照包裹内容来处理,宽度就拿所有控件的宽度和为宽度
            for (int i = 0; i < chileCount; i++) {
                View child = getChildAt(i);
                childWidth = child.getMeasuredWidth();
                params = (MyLayoutParamsWithMargin) child.getLayoutParams();
                //获取子控件最大宽度(要算上左右间距)
                layoutWidth = childWidth > layoutWidth + params.leftMargin + params.rightMargin ? childWidth : layoutWidth;
            }
        }

        //高度模式一样
        if (heightMode == MeasureSpec.EXACTLY) {
            //这时不具容器的宽度模式是确定的(具体的size或者match_patent,直接使用父窗体建议的宽度)
            layoutHeight = heightSize;
        } else {
            //如果是未指定的活wrap_content,我们一般按照包裹内容来处理,宽度就拿所有控件的宽度和为宽度
            for (int i = 0; i < chileCount; i++) {
                View child = getChildAt(i);
                childHeight = child.getMeasuredHeight();
                params = (MyLayoutParamsWithMargin) child.getLayoutParams();
                //获取子控件最大高度
                layoutHeight = childHeight > layoutHeight + params.topMargin + params.bottomMargin ? childHeight : layoutHeight;
            }
        }

        //保存测量宽高数据
        setMeasuredDimension(layoutWidth, layoutHeight);
    }

    /**
     * 为所有的子控件摆放位置
     *
     * @param changed
     * @param left
     * @param top
     * @param right
     * @param bottom
     */
    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int childWidth = 0;
        int childHeight = 0;
        int chileCount = getChildCount();
        MyLayoutParamsWithMargin params = null;

        for (int i = 0; i < chileCount; i++) {
            childWidth = getChildAt(i).getMeasuredWidth();
            childHeight = getChildAt(i).getMeasuredHeight();

            params = (MyLayoutParamsWithMargin) getChildAt(i).getLayoutParams();
            switch (params.position) {
                case MyLayoutParams.POSITION_LEFT:
                    //左上方
                    left = 0 + params.leftMargin;
                    top = 0 + params.topMargin;
                    break;
                case MyLayoutParams.POSITION_TOP:
                    //右上方
                    left = getWidth() - childWidth - params.rightMargin;
                    top = 0 + params.topMargin;
                    break;
                case MyLayoutParams.POSITION_RIGHT:
                    //左下方
                    left = 0 + params.leftMargin;
                    top = getHeight() - childHeight - params.bottomMargin;
                    break;
                case MyLayoutParams.POSITION_BOTTOM:
                    //右下角
                    left = getWidth() - childWidth - params.rightMargin;
                    top = getHeight() - childHeight - params.bottomMargin;
                    break;
                case MyLayoutParams.POSITION_CENTER:
                    left = (getWidth() - childWidth) / 2;
                    top = (getHeight() - childHeight) / 2;
                    break;
                default:
                    break;
            }
            // 确定子控件的位置,四个参数分别代表(左上右下)点的坐标值
            getChildAt(i).layout(left, top, left + childWidth, top + childHeight);

        }
    }

  运行效果

  总结一下我们的学习内容

  自定义ViewGroup的步骤

①. 继承ViewGroup,覆盖构造方法 
②. 重写onMeasure方法测量子控件和自身宽高 
③. 实现onLayout方法摆放子控件

  为布局容器自定义布局属性:

①. 大致明确布局容器的需求,初步定义布局属性 
②. 继承LayoutParams,定义布局参数类 
③. 重写获取布局参数的方法 
④. 在布局文件中使用布局属性 
⑤. 在onMeasure和onLayout中使用布局参数

posted @ 2017-03-23 14:42  阿呆哥哥  阅读(627)  评论(0编辑  收藏  举报