你的自定义View是否真的支持Margin

作者: Alien的小窝 | 来源:发表于2017-10-27 10:37 被阅读961次

简书 编程之乐
转载请注明原创出处!

复习自定义View过程中我发现几乎 很多人 都犯了一个细节上的错误,就是ViewGroup中的子View 不支持margin。

注: 关于自定义View的基础教程 请参阅其他博客

先总结两点

  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布局。

关于自定义View和自定义ViewGroup的其他细节就参阅其他文章吧。最好参考Android系统自带控件的 源码,毕竟这是最准确无误的,阅读他们文章加上自己的见解和怀疑,大牛也会有犯错的时候。

相关文章

网友评论

  • 碧海鱼龙:写的很好,学习了!
    Alien的小窝:@最后的大魔王 谢谢,很久没动过笔了
  • doc__wei:这个修改可以跟Linearlayout的源码很像了
  • 琼珶和予:楼主,我感觉你的理解有点问题。因为在xml文件添加子View,先走的是addViewToParent方法,再走addView方法,而在走addViewToParent之前,子View已经通过generateLayoutParams方法获得LayoutParams,所以在走addView方法时,不会调用generateDefaultLayoutParams方法。所以得出结论,如果一个ViewGroup只是在xml文件声明子View的话,只需要重写generateLayoutParams方法就行了,而generateDefaultLayoutParams方法,是为了代码中直接调用addView能够成功的生成LayoutParams
    琼珶和予:@编程之乐 嗯,十分感谢楼主的分享和解答!
    Alien的小窝:你的理解很正确,我就是为了在代码和xml中同时生效所以才重写了那几个方法,如果你确定你只在代码或xml中使用,可以只重写某一个,但不建议!
  • f53bcbbc7c3e:感谢指点:smiley:
  • 69c600613d1b:鸿洋底下看到的0-0
    Alien的小窝:@小菜包子 嗯
    69c600613d1b:@编程之乐 郭霖是很严谨的。开源的库还一直在维护更新!
    69c600613d1b:鸿洋有不少细节上的错误啊--而且他都不改的,github也是,写出来从不维护--

本文标题:你的自定义View是否真的支持Margin

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