ViewGroup详解

什么是父控件和子控件

        父控件就是容纳子控件的控件(也就是我们常说的布局)也称作容器,常见的父控件有LinearLayout,RelativeLayout,FrameLayout,TableLayout,GridLayout;
        子控件是被父控件(也就是我们常说的布局)包裹住的控件,常见的子控件有TextView,Button,Edit Text,ImageView
例:下面的代码中LinearLayout为父控件,里面容纳的TextView为子控件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
  <TextView 
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="这是一段文字"
      android:textSize="25dp"
      android:textColor="@android:color/white"
      android:background="@android:color/holo_blue_light"/>
</LinearLayout>

什么是ViewGroup

        ViewGroup是上面提到的所有的父控件的父类;但ViewGroup是一个抽象类,它里面有一个抽象方法onLayout,这个方法的作用就是摆放它所有的子控件(安排位置),因为是抽象类,不能直接new对象,所以我们在布局文件中不能直接使用 ViewGroup
例:直接使用ViewGroup为父控件运行后会报错

<?xml version="1.0" encoding="utf-8"?>
<ViewGroup
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
  <TextView
      android:layout_width="match_parent"
      android:layout_height="wrap_content"
      android:text="这是一段文字"
      android:textSize="25dp"
      android:textColor="@android:color/white"
      android:background="@android:color/holo_blue_light"/>
</ViewGroup>

在这里插入图片描述                                                                                               不能实例化抽象类android.view.ViewGroup

ViewGroup的工作原理

        创建一个View(执行构造方法)的时候不需要测量控件的大小,只有将这个view放入容器(父控件)中的时候才需要测量;当子控件的父控件要放置该子控件的时候,父控件会调用子控件的onMeaonsure()方法,然后传入两个参数widthMeasureSpec和heightMeasureSpec,这两个参数就是父控件告诉子控件可获得的空间以及关于这个空间的约束条件,子控件取得这些条件就能正确的测量自身的宽高了
如果还有不明白请点击这里onMeaonsure()方法详解

自定义ViewGroup步骤

  1. 覆盖构造方法(必须)
  2. 重写onMeaonsure()测量控件尺寸(可选)
  3. 重写onLayout()方法摆放控件(必须)

下面是个简单的自定义ViewGroup的例子,顺序放置所有控件,如果一行不够放置则换行

public class MyLayout extends ViewGroup {

    private Context mContext;

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

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs, 0);
        this.mContext=context;
    }

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

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        measureChildren(widthMeasureSpec, heightMeasureSpec);// 计算出所有的childView的宽和高
        //测量并保存layout的宽高(使用getDefaultSize时,wrap_content和match_perent都是填充屏幕)
        //稍后会重新写这个方法,能达到wrap_content的效果
        setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();   //子控件个数
        int childWidth = 0;                  //子控件宽度
        int childHeight = 0;                 //子控件高度
        int myLayoutWidth = 0;               //MyLayout当前宽度
        int myLayoutHeight = 0;              //MyLayout当前高度
        int maxHeight = 0;                   //一行中子控件最高的高度
        for(int i = 0; i<count; i++){
            View child = getChildAt(i);
            //注意此处不能使用getWidth和getHeight,这两个方法必须在onLayout执行完,才能正确获取宽高
            childWidth = child.getMeasuredWidth();       //子控件的测量宽度
            childHeight = child.getMeasuredHeight();     //子控件的测量高度
            if( getWindowWidthPixels ()-myLayoutWidth > childWidth ){ //屏幕剩余宽度大于子控件宽度
                left = myLayoutWidth;
                right = left+childWidth;
                top = myLayoutHeight;
                bottom = top+childHeight;
            } else{ //屏幕剩余宽度小于子控件宽度就换行
                myLayoutWidth = 0;            //myLayoutWidth重置为0
                myLayoutHeight += maxHeight;  //当前myLayoutHeight=myLayoutHeight+上一行子控件的最大高
                maxHeight = 0;                //maxHeight重置为0

                left = myLayoutWidth;
                right = left+childWidth;
                top = myLayoutHeight;
                bottom = top+childHeight;
            }
            myLayoutWidth += childWidth;  //宽度累加
            if(childHeight > maxHeight){  //如果子控件的高度大于行最大高度
                maxHeight = childHeight;  //行最大高度设置为子控件高度
            }
            child.layout(left, top, right, bottom);//摆放子控件(左上右下)点的坐标值
        }
    }

    //获取屏幕宽度
    private int getWindowWidthPixels () {
        Resources resources = mContext.getResources();
        DisplayMetrics displayMetrics = resources.getDisplayMetrics();
        int width = displayMetrics.widthPixels;
        return width;
    }
}

<com.example.andy.mytest.MyLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView1"
        android:textSize="25dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"/>
    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView2"
        android:textSize="25dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"/>
    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView3"
        android:textSize="25dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"/>
    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView4"
        android:textSize="25dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"/>
    <TextView
        android:id="@+id/textView5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView5"
        android:textSize="25dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"/>
    <TextView
        android:id="@+id/textView6"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView6"
        android:textSize="25dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"/>
    <TextView
        android:id="@+id/textView7"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView7"
        android:textSize="25dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"/>
    <TextView
        android:id="@+id/textView8"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="TextView8"
        android:textSize="25dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"/>
</com.example.andy.mytest.MyLayout>

在这里插入图片描述

复杂的自定义ViewGroup

例:编写一个自定义ViewGroup扩展它的布局属性,子控件按扩展的布局属性放置显示

  1. 自定义ViewGroup的布局属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="MyTextView">
        <!--声明MyTextView需要使用系统定义过的text属性,注意前面需要加上android命名-->
        <attr name="android:text" />
        <attr name="android:layout_width" />
        <attr name="android:layout_height" />
        <attr name="android:background" />
        <attr name="mTextColor" format="color" />
        <attr name="mTextSize" format="dimension" />
    </declare-styleable>

    <declare-styleable name ="CustomLayout">
        <attr name ="layout_position">
            <enum name ="center" value="0" />
            <enum name = "Left" value="1"/>
            <enum name = "Right" value="2"/>
            <enum name = "top" value="3"/>
            <enum name = "Bottom" value="4"/>
            <enum name ="LeftTop" value="5" />
            <enum name ="RightTop" value="6" />
            <enum name ="LeftBottom" value="7" />
            <enum name ="RightBottom" value="8" />
        </attr >
    </declare-styleable>
</resources>
  1. 自定义LayoutParams类
    自定义LayoutParams的作用是扩展ViewGroup的布局属性
           可以继承ViewGroup.LayoutParams,也可以继承ViewGroup.MarginLayoutParams;
           继承ViewGroup.LayoutParams,自定义布局只是简单的支持layout_width和layout_height属性;
           继承ViewGroup.MarginLayoutParams,就能使用layout_marginxxx属性
public class MyLayoutParams extends ViewGroup.MarginLayoutParams {

    public static final int POSITION_CENTER = 0;        // 中间
    public static final int POSITION_LEFT = 1;          // 左侧
    public static final int POSITION_RIGHT = 2;         // 右侧
    public static final int POSITION_TOP = 3;           // 上侧
    public static final int POSITION_BOTTOM = 4;        // 下侧
    public static final int POSITION_LEFT_TOP = 5;      // 左上方
    public static final int POSITION_RIGHT_TOP = 6;     // 右上方
    public static final int POSITION_LEFT_BOTTOM = 7;   // 左下方
    public static final int POSITION_RIGHT_BOTTOM = 8;  // 右下方

    public int POSITION = POSITION_CENTER;              // 默认位置为中间

    //在这个构造方法中初始化参数值,会使布局文件被映射为对象的时候被调用
    public MyLayoutParams(Context c, AttributeSet attrs) {
        super(c, attrs);
        TypedArray typedArray = c.obtainStyledAttributes(attrs,R.styleable.CustomLayout );
        //获取设置在子控件上的位置属性
        POSITION = typedArray.getInt(R.styleable.CustomLayout_layout_position ,POSITION );
        typedArray.recycle();
    }

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

    public MyLayoutParams(ViewGroup.MarginLayoutParams source) {
        super(source);
    }

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

  1. 重写generateLayoutParams()
<com.example.andy.mytest.MyLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ying="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/holo_red_light">
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="文字1"
        android:textSize="25dp"
        android:padding="10dp"
        android:layout_marginLeft="10dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"
        ying:layout_position="LeftTop"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="文字2"
        android:textSize="25dp"
        android:padding="10dp"
        android:layout_marginLeft="10dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"
        ying:layout_position="LeftBottom"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="文字3"
        android:textSize="25dp"
        android:padding="10dp"
        android:layout_marginRight="10dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"
        ying:layout_position="RightTop"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="文字4"
        android:textSize="25dp"
        android:padding="10dp"
        android:layout_marginRight="10dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"
        ying:layout_position="RightBottom"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="文字5"
        android:textSize="25dp"
        android:padding="10dp"
        android:layout_marginTop="50dp"
        android:textColor="@android:color/white"
        android:background="@android:color/holo_blue_light"
        ying:layout_position="center"/>
</com.example.andy.mytest.MyLayout>
public class MyLayout extends ViewGroup {

    private Context mContext;

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

    public MyLayout(Context context, AttributeSet attrs) {
        super(context, attrs, 0);
        this.mContext=context;
    }

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

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //父控件ViewGroupMode和Size
        int widthMode = MeasureSpec. getMode(widthMeasureSpec);
        int heightMode = MeasureSpec. getMode(heightMeasureSpec);
        int widthSize = MeasureSpec. getSize(widthMeasureSpec);
        int heightSize = MeasureSpec. getSize(heightMeasureSpec);
        int layoutWidth = 0;
        int layoutHeight = 0;
        int measureWidth = 0;
        int measureHeight = 0;
        int count = getChildCount();
        //遍历所有子控件并保存了宽和高
        for( int i = 0; i < count; i++){
            View child = getChildAt(i);
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
        }
        MyLayoutParams params = null;
        if(widthMode == MeasureSpec. EXACTLY){ //父控件ViewGroup宽度设置(size/match_parent)
            layoutWidth = widthSize;           //layoutWidth=父控件剩余的宽度
        } else{ //父控件ViewGroup宽度设置(UNSPECIFIED/wrap_content)
            for ( int i = 0; i < count; i++)  { //layoutWidth=遍历子控件后,所有子控件宽度最大的
                View child = getChildAt(i);
                //子控件宽度值
                //不管子控件宽度设置是match_parent或wrap_content;则宽度值为子控件宽度+左右padding值
                //若高度设置是具体高度值;则宽度值为设置的具体宽度值,不包含左右padding值
                measureWidth = child.getMeasuredWidth(); //子控件宽度值(若设置了padding则包含左右padding值)
                params = (MyLayoutParams) child.getLayoutParams();
                //获取子控件宽度和左右边距之和,作为这个控件需要占据的宽度
                int marginWidth = measureWidth + params.leftMargin + params.rightMargin ;
                layoutWidth = marginWidth > layoutWidth ? marginWidth : layoutWidth;
            }
        }
        if(heightMode == MeasureSpec. EXACTLY){ //父控件ViewGroup高度设置(size/match_parent)
            layoutHeight = heightSize;          //layoutHeight=父控件剩余的高度
        } else{ //父控件ViewGroup高度设置(UNSPECIFIED/wrap_content)
            for ( int i = 0; i < count; i++)  {
                View child = getChildAt(i);
                 //子控件高度值
                 //不管子控件高度设置是match_parent或wrap_content;则高度值为子控件高度+上下padding值
                 //若高度设置是具体高度值;高度值为设置的具体高度值,不包含上下padding值
                measureHeight = child.getMeasuredHeight();
                params = (MyLayoutParams) child.getLayoutParams();
                //获取子控件高度和上下边距之和,作为这个控件需要占据的高度
                int marginHeight = measureHeight + params.topMargin + params.bottomMargin ;
                layoutHeight = marginHeight > layoutHeight ? marginHeight : layoutHeight;
            }
        }
        // 测量并保存父控件ViewGroup的宽高
        setMeasuredDimension(layoutWidth, layoutHeight);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        final int count = getChildCount();
        int childMeasureWidth = 0;
        int childMeasureHeight = 0;
        MyLayoutParams params = null;
        for ( int i = 0; i < count; i++) {
            View child = getChildAt(i);
            // 注意此处不能使用getWidth和getHeight,这两个方法必须在onLayout执行完,才能正确获取宽高
            childMeasureWidth = child.getMeasuredWidth();
            childMeasureHeight = child.getMeasuredHeight();
            params = (MyLayoutParams) child.getLayoutParams();
            switch (params. POSITION) {
                case MyLayoutParams. POSITION_CENTER:    // 中间
                    //getWidth()为父布局宽度
                    left = (getWidth()-childMeasureWidth)/2 - params.rightMargin + params.leftMargin ;
                    top = (getHeight()-childMeasureHeight)/2 + params.topMargin - params.bottomMargin ;
                    break;
                case MyLayoutParams. POSITION_LEFT_TOP:      // 左上方
                    left = 0 + params. leftMargin;
                    top = 0 + params. topMargin;
                    break;
                case MyLayoutParams. POSITION_RIGHT_TOP:     // 右上方
                    left = getWidth() - params.rightMargin - childMeasureWidth;
                    top = 0 + params. topMargin;
                    break;
                case MyLayoutParams. POSITION_LEFT_BOTTOM:    // 左下角
                    left = 0 + params. leftMargin;
                    top = getHeight() - params.bottomMargin - childMeasureHeight;
                    break;
                case MyLayoutParams. POSITION_RIGHT_BOTTOM:// 右下角
                    left = getWidth() - params.rightMargin - childMeasureWidth;
                    top = getHeight() - params.bottomMargin -childMeasureHeight;
                    break;
                default:
                    break;
            }

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

    //在布局文件被填充为对象的时候调用
    //如果不重写布局文件中设置的布局参数无法取得
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

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

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

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

}

在这里插入图片描述

  • 4
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值