Android-自定义ViewGroup-LayoutParam

作者: MonkeyLei | 来源:发表于2019-07-18 08:58 被阅读1次

    不多说,我们一起看官方ViewGroup.LayoutParams | Android Developers

    **1. **继承关系走一走,可以干到九十九

    public static class ViewGroup.LayoutParams 
    extends Object 
    
    java.lang.Object
       ↳    android.view.ViewGroup.LayoutParams
    Known direct subclasses
    AbsListView.LayoutParams, AbsoluteLayout.LayoutParams, Gallery.LayoutParams, ViewGroup.MarginLayoutParams, WindowManager.LayoutParams
    Known indirect subclasses
    ActionBar.LayoutParams, ActionMenuView.LayoutParams, FrameLayout.LayoutParams, GridLayout.LayoutParams, LinearLayout.LayoutParams, RadioGroup.LayoutParams, RelativeLayout.LayoutParams, TableLayout.LayoutParams, TableRow.LayoutParams, Toolbar.LayoutParams
    
    

    从继承关系可以知道基本上我们空间的布局属性都是继承它。像ListView, LinearLayout,RelativeLayout,FrameLayout等都直接或者间接继承自ViewGroup.LayoutParams.这样看来有必要了解下这个东东。。。

    **2. **接着来一段说明 - 我觉得挺有用处的

    LayoutParams are used by views to tell their parents how they want to be laid out. See ViewGroup Layout Attributes for a list of all child view attributes that this class supports.
    
    The base LayoutParams class just describes how big the view wants to be for both width and height. For each dimension, it can specify one of:
    
    FILL_PARENT (renamed MATCH_PARENT in API Level 8 and higher), which means that the view wants to be as big as its parent (minus padding)
    WRAP_CONTENT, which means that the view wants to be just big enough to enclose its content (plus padding)
    an exact number
    There are subclasses of LayoutParams for different subclasses of ViewGroup. For example, AbsoluteLayout has its own subclass of LayoutParams which adds an X and Y value.
    

    重点了解其含义:LayoutParams是视图用于告诉他们的父亲孩儿们想怎么样被摆放。请看ViewGroup Layout Attributes 对于自视图属性列表的支持。跳过去看,忒多..

    随便截几张图看看.....

    image image

    其中我们注意ViewGroup_MarginLayout_layout_margin, 这个是由ViewGroup_MarginLayout实现提供...ViewGroup.MarginLayoutParams | Android Developers

    可以看到xml里面的属性:

    image

    然后还有一些的public方法可以调用,其中包括我们常见的setMargins...

    image

    2.1 上面有了一定了解,我们现在来回想下为什么之前ViewGroup.LayoutParams没有这个setMargins函数,或者没有leftMargin等属性获取? 不了解继承关系我们或许会迷糊,了解了后就不需要太迷糊了吧!!!

    而我们之前的LinearLayout.LayoutParams为什么就可以设置setMargins等???

    image

    其实我们跟进去看眼就知道了:

    image

    到这里小白想表达:以前的我们想要动态设置某个布局的一些个属性,比如LinearLayout,RelativeLayout,总是不知道应该获取什么布局属性对象?然后不是很清楚哪个方法设置?主要是我们不了解一些个本质 - android的原生很多ViewGroup都有自己的LayoutParams/同样的,我们自定义ViewGroup也可以定制自己的LayoutParams,进而实现完全自主封装,相信就就是我们自定义View学习的终极目标!!!

    既然说到这个MarginLayoutParams,我们跟进去看看如何就得到了我们xml设置的margin等属性值。

    image image

    看见木有,我们如果统一设置android:layout_margin="10dp",将会获取到这个值并且上下左右都是这个值,nice。。。

    image

    而其他情况,我们看上面就是分别获取left,right等。其中还涉及到了一堆的处理。都不知道是考虑了多少种情况....另外随着约束布局的出现,估计也进行了相应的扩展(猜测一下).

    再看一个setMarins函数实现吧:

      /**
             * Sets the margins, in pixels. A call to {@link android.view.View#requestLayout()} needs
             * to be done so that the new margins are taken into account. Left and right margins may be
             * overriden by {@link android.view.View#requestLayout()} depending on layout direction.
             * Margin values should be positive.
             *
             * @param left the left margin size
             * @param top the top margin size
             * @param right the right margin size
             * @param bottom the bottom margin size
             *
             * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginLeft
             * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginTop
             * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginRight
             * @attr ref android.R.styleable#ViewGroup_MarginLayout_layout_marginBottom
             */
            public void setMargins(int left, int top, int right, int bottom) {
                leftMargin = left;
                topMargin = top;
                rightMargin = right;
                bottomMargin = bottom;
                mMarginFlags &= ~LEFT_MARGIN_UNDEFINED_MASK;
                mMarginFlags &= ~RIGHT_MARGIN_UNDEFINED_MASK;
                if (isMarginRelative()) {
                    mMarginFlags |= NEED_RESOLUTION_MASK;
                } else {
                    mMarginFlags &= ~NEED_RESOLUTION_MASK;
                }
            }
    

    其中涉及到了mMarginFlags, 这个很多地方用到。我们目前暂时就不深入去研究这个margin具体如何实现了。我们先知道这个过程,然后把自定义的过程,简单的原理搞下,等初出茅庐后再看是不是要研究下具体的实现。

    3. 然后小白还有个疑问就是padding呢?

    3.1 padding其实就是内容的边距,所以这个就跟这个绘制有关了哟! So,我们应该在onDraw里面做一些事情...但是,我们之前看官方的CustomLayout的时候,其中getChildMeasureSpec有需要一个padding的参数。那个地方其实也是有个一个padding的需要,也就是控件的宽度+paddingleft+paddingRight, 同理高度+paddingTop + paddingBottom - 前面都搞了,这个不难理解!!

    那padding怎么获取了?

    还记得View类么View | Android Developers

    里面有个方法,还有其他些方法。。。

    image

    So, 我们可以再onDraw里面针对子控件进行处理,先打印看下值有木有?

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int count = getChildCount();
            for (int i = 0; i < count; ++i) {
                ///< 获取子控件的宽高
                View view = getChildAt(i);
                Log.e("test", "getPaddingLeft()=" + view.getPaddingLeft());
                Log.e("test", "getPaddingRight()=" + view.getPaddingRight());
                Log.e("test", "getPaddingTop()=" + view.getPaddingTop());
                Log.e("test", "getPaddingBottom()=" + view.getPaddingBottom());
            }
        }
    

    如果布局进行了设置就可以获取到10dp->26px(1080p)....

    image image

    至于为什么margin是有marginLayoutparmas来进行设置和获取然后属性与控件绑定?而padding则有View/ViewGroup类提供方法获取呢? --- 小白有这个疑问(后面估计得看看整个流程源码啥的,目前感觉可能是复杂度的问题,padding涉及计算貌似要相对简单些....瞎几把猜想下)

    **4. **既然有所了解了,下面我们就可以把子控件的padding处理下。。然后看看ViewGroup的padding如何处理下...也一并处理下...

    **布局修改增加容器padding和第一个子控件padding... **android:padding="10dp"

    <?xml version="1.0" encoding="utf-8"?>
    <me.heyclock.hl.customcopy.CustomViewGroupLastNew xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:background="#ffff00ee"
        android:padding="10dp">
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="10dp"
            android:layout_marginTop="10dp"
            android:background="#ff87addd"
            android:padding="10dp"
            android:text="aaaaaaaa哇咔咔哇咔咔"
            android:textColor="#ff000000"
            android:textSize="22sp" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ffa43dee"
            android:text="sssssss"
            android:textColor="#ff000000"
            android:textSize="12sp" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ffa43dee"
            android:text="wwwwwww"
            android:textColor="#ff000000"
            android:textSize="18sp" />
    
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ffa43dee"
            android:text="大幅度发"
            android:textColor="#ff000000"
            android:textSize="12sp" />
    
    </me.heyclock.hl.customcopy.CustomViewGroupLastNew>
    

    父控件的padding其实我们已经处理了,如下所示(之前我们摆放控件的时候的左上角已经针对设置了padding的容器进行了位移,所以已经有了padding的支持了...)

    image

    但是有个问题就是,我们的容器的宽度和高度没有加入这padding的处理,所以可能导致宽度或者高度不够,导致子控件显示不全,如下:

    image

    那就增加一下宽高吧...但是如果是子控件设置精确尺寸的情况下,没有必要了!而如果是wrap_content的情况则需要处理(也就是内容包裹的情况下宽高需要增加padding尺寸),如下所示:

             ///< wrap_content的模式
            if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(
                        maxWidth + getPaddingLeft() + getPaddingRight(),
                        maxHeight + getPaddingTop() + getPaddingBottom());
            }
            ///< 精确尺寸的模式
            else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
                setMeasuredDimension(wSize, hSize);
            }
            ///< 宽度尺寸不确定,高度确定
            else if (wSpecMode == MeasureSpec.UNSPECIFIED) {
                setMeasuredDimension(maxWidth + getPaddingLeft() + getPaddingRight(), hSize);
            }
            ///< 宽度确定,高度不确定
            else if (hSpecMode == MeasureSpec.UNSPECIFIED) {
                setMeasuredDimension(wSize, maxHeight + getPaddingTop() + getPaddingBottom());
            }
    

    然后我接着处理下子控件的padding...???? 再想想呢?? 需要么? 这个View是你自定义的么? 哈哈~~~是系统的TextView呀....所以不要的呀!.....

    So, 就这样实现就行(仅仅改动了下宽高尺寸就可以兼容padding了):

    package me.heyclock.hl.customcopy;
    
    import android.content.Context;
    import android.content.res.TypedArray;
    import android.graphics.Canvas;
    import android.graphics.Rect;
    import android.util.AttributeSet;
    import android.util.Log;
    import android.view.Gravity;
    import android.view.View;
    import android.view.ViewGroup;
    
    /*
     *@Description: 自定义ViewGroup + 纵向垂直布局 + 单列
     *@Author: hl
     *@Time: 2018/10/25 10:18
     */
    public class CustomViewGroupLastNew extends ViewGroup {
        private Context context;///< 上下文
        /**
         * 计算子控件的布局位置.
         */
        private final Rect mTmpContainerRect = new Rect();
        private final Rect mTmpChildRect = new Rect();
    
        public CustomViewGroupLastNew(Context context) {
            this(context, null);
        }
    
        public CustomViewGroupLastNew(Context context, AttributeSet attrs) {
            this(context, attrs, 0);
        }
    
        public CustomViewGroupLastNew(Context context, AttributeSet attrs, int defStyleAttr) {
            this(context, attrs, 0, 0);
        }
    
        public CustomViewGroupLastNew(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
            super(context, attrs, defStyleAttr, defStyleRes);
            this.context = context;
        }
    
        /**
         * 测量容器的宽高 = 所有子控件的尺寸 + 容器本身的尺寸 -->综合考虑
         *
         * @param widthMeasureSpec
         * @param heightMeasureSpec
         */
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            ///< 定义最大宽度和高度
            int maxWidth = 0;
            int maxHeight = 0;
            ///< 获取子控件的个数
            int count = getChildCount();
            for (int i = 0; i < count; ++i) {
                View view = getChildAt(i);
                ///< 子控件如果是GONE - 不可见也不占据任何位置则不进行测量
                if (view.getVisibility() != GONE) {
                    ///< 获取子控件的属性 - margin、padding
                    CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();
                    ///< 调用子控件测量的方法getChildMeasureSpec(先不考虑margin、padding)
                    ///<  - 内部处理还是比我们自己的麻烦的,后面我们可能要研究和参考
                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, layoutParams.leftMargin + layoutParams.rightMargin, layoutParams.width);
                    final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, layoutParams.topMargin + layoutParams.bottomMargin, layoutParams.height);
                    ///< 然后真正测量下子控件 - 到这一步我们就对子控件进行了宽高的设置了咯
                    view.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    
                    ///< 然后再次获取测量后的子控件的属性
                    layoutParams = (CustomViewGroupLastNew.LayoutParams) view.getLayoutParams();
                    ///< 然后获取宽度的最大值、高度的累加
                    maxWidth = Math.max(maxWidth, view.getMeasuredWidth() + layoutParams.leftMargin + layoutParams.rightMargin);
                    maxHeight += view.getMeasuredHeight() + layoutParams.topMargin + layoutParams.bottomMargin;
                }
            }
    
            ///< 然后再与容器本身的最小宽高对比,取其最大值 - 有一种情况就是带背景图片的容器,要考虑图片尺寸
            maxWidth = Math.max(maxWidth, getMinimumWidth());
            maxHeight = Math.max(maxHeight, getMinimumHeight());
    
            ///< 然后根据容器的模式进行对应的宽高设置 - 参考我们之前的自定义View的测试方式
            int wSpecMode = MeasureSpec.getMode(widthMeasureSpec);
            int wSize = MeasureSpec.getSize(widthMeasureSpec);
            int hSpecMode = MeasureSpec.getMode(heightMeasureSpec);
            int hSize = MeasureSpec.getSize(heightMeasureSpec);
    
            ///< wrap_content的模式
            if (wSpecMode == MeasureSpec.AT_MOST && hSpecMode == MeasureSpec.AT_MOST) {
                setMeasuredDimension(
                        maxWidth + getPaddingLeft() + getPaddingRight(),
                        maxHeight + getPaddingTop() + getPaddingBottom());
            }
            ///< 精确尺寸的模式
            else if (wSpecMode == MeasureSpec.EXACTLY && hSpecMode == MeasureSpec.EXACTLY) {
                setMeasuredDimension(wSize, hSize);
            }
            ///< 宽度尺寸不确定,高度确定
            else if (wSpecMode == MeasureSpec.UNSPECIFIED) {
                setMeasuredDimension(maxWidth + getPaddingLeft() + getPaddingRight(), hSize);
            }
            ///< 宽度确定,高度不确定
            else if (hSpecMode == MeasureSpec.UNSPECIFIED) {
                setMeasuredDimension(wSize, maxHeight + getPaddingTop() + getPaddingBottom());
            }
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            ///< 获取范围初始左上角 - 这个决定子控件绘制的位置,我们绘制理论可以从0,0开始,margin容器本身已经考虑过了...所以别和margin混淆了
            int leftPos = getPaddingLeft();
            int leftTop = getPaddingTop();
            ///< 获取范围初始右下角 - 如果考虑控件的位置,比如靠右,靠下等可能就要利用右下角范围来进行范围计算了...
            ///< 后面我们逐步完善控件的时候用会用到这里...
            //int rightPos = right - left - getPaddingRight();
            //int rightBottom = bottom - top - getPaddingBottom();
    
            ///< 由于我们是垂直布局,并且一律左上角开始绘制的情况下,我们只需要计算出leftPos, leftTop就可以了
            int count = getChildCount();
            for (int i = 0; i < count; ++i){
                View childView = getChildAt(i);
                ///< 控件占位的情况下进行计算
                if (childView.getVisibility() != GONE){
                    ///< 获取子控件的属性 - margin、padding
                    CustomViewGroupLastNew.LayoutParams layoutParams = (CustomViewGroupLastNew.LayoutParams) childView.getLayoutParams();
    
                    int childW = childView.getMeasuredWidth();
                    int childH = childView.getMeasuredHeight();
    
                    ///< 先不管控件的margin哈!
                    int cleft = leftPos + layoutParams.leftMargin;
                    int cright = cleft + childW;
                    int ctop = leftTop + layoutParams.topMargin;
                    int cbottom = ctop + childH;
    
                    ///< 下一个控件的左上角需要向y轴移动上一个控件的高度 - 不然都重叠了!
                    leftTop += childH + layoutParams.topMargin + layoutParams.bottomMargin;
    
                    ///< 需要一个范围,然后进行摆放
                    childView.layout(cleft, ctop, cright, cbottom);
                }
            }
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            int count = getChildCount();
            for (int i = 0; i < count; ++i) {
                ///< 获取子控件的宽高
                View view = getChildAt(i);
                Log.e("test", "getPaddingLeft()=" + view.getPaddingLeft());
                Log.e("test", "getPaddingRight()=" + view.getPaddingRight());
                Log.e("test", "getPaddingTop()=" + view.getPaddingTop());
                Log.e("test", "getPaddingBottom()=" + view.getPaddingBottom());
            }
        }
    
        @Override
        public CustomViewGroupLastNew.LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new CustomViewGroupLastNew.LayoutParams(getContext(), attrs);
        }
    
        /**
         * 这个是布局相关的属性,最终继承的是ViewGroup.LayoutParams,所以上面我们可以直接进行转换
         * --目的是获取自定义属性以及一些使用常量的自定义
         */
        public static class LayoutParams extends MarginLayoutParams {
            public LayoutParams(Context c, AttributeSet attrs) {
                super(c, attrs);
    
                // Pull the layout param values from the layout XML during
                // inflation.  This is not needed if you don't care about
                // changing the layout behavior in XML.
                TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.CustomLayoutLP);
                ///< TODO 一些属性的自定义
                a.recycle();
            }
    
            public LayoutParams(int width, int height) {
                super(width, height);
            }
    
            public LayoutParams(ViewGroup.LayoutParams source) {
                super(source);
            }
        }
    }
    
    image

    再试试第三个控件也加上:

      <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:background="#ffa43dee"
            android:padding="100dp"
            android:text="wwwwwww"
            android:textColor="#ff000000"
            android:textSize="18sp" />
    
    image

    看样子没什么问题---不一定是你想要的自定义哈! 我们如果要做一个完美的自定义控件肯定还要做更多的处理,看看官方的就大概能想到了。。

    所以到这里我们就暂时性的针对margin呀padding呀做了出入分析。。。下面我们接着完善下这个位置,比如增加靠左,靠右的自定义属性和处理,然后再完善下父容器match_parent或者固定尺寸的情况....然后布局和定位的部分暂时结束。然后我们需要去分析事件传递处理部分,这样初步的自定义的流程才算是走过一遍。。。

    加油,么么哒....

    最后借鉴网友的总结:

    总结一下:
    
    1\. 在自定义View中处理padding,只需要在onDraw()中处理,别忘记处理布局为wrap_content的情况。
    2\. 在自定义ViewGroup中处理padding,只需要在onLayout()中,给子View布局时算上padding的值即可,
       也别忘记处理布局为wrap_content的情况。
    3\. 自定义View无需处理margin,在自定义ViewGroup中处理margin时,
       需要在onMeasure()中根据margin计算ViewGroup的宽、高,
       同时在onLayout中布局子View时也别忘记根据margin来布局。
    

    相关文章

      网友评论

        本文标题:Android-自定义ViewGroup-LayoutParam

        本文链接:https://www.haomeiwen.com/subject/xjcklctx.html