自定义ViewGroup考虑padding,margin

在View.java中:

public final void measure(int widthMeasureSpec,int heightMeasureSpec){

... 

onMeasure();

...

}

protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec) {

        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),

                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));

}


  public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED: //未指定
            result = size;
            break;
        case MeasureSpec.AT_MOST: //至多
        case MeasureSpec.EXACTLY: //精确
            result = specSize;
            break;
        }
        return result;
    }

在ViewGroup中:

   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);
            }
        }
    }

   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);
    }

 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);
    }

说明:

1. measure是final修饰的方法,不可被重写。

    在外部直接调用view.measure(int wSpec, int hSpec)来对view进行自身宽高的测量。

    measure()内部调用onMeasure(), 所以自定义view时,重写onMeasure即可。


2.MeasureSpec 这是一个含mode和size的结合体,不需要我们来具体的关心。

   当在测量时,可以调用MeasureSpec.getSize | getMode() 得到相应的size和mode。

   然后使用MeasureSpec.makeMeasureSpec(size, mode); 来创建MeasureSpec对象。

   mode是根据使用该自定义view时的layout_with|  height参数决定的,不能随便new一个。

   size可以自己指定,也可以直接使用 measureSpec.getSize()。


3.如果是一个View,重写onMeasure时要注意:

  如果在使用自定义view时,用了wrap_content。那么在onMeasure中就要调用setMeasuredDimension,

  来指定view的宽高。如果使用的fill_parent或者一个具体的dp值。那么直接使用super.onMeasure即可。


4.如果是一个ViewGroup,重写onMeasure时要注意:

  首先,结合上面的介绍测量子View的宽高

  然后,结合子view的测量宽高来设置自身的测量宽高

  测量子view的方式有:

        getChildAt(int index).可以拿到index上的子view。 

       通过getChildCount()得到直接子view的数目,再循环遍历出子subView。

       接着,subView.measure(int wSpec, int hSpec); // 使用子view自身的测量方法

    或者调用viewGroup的测量子view的方法:

       // 某一个子view,宽,高, 内部加上了viewGroup的padding值

       measureChild(subView, int wSpec, int hSpec); 

       //所有子view 宽,高, 内部调用了measureChild方法

       measureChildren(int wSpec, int hSpec);

       //某一个子view,宽,高, 内部加上了viewGroup的padding值、margin值和传入的宽高wUsed、hUsed  

       measureChildWithMargins(subView, intwSpec, int wUsed, int hSpec, int hUsed); 


ViewGroup中的子View 不支持margin

 
 

总结两点

  1. 自定义View在onDraw里面需要处理padding的影响,widthMeasureSpec和heightMeasureSpec是包含padding大小的。
  2. 子View的margin属性是由ViewGroup处理的,ViewGroup在onMeasure和onLayout时一定要考虑 ViewGroup自己的padding和子View的margin的影响。

 
 

你可能遇到过下面这样的错误。

java.lang.ClassCastException: android.view.ViewGroup$LayoutParams cannot be cast to android.view.ViewGroup$MarginLayoutParams

下面我们分析为什么会遇到这种错误以及解决方法。

你可能见过很多人在自定义ViewGroup的
onMeasure()中使用
measureChildren(widthMeasureSpec, heightMeasureSpec); 来测量所有子View的尺寸。

ViewGroup.measureChildren的源码如下:

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);
    }
}

measureChild是不是不太合适呢,查阅了FrameLayout和LinearLayout等都没有用过这个measureChildren呢,几乎全部都重写了,我们的自定义ViewGroup的measureChildren是不是应该是改成下面这样才对。

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) {
        // ******************* 注意这里 ********************
        measureChildWithMargins(child, widthMeasureSpec, heightMeasureSpec);
    }
}

你应该看到了区别,measureChild和measureChildWithMargins区别就是
测量child尺寸时,保证child的 最大可用尺寸,感觉这个with前缀起的不太好。

  1. measureChild减去了 ViewGroup的padding 保证child最大可用空间
  2. measureChildWithMargins减去了ViewGroup的padding子View的margin 保证child最大可用空间

至于 measureChild和measureChildWithMargins中是如何**生成child的MeasureSpec,并最终调用child.measure() -- > child.onMeasure()的,这里就不贴源码了。

总结 : ViewGroup中测量child一定要用measureChildWithMargins而不是measureChild

使用measureChildWithMargins后却产生异常
终于改成measureChildWithMargins了,却突然产生了异常,这是为什么?
找到异常产生的位置,追踪到ViewGroup.addView()方法,源码如下:

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        // **************** 注意这里 ****************
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

异常信息是 ClassCastException
cannot be cast to android.view.ViewGroup$MarginLayoutParams
而addView中,如果child.getLayoutParams();获取不到,则默认生成一个
generateDefaultLayoutParams();

protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

这个默认生成的肯定不能强制转换为MarginLayoutParams了。

再来看addView中的其他方法

private void addViewInner(View child, int index, LayoutParams params,boolean preventRequestLayout) {
        if (!checkLayoutParams(params)) {
            // **************** 注意这里 ****************
            params = generateLayoutParams(params);
        }

        if (preventRequestLayout) {
            child.mLayoutParams = params;
        } else {
            child.setLayoutParams(params);
        }

        if (index < 0) {
            index = mChildrenCount;
        }

        addInArray(child, index);
        ................
        ................
}

里面还有检测这个child的LayoutParams 是不是为空的,干脆全部重写得了。

在你的自定义ViewGroup中加入如下代码即可令 子View 的margin生效。

public class MyViewGroup extends ViewGroup {
    // ..................... 其他代码省略 .....................
    
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return new MyLayoutParams(lp);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    public static class MyLayoutParams extends MarginLayoutParams {

        public MyLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public MyLayoutParams(int width, int height) {
            super(width, height);
        }

        public MyLayoutParams(LayoutParams lp) {
            super(lp);
        }
    }
}

另外在ViewGroup.onLayout()时中千万别忘记根据 ViewGroup的padding和子View的margin 灵活给子View布局。



ViewGroupX.java


public class ViewGroupX extends ViewGroup {
    private int mPaddingLeft;
    private int mPaddingTop;
    private int mPaddingRight;
    private int mPaddingBottom;


    public ViewGroupX(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        /**
         * getPaddingLeft()         当前容器的paddingLeft
         * widthMeasureSpec.size    当前容器的宽度(parentWidth - parentPadding - this.margin)
         * view.getMeasuredHeight() (widthMeasureSpec.size - this.padding - viewMargin)
         */
        mPaddingLeft = getPaddingLeft();
        mPaddingTop = getPaddingTop();
        mPaddingRight = getPaddingRight();
        mPaddingBottom = getPaddingBottom();

        int desireWidth = 0, desireHeight = 0;
        int childCount = getChildCount(); // 直接子元素的个数
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            if (view.getVisibility() != GONE) {
                LayoutParams params = (LayoutParams) view.getLayoutParams();
                measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0);
                desireWidth = Math.max(desireWidth, view.getMeasuredWidth());
                desireHeight += view.getMeasuredHeight() + (params.bottomMargin + params.topMargin);
            }
        }
        // count with padding
        desireWidth += mPaddingLeft + mPaddingRight;
        desireHeight += mPaddingTop + mPaddingBottom;

        // see if the size is big enough
        desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
        desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

        setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        /**
         * l this(相对父布局)left
         * r this(相对父布局)right
         */
        int top = mPaddingTop;
        int childCount = getChildCount(); // 直接子元素的个数
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            if (view.getVisibility() != GONE) {
                LayoutParams params = (LayoutParams) view.getLayoutParams();

                int viewLeft = mPaddingLeft + params.leftMargin;
                int viewTop = top + params.topMargin;

                view.layout(viewLeft, viewTop, viewLeft + view.getMeasuredWidth(),  viewTop + view.getMeasuredHeight());
                top += params.topMargin + view.getMeasuredHeight() + params.bottomMargin;
            }
        }
    }

    @Override
    protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
    }

    @Override
    public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    @Override
    protected android.view.ViewGroup.LayoutParams generateLayoutParams(
            android.view.ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    public static class LayoutParams extends MarginLayoutParams {

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(android.view.ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}


LineViewGroup.java


public class LineViewGroup extends ViewGroup {

    private int mPaddingLeft;
    private int mPaddingTop;
    private int mPaddingRight;
    private int mPaddingBottom;

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

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

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int desireWidth = 0, desireHeight = 0;

        mPaddingLeft = getPaddingLeft();
        mPaddingTop = getPaddingTop();
        mPaddingRight = getPaddingRight();
        mPaddingBottom = getPaddingBottom();

        int childCount = getChildCount(); // 直接子元素的个数
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            if (view.getVisibility() != GONE) {
                ViewGroupX.LayoutParams params = (ViewGroupX.LayoutParams) view.getLayoutParams();
                measureChildWithMargins(view, widthMeasureSpec, 0, heightMeasureSpec, 0);
                desireWidth = Math.max(desireWidth, view.getMeasuredWidth());
                desireHeight += view.getMeasuredHeight() + params.bottomMargin + params.topMargin;
            }
        }
        // count with padding
        desireWidth += mPaddingLeft + mPaddingRight;
        desireHeight += mPaddingTop + mPaddingBottom;

        // see if the size is big enough
        desireWidth = Math.max(desireWidth, getSuggestedMinimumWidth());
        desireHeight = Math.max(desireHeight, getSuggestedMinimumHeight());

        setMeasuredDimension(resolveSize(desireWidth, widthMeasureSpec), resolveSize(desireHeight, heightMeasureSpec));
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        /**
         * l this相对父布局的left
         */
        int top = mPaddingTop;
        int childCount = getChildCount(); // 直接子元素的个数
        for (int i = 0; i < childCount; i++) {
            View view = getChildAt(i);
            if (view.getVisibility() != GONE) {
                ViewGroupX.LayoutParams params = (ViewGroupX.LayoutParams) view.getLayoutParams();

                int viewLeft = mPaddingLeft + params.leftMargin;
                int viewTop = top + params.topMargin;
                view.layout(viewLeft, viewTop, viewLeft + view.getMeasuredWidth(), viewTop + view.getMeasuredHeight());

                top += params.topMargin + view.getMeasuredHeight() + params.bottomMargin;
            }
        }
    }

    @Override
    protected android.view.ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new ViewGroupX.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT);
    }

    @Override
    public android.view.ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new ViewGroupX.LayoutParams(getContext(), attrs);
    }

    @Override
    protected android.view.ViewGroup.LayoutParams generateLayoutParams(
            android.view.ViewGroup.LayoutParams p) {
        return new ViewGroupX.LayoutParams(p);
    }
}


activity_main.xml


<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <LinearLayout
        android:orientation="vertical"
        android:padding="@dimen/bottom_padding"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <com.lenovo.ext.views.ViewGroupX
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#123"
            android:layout_margin="@dimen/margin"
            android:padding="@dimen/view_group_padding">

            <LinearLayout
                android:id="@+id/top_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="#234"
                android:layout_margin="@dimen/layout_padding"
                android:orientation="vertical">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/view_group_text"/>
                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="#FFF"
                    android:src="@mipmap/ic_launcher"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/view_group_text"/>
            </LinearLayout>

            <LinearLayout
                android:id="@+id/middle_layout"
                android:background="#749"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:orientation="vertical">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/view_group_middle_text"/>
                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="#000"
                    android:src="@mipmap/ic_launcher_foreground"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/view_group_middle_text"/>
            </LinearLayout>

            <com.lenovo.ext.views.LineViewGroup
                android:layout_margin="@dimen/line_padding"
                android:padding="@dimen/line_padding"
                android:background="#264"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/view_group_bottom_text"/>
                <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="#000"
                    android:src="@mipmap/ic_launcher_round"/>

                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="@string/view_group_bottom_text"/>
            </com.lenovo.ext.views.LineViewGroup>

        </com.lenovo.ext.views.ViewGroupX>
    </LinearLayout>
</ScrollView>


dimens.xml


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <dimen name="bottom_height">120dp</dimen>
    <dimen name="layout_padding">10dp</dimen>
    <dimen name="padding">5dp</dimen>
    <dimen name="margin">5dp</dimen>
    <dimen name="line_padding">7dp</dimen>
    <dimen name="view_group_padding">15dp</dimen>
    <dimen name="bottom_padding">8dp</dimen>
    <dimen name="divider_height">0.5dp</dimen>
</resources>


string.xml


<resources>
    <string name="app_name">DateEngine</string>

    <string name="view_group_text">因此只有hashCode()的低位参加运算,发生不同的hash值,但是得到的index相同的情况的几率会大大增加,这种情况称之为hash碰撞。 即,碰撞率会增大。</string>

    <string name="view_group_middle_text">对于扩容导致需要新建数组存放更多元素时,除了要将老数组中的元素迁移过来,也记得将老数组中的引用置null,以便GC</string>

    <string name="view_group_bottom_text">当手指MotionEvent.ACTION_DOWN,需要确定手指点击的位置是否在两个滑块中的其中一个。而滑块实际上是通过 Canvas.drawRect() 函数绘制的矩形,于是可以将手指触摸的坐标与左右滑块的 top、bottom、left 和 right 进行判断,如果触摸,将 Slider.isTouching 设置为false</string>
</resources>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值