Android 扩展-ViewGroup的子View真正实现Margin属性

  楼主最近在复习自定义View,在复习到自定义ViewGroup这个知识点时,发现了一个问题–就是我们之前的定义ViewGroup在考虑Margin属性可能有问题。本文在解决该问题给出建议性的意见,但是不一定是正确的,如果有错误或者不当的地方,希望指正。
  本文参考文章:
  1.Android 手把手教您自定义ViewGroup(一)
  2.你的自定义View是否真的支持Margin

1.提出问题

  这里我举一个简单的例子来说,假设我们需要定义一个ViewGroup放置一个子View,同时这个子View支持Padding和Margin属性。
  这里我先贴出一个常规的写法:

public class CustomViewGroup02 extends ViewGroup {

    public CustomViewGroup02(Context context) {
        super(context);
    }

    public CustomViewGroup02(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        //这里假设只有一个子View
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        View view = getChildAt(0);
        MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
        int width = view.getMeasuredWidth() + lp.leftMargin + lp.rightMargin + getPaddingLeft() + getPaddingRight();
        int height = view.getMeasuredHeight() + lp.topMargin + lp.bottomMargin + getPaddingTop() + getPaddingBottom();

        setMeasuredDimension((widthMode == MeasureSpec.EXACTLY) ? widthSize:width, (heightMode == MeasureSpec.EXACTLY) ? heightSize:height);

    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        View view = getChildAt(0);

        MarginLayoutParams lp = (MarginLayoutParams) view.getLayoutParams();
        int left = getPaddingLeft() + lp.leftMargin;
        int top = getPaddingTop() + lp.topMargin;
        view.layout(left, top, left + view.getMeasuredWidth(), top + view.getMeasuredHeight());

    }

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

}

  在代码中,我们考虑到了padding属性和Margin属性,同时我们可以在xml代码测试一下效果
  xml中这样写:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.apple.android_demo08.CustomViewGroup02
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_marginLeft="20dp"
            android:background="#FFDAB9" />
    </com.example.apple.android_demo08.CustomViewGroup02>
</android.support.constraint.ConstraintLayout>

  模拟器上展示的效果图:

《Android 扩展-ViewGroup的子View真正实现Margin属性》

  看上去似乎是没有问题的,我们给TextView设置了marginLeft为20dp,在手机上也能正常显示出来margin属性。但是,如果TextView的layout_width设置为match_parent会怎么样呢?
  xml代码:

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.example.apple.android_demo08.CustomViewGroup02
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginLeft="20dp"
            android:background="#FFDAB9" />
    </com.example.apple.android_demo08.CustomViewGroup02>
</android.support.constraint.ConstraintLayout>

  此时我们在Android studio右侧的预览界面来看看此时效果:

《Android 扩展-ViewGroup的子View真正实现Margin属性》

  我们发现虽然TextVeiw向左移动了20dp,但是我们发现了一个问题,就是TextView右侧超出了屏幕,也就是说,TextView的layout_marginLeft 属性根本没有影响到它的width,只是单纯将TextView向右移动了20dp。这个是有问题的,我们去看看系统的LinearLayout布局,margin属性会影响View的宽和高的。从而得知,我们这里支持的Margin属性是假的!那怎么才能真正的支持Margin属性呢?

2.解决问题

  要想解决问题,必须先知道问题出现在哪里。这个问题就出现在onMeasure方法中measureChildren方法。
  我们先来看看measureChildren方法的源码:

    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

  这个方法表达的意思非常简单,就是循环测量每个子View。然后我们再来看看measureChild方法:

    protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

  在measureChild方法里面,先利用父布局的XXXXMeasureSpec、padding值和子View向父布局申请的大小来生成子View的宽和高。这里我们就看出问题了,我们发现系统在测量子View的width和height时,只是考虑了padding的影响,没有考虑Margin对View的width和height的影响。
  看到这里,我们明白了,为什么之前我们给TextView设置了marginLeft,同时设置TextView的layout_width为match_parent时,TextView只是单纯的向右移动了,而没有调整TextView的大小。因为我们通过measureChild方法来测量每个子View是不会考虑Margin属性对View的大小的影响。
  知道的问题所在,解决问题就非常的容易。解决的问题的办法就是重写measureChildren方法,在测量每个View时,考虑到margin的影响。其实在ViewGroup还有一个方法那就是measureChildWidthMargins方法,这个方法测量每个View时,考虑到了每个View的margin属性的影响。我们来看看measureChildWidthMargins方法的源代码:

    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

  我们发现在这个方法里面,将Margin属性的影响也考虑到的。那么我们就来重写measureChildren方法:

    @Override
    protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View view = getChildAt(i);
            if (view != null && view.getVisibility() != GONE){
                measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0);
            }
        }
    }

  在这个重写的代码中,我们需要主要两点:

  1.在原来的measureChildren方法的if判断条件是:(child.mViewFlags & VISIBILITY_MASK) != GONE,而我们这里是:view != null && view.getVisibility() != GONE。我们这里的依据是LinearLayout,系统的LinearLayout也重写了measureChildren方法的,它的判断条件就是:view != null && view.getVisibility() != GONE。
  2.measureChildrenWithMargins方法多出两个参数,分别是:widthUsed,heightUsed,这里传入的是两个0,这里的依据还是LinearLayout,LinearLayout调用measureChildrenWithMargins传入就是两个0。

  重写之后,我们来看看之前的match_parent的情况(记得Rebuild一下工程):

《Android 扩展-ViewGroup的子View真正实现Margin属性》

  这下就变得正常得多了!

    原文作者:琼珶和予
    原文地址: https://www.jianshu.com/p/496d5e2d7e27
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞