美文网首页Android自定义View
Android 踩坑系列-ViewGroup的子View真正实现

Android 踩坑系列-ViewGroup的子View真正实现

作者: 琼珶和予 | 来源:发表于2017-12-16 20:57 被阅读10次

      楼主最近在复习自定义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>
    

      模拟器上展示的效果图:



      看上去似乎是没有问题的,我们给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右侧的预览界面来看看此时效果:



      我们发现虽然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真正实现

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