关于自定义View 自定义ViewGroup

2021-10-16  本文已影响0人  捉影T_T900

场景一:自定义View,使用父类的 super.onMeasure

这种场景实际上是使用了 super.onMeasure 先测量一遍,让系统自己先填充 mMeasuredWidth,mMeasuredHeight 成员变量,之后就可以通过
getMeasuredWidth(); getMeasuredHeight(); 直接获取测量之后的宽高值。最后再调用 setMeasuredDimension 重新将计算出来的新的宽高填充 mMeasuredWidth,mMeasuredHeight 成员变量

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int measureWidth = getMeasuredWidth();
        int measureHeight = getMeasuredHeight();
        if (measureWidth > measureHeight) {
            measureWidth = measureHeight;
        } else {
            measureHeight = measureWidth;
        }

        setMeasuredDimension(measureWidth, measureHeight);
    }

场景二:自定义View,【不】使用父类的 super.onMeasure

这种场景需要自行根据View的测量类型,算出真实的宽高,并将结果填充至 mMeasuredWidth,mMeasuredHeight 成员变量。

特别说明:widthMeasureSpec、heightMeasureSpec 是一个32位的数值,前2位指代测量模式,后30位指代大小

xml 中 layout_xxx 的属性就是父布局对子Veiw的属性声明

MeasureSpec.UNSPECIFIED:父布局对子View的大小没有限制,子View想多大都可以
MeasureSpec. AT_MOST:父布局限制了子View的大小上限,子View最大不得超过父布局的上限
MeasureSpec. EXACTLY:父布局指定了子View的大小,子View只能使用这个固定值

实际返回的测量结果,需要根据具体业务进行计算。
最后依然要调用 setMeasuredDimension 把测量出来的结果填充至 mMeasuredWidth,mMeasuredHeight 成员变量。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int userSize = 200;
        int measureWidth = customResolveSize(userSize, widthMeasureSpec);
        int measureHeight = customResolveSize(userSize, heightMeasureSpec);

        setMeasuredDimension(measureWidth, measureHeight);
    }

    private static int customResolveSize(int size, int measureSpec) {
        int measureMode = MeasureSpec.getMode(measureSpec);
        int measureSize = MeasureSpec.getSize(measureSpec);

        int realSize = 0;

        switch (measureMode) {
            case MeasureSpec.UNSPECIFIED: // 父view对子view的大小没有限制,直接返回子view的size
                realSize = size;
                break;
                case MeasureSpec.AT_MOST: // 父view限制了子view的大小上限,子view的大小不得超过父view指定的值
                    if (size >= measureSize) {
                        realSize = measureSize;
                    } else {
                        realSize = size;
                    }
                    break;
                    case MeasureSpec.EXACTLY: // 父view指定了子view的大小,直接返回父view指定的值
                        realSize = measureSize;
                        break;
            default:
                realSize = size;
                break;
        }

        return realSize;
    }

场景三:自定义ViewGroup

自定义ViewGroup比较复杂,难点在测量过程,例子代码,具体返回的测量宽高值,根据实际业务计算。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //触发所有子View的onMeasure函数去测量宽高
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        //MeasureSpec封装了父View传递给子View的布局要求
        int wMode = MeasureSpec.getMode(widthMeasureSpec);
        int wSize = MeasureSpec.getSize(widthMeasureSpec);
        int hMode = MeasureSpec.getMode(heightMeasureSpec);
        int hSize = MeasureSpec.getSize(heightMeasureSpec);

        switch (wMode) {
            case MeasureSpec.EXACTLY:  // 说明这个ViewGroup在父布局中的宽度是一个定值
                mWidth = wSize;
                break;
            case MeasureSpec.AT_MOST:  // 说明这个ViewGroup会尽量填满父布局的宽度,但不能超过父布局的宽度
                mWidth = wSize;
                break;
            case MeasureSpec.UNSPECIFIED:  // 说明这个ViewGroup的宽度不受父布局的宽度约束,有可能会超过父布局的宽度
                break;
        }

        switch (hMode) {
            case MeasureSpec.EXACTLY:  // 说明这个ViewGroup在父布局中的高度是一个定值
                mHeight = hSize;
                break;
            case MeasureSpec.AT_MOST:  // 说明这个ViewGroup会尽量填满父布局的高度,但不能超过父布局的高度
                mHeight = hSize;
                break;
            case MeasureSpec.UNSPECIFIED:  // 说明这个ViewGroup的宽度不受父布局的高度约束,有可能会超过父布局的高度
                break;
        }

        // setMeasuredDimension 的作用是将测量出来的最新宽高值设置到成员变量  mMeasuredWidth,mMeasuredHeight  中,下一阶段
        // onLayout 可以获取到经过测量之后的准确宽高值
        setMeasuredDimension(mWidth, mHeight);
    }

【重点来了】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,过滤掉Gone的子View,并再次调用 measureChild 方法。通过 getChildMeasureSpec 方法算出子View的 MeasureSpec 值,并调用子View的measure方法,进行子View的测量

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

蛋疼的来了,getChildMeasureSpec 做了什么?

/**
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size.
     *
     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

实际就是根据不同的测量模式,算出真实的 mode、size,并调用 MeasureSpec.makeMeasureSpec 生成 MeasureSpec,并返回。
过程很绕,看英文原著吧。实际使用其实用 measureChildren 让系统自己测量就好了,ViewGroup的实际宽高值根据具体情况计算测量值即可。

下一个阶段是 onLayout,根据左、上、右、下的原则,计算子View在ViewGroup的内部位置,之后使用 child. layout(int l, int t, int r, int b) 方法,将子View进行重新定位。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 对子View进行位置布局
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            childView.layout(xx,xx,xx,xx);
        }
    }

场景四:让ViewGroup支持margin

要让自定义ViewGroup支持 layout_margin 属性,需要重写 generateLayoutParams,generateDefaultLayoutParams

    @Override
    protected LayoutParams generateLayoutParams(LayoutParams p) {
        return new MarginLayoutParams(p);
    }

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

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);

            MarginLayoutParams marginLayoutParams = (MarginLayoutParams)childView.getLayoutParams();
            int childWidth =
                    childView.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
            int childHeight =
                    childView.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;

            // ........
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int count = getChildCount();
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);

            MarginLayoutParams marginLayoutParams = (MarginLayoutParams)childView.getLayoutParams();
            int childWidth =
                    childView.getMeasuredWidth() + marginLayoutParams.leftMargin + marginLayoutParams.rightMargin;
            int childHeight =
                    childView.getMeasuredHeight() + marginLayoutParams.topMargin + marginLayoutParams.bottomMargin;

            // .......

        }
    }

为什么要重写 generateLayoutParams,generateDefaultLayoutParams ?

/**
     * Returns a new set of layout parameters based on the supplied attributes set.
     *
     * @param attrs the attributes to build the layout parameters from
     *
     * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
     *         of its descendants
     */
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

    /**
     * Returns a safe set of layout parameters based on the supplied layout params.
     * When a ViewGroup is passed a View whose layout params do not pass the test of
     * {@link #checkLayoutParams(android.view.ViewGroup.LayoutParams)}, this method
     * is invoked. This method should return a new set of layout params suitable for
     * this ViewGroup, possibly by copying the appropriate attributes from the
     * specified set of layout params.
     *
     * @param p The layout parameters to convert into a suitable set of layout parameters
     *          for this ViewGroup.
     *
     * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
     *         of its descendants
     */
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return p;
    }

    /**
     * Returns a set of default layout parameters. These parameters are requested
     * when the View passed to {@link #addView(View)} has no layout parameters
     * already set. If null is returned, an exception is thrown from addView.
     *
     * @return a set of default layout parameters or null
     */
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

系统默认的ViewGroup只返回了LayoutParams对象,只能获取到 layout_width,layout_height 属性,获取不到 margin 的属性

        public LayoutParams(Context c, AttributeSet attrs) {
            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
            setBaseAttributes(a,
                    R.styleable.ViewGroup_Layout_layout_width,
                    R.styleable.ViewGroup_Layout_layout_height);
            a.recycle();
        }

如果想获取 margin 的属性,则需要返回 MarginLayoutParams

        public MarginLayoutParams(Context c, AttributeSet attrs) {
            super();

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
            setBaseAttributes(a,
                    R.styleable.ViewGroup_MarginLayout_layout_width,
                    R.styleable.ViewGroup_MarginLayout_layout_height);

            int margin = a.getDimensionPixelSize(
                    com.android.internal.R.styleable.ViewGroup_MarginLayout_layout_margin, -1);
            .......
        }

由于 MarginLayoutParams 是 LayoutParams 的派生类,所以 (MarginLayoutParams) 强转是合法的,不会报错

自定义View、ViewGroup讲完。

上一篇下一篇

猜你喜欢

热点阅读