美文网首页
关于自定义View 自定义ViewGroup

关于自定义View 自定义ViewGroup

作者: 捉影T_T900 | 来源:发表于2021-10-16 23:46 被阅读0次

    场景一:自定义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讲完。

    相关文章

      网友评论

          本文标题:关于自定义View 自定义ViewGroup

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