美文网首页Android开发
开发者们常谈的自定义View/ViewGroup

开发者们常谈的自定义View/ViewGroup

作者: 丘卡皮 | 来源:发表于2022-06-29 21:15 被阅读0次

    前言

    自定义View和自定义ViewGroup前,如果了解了需要重写方法的一些工作流程,会让自定义工作更加得心应手。

    自定义的时候至少要干的事情

    • 自定义View主要需要重写onMeaure方法和onDraw方法,一个是确定View的大小,另一个是绘制View的内容
    • 自定义ViewGroup主要需要重写onMesure方法和onLayout方法,一个用于确定ViewGroup的大小,另一个确定子View的位置。onDraw方法反而不是必须重写的,甚至不一定会被调用,只有当ViewGroup拥有background或调用setWillNotDraw(false)后才会回调onDraw方法。

    onMeasure的默认实现

    在View的源码中存在onMeasure的默认实现,ViewGroup继承View并且没有重写该方法,使用的还是默认的实现。

    //onMeasure的实现
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
    

    关于参数MeasureSpec详见 MeasureSpec到底是个什么东西

    方法的实现具体涉及额外的四个方法:setMeasuredDimensiongetDefaultSizegetSuggestedMinimumWidthgetSuggestedMinimumHeight,其中getSuggestedMinimumWidthgetSuggestedMinimumHeight差不多,返回了mBackgroundsizeminiSize的其中较大的那一个,所以背景图片能够撑开一个View。

    getDefaultSize

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

    通过源码可以了解到这个方法是根据测量模式来返回指定值,当模式是UNSPECIFIED的时候返回getSuggestedMinimumSize方法传过来的值,当测量模式是AT_MOSTEXACTLY时返回由父View给定的适当尺寸。从这里可以看出在自定义View的时候如果不重写onMeasure方法处理AT_MOST测量模式的话,wrap_content是不会生效的。

    setMeasuredDimension

    它是最后设置View宽高的方法。

    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getOpticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }
    

    这里做了一个光学边界判断isLayoutModeOptical。简单查了一下资料,据说是Android 5.0才加的,具体作用还未详细了解不过出现在这里并不影响后续阅读,不过这个点需要今后留意。之后就调用了setMeasuredDimensionRaw方法。

    setMeasuredDimensionRaw

    这个方法就是将measureSize存储下来,该方法还在measure方法中调用了。

    private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        //这里将mPrivateFlags的第14位设置成1,在measure方法中会用到
        //其实就是将flag标记为已测量。
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }
    

    onMeasure被调用的地方

    在View的源码中,会有两个地方调用,一个是measure方法,另一个是layout方法。这里主要对measure方法逐行解析,而layout方法的内容将放到分析onLayout时再提。

    //注:该部分代码有部分省略
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        ···//与setMeasuredDimension方法中一样的光学边界判断,不过有使用MeasureSpec.adjust对最后值进行调整。
    
        // 将widthMeasureSpec强制转换成long,并且向左位移32位。将heightMeasureSpec强制转换为long后与0xffffffffL做与运算最后通过非运算合并成key,也就是说key的前32位存储widthMeasureSpec,后32位存储heightMeasureSpec
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        //这里出现了mMeasureCache,mMeasureCache会在这里初次赋值,目前还不清楚具体作用,先继续往下看
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        //这里创建了forceLayout临时变量,其中mPrivateFlags在方法setMeasuredDimensionRaw中有看到过,不过此时setMeasuredDimensionRaw还没有被执行因为onMeasure方法还没被回调
        //PFLAG_FORCE_LAYOUT = 0x00001000,结果就是判断mPrivateFlags的第13位是不是1,(也就是说mPrivateFlags的第13位存储着PFLAG_FORCE_LAYOUT的标记)
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        // Optimize layout by avoiding an extra EXACTLY pass when the view is
        // already measured as the correct size. In API 23 and below, this
        // extra pass is required to make LinearLayout re-distribute weight.
        //判断MeasureSpec是否发生变化
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        //当isSpecExactly是true,说明view的大小是明确的
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        //这里应该二次测量的时候处理,因为measure好像会被回调两次
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        //通过上面的那些tag判断是否需要测量
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
    
        if (forceLayout || needsLayout) {
            // first clears the measured dimension flag
            //PFLAG_MEASURED_DIMENSION_SET = 0x00000800,也就是第12位上的1,也就是清除了mPrivateFlags第12位上的标记,设置成0
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            //这个方法是针对小语种左右对调处理,后面再详解里面的代码
            resolveRtlPropertiesIfNeeded();
            //在PFLAG_FORCE_LAYOUT的情况下cacheIndex为-1,否则就从mMeasureCache取值
            //由此可以看出mMeasureCache在前面新建,后面会把widthMeasureSpec和heightMeasureSpec合并的值以及index以key-value的形式存入mMeasureCache
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                //当cacheIndex小于0,也就是有PFLAG_FORCE_LAYOUT的情况下会回调onMeasure方法,当然sIgnoreMeasureCache也会回调,在api<19的情况下就会不使用缓存
                // measure ourselves, this should set the measured dimension flag back
                //这里就回调了view中的onMeasure方法。
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                //清除了mPrivateFlags3第4位上标记,用来表明已经measure过了?PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT = 0x8
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                //获取cache中cacheIndex这个位置的值
                long value = mMeasureCache.valueAt(cacheIndex);
                // Casting a long to int drops the high 32 bits, no mask needed
                //直接从cache中取出然后通过setMeasuredDimensionRaw方法设置测量的宽度和高度了
                //宽度先右移32位然后强转int,高度直接强转就能保留数据,不需要再做额外处理
                setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                //设置了mPrivateFlags3第4位上的标记,标记为1
                mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            // flag not set, setMeasuredDimension() was not invoked, we raise
            // an exception to warn the developer
            //此时判断mPrivateFlags的第14位有没有被设置成1,也就是有没有PFLAG_MEASURED_DIMENSION_SET这个标记,而这个标记代表这setMeasuredDimensionRaw方法有没有被调用过,如果没被调用过则抛出一个异常,通常这个情况发生在自定义View中重写onMeasure的时候没有调用setMeasuredDimension
            if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
                throw new IllegalStateException("View with id " + getId() + ": "
                        + getClass().getName() + "#onMeasure() did not set the"
                        + " measured dimension by calling"
                        + " setMeasuredDimension()");
            }
            //然后将mPrivateFlags的第15位标记成1,也就是说可以layout了
            mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
        }
        //在上面执行完后存储历史MeasureSpec,以及在cache也缓存,可以发现mMeasureCache中key和value是一样的
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
        //将数据加入缓存
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
    

    从上面的代码也可以看出来view的onMeasure方法会被回调多次,具体为什么会被回调多次,那要看measure被回调的时机。

    关于ViewGroup与onMeasure的扩展

    对于ViewGroup来说,它继承了View但没有重写onMeasure方法,但是它提供了三个方法用于遍历测量子View:easureChildrenmeasureChildmeasureChildWithMargins,在自定义ViewGroup的时候有必要的情况下可以重写这几个方法来更细致地处理子View地测量。

    measureChildren

    在方法measureChildren中遍历了ViewGroup的所有子View,只要不是GONE的都会调用measureChild方法来对子View进行测量

        protected void measureChild(View child, int parentWidthMeasureSpec,
                int parentHeightMeasureSpec) {
            final LayoutParams lp = child.getLayoutParams();
            //这里通过了getChildMeasureSpec来确定子View宽和高的MeasureSpec
            final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                    mPaddingLeft + mPaddingRight, lp.width);
            final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                    mPaddingTop + mPaddingBottom, lp.height);
            //最后调用了子View的measure方法,最终又会到子View的onMeasure方法。
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    

    getChildMeasureSpec

        //总共三个参数,分别是父View的MeasureSpec、父View的padding值,以及子View对于的尺寸(这个尺寸会是wrap_content、match_parent、确定的尺寸三种情况)
        public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
            //获取父View的测量模式和配给的尺寸
            int specMode = MeasureSpec.getMode(spec);
            int specSize = MeasureSpec.getSize(spec);
    
            //在配给的尺寸中减去父View的padding值
            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:
                //父View尺寸是明确的
                if (childDimension >= 0) {
                    //这时子View的尺寸也是明确的,因为childDimension>0说明里面存放的是明确的尺寸
                    //然后基于EXACTLY测量模式
                    resultSize = childDimension;
                    resultMode = MeasureSpec.EXACTLY;
                } else if (childDimension == LayoutParams.MATCH_PARENT) {
                    // Child wants to be our size. So be it.
                    //这时子View希望填充父View,所以把父View能分配的尺寸都给出去,并给予EXACTLY测量模式
                    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.
                    //这时候子View只需要能装下自己内容的大小,那么父View就先给它能利用的空间大小,告诉子view不能超过这个大小,并且给予AT_MOST测量模式让子View按需计算大小
                    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
                    //这时子View尺寸是明确的,处理方式同上
                    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.
                    //子View希望填充父View,但是父View自己的大小也没确定,只能先告诉子View它能够分配的大小,并且给予AT_MOST测量模式,这里就要注意子View的宽度哪怕是设置成MATCH_PARENT,测量模式也会是AT_MOST
                    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.
                    //这里的处理情况大致也和上面差不多,依旧是由父View告诉子View能够分配多少空间,以及基于对应的测量模式
                    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
                    //
                    //子View希望填充父View,但父View的测量模式是UNSPECIFIED,UNSPECIFIED一般是例如RecycleView会给子View强制设置的测量模式,也就是说尺寸是没有限制,可以滑动的。
                    //所以这里的size给多少都无所谓,这一块在自定义View中是一定要处理的
                    //在api23以下会给0,之后的版本会把父View能分配的size给出来,这样自定义View的时候哪怕不处理这种测量模式只是用用还是能用的,至少不会出现View不显示的问题。
                    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
            //最后构建MeasureSpec并返回
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    

    measureChildWithMargins相较于measureChild把子View的Margin也计算进去,前提是子View的LayoutParamsMarginLayoutParams,否则在强制类型转换的会抛出异常。

    结语

    在了解以上内容的情况下结合一两个实际应用就能比较好的编写自定义View中的onMeasure方法中的逻辑。

    在自定义View的onMeasure方法中,需要结合内容、测量模式、建议尺寸给定view的最终大小。而在自定义ViewGroup中的onMeasure方法中,需要根据自身的测量模式、建议尺寸去分配子view的这两个参数,并且遍历他们完成他们的测量,在完成子view的测量后再根据他们的大小来最终确定自己的大小。

    在测量之后,View的工作就是进行绘制了,而ViewGroup还需要对子View进行布局,就需要重写onLayout方法。

    推荐

    B站:
    想要彻底理解自定义View/ViewGroup,先搞定这三个步骤
    从自定义View到自定义ViewGroup,你想要的关键操作都在这

    另外,我分享一份从网络上收录整理的 Android学习PDF+架构视频+面试文档等,还有高级进阶架构文档供大家学习进阶,如果你现在有需要的话,可以可以 点击这里直接获取!!!里面记录许多Android 相关学习知识点。

    相关文章

      网友评论

        本文标题:开发者们常谈的自定义View/ViewGroup

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