Android-自定义ViewGroup-LayoutParams+Margin+Padding相关

不多说,我们一起看官方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来布局。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容