• 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_widthlayout_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中使用布局参数

  • 相关阅读:
    oracle 查询判断语句
    C#后台调用oracle存储过程,参数传入的是clob字段,怎样处理
    devexpress chart 弧度曲线图
    回车键提交与不提交表单的解决方法
    ajax请求在ie下返回undefined
    [PhpStorm]找回Excluded后的目录
    javasript之toString怪异的情况
    强制页面不缓存
    [争论]localhost与127.0.0.1的区别
    Windows下创建空名文件夹
  • 原文地址:https://www.cnblogs.com/wjtaigwh/p/6605082.html
Copyright © 2020-2023  润新知