Android进阶 - View 工作原理探究

作者: Goo_Yao | 来源:发表于2017-01-10 14:21 被阅读263次

    前言

    探究分析了View绘制的总体流程:onMeasure、onLayout、onDraw三大方法。

    知识准备

    ViewRoot

    ViewRoot对应ViewRootImpl类,是连接WindowManager与DecorView的纽带。View的三大流程都是通过ViewRoot完成的。ActivityThread中,Activity对象被创建时,会将DecorView添加到Window中,同时创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView对象建立关联。
    代码示例:

    //创建ViewRootImpl对象
    root = new ViewRootImpl(view.getContext(),display);
    //添加关联
    root.setView(view,wparams,panelparentView);
    

    View绘制流程:从ViewRoot的performTraversals开始,经过measure、layout、draw三大流程后才将View绘制出来。
    performTraversals方法,会依次调用performMeasure、performLayout、performDraw方法(这三个方法分别完成顶级View - DecorView 的measure、layout、draw方法)

    其中performMeasure方法会调用measure方法,在measure方法中又会调用onMeasure方法,onMeasure方法则会对所有子元素进行measure,从而达到measure流程从父容器传递到子元素中的目的。接着子元素会重复父容器的measure过程,如此反复完成整个View树的遍历。

    最后,perfromLayout、performDraw的传递过程也是类似的,唯一不同,而performDraw的传递过程在draw方法中通过dispatchDraw来实现。

    DecorView - 继承FrameLayout

    DecorView作为顶级View,一般情况下会包含一个LinearLayout,该LinearLayout分为上下两部分,titlebar部分、android.R.id.content部分(所以,setContentView方法其实就是将布局添加到id为content的FrameLayout中)正如小标题,DecorView本质是FrameLayout,View层时间都必先经过DecorView然后在传递给其中的View。

    MeasureSpec - 很大程度决定View的尺寸规格

    MeasureSpec代表一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize(规格大小)
    小白科普:Java中int为4个字节,Android使用第一个高位字节存储Mode,剩下三个字节存储Size

    MeasureSpec内部实现原理

    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
    
    //下面是三种测量模式
    /**
    * 父控件不对子控件施加任何约束,一般用于系统内部
    */
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;
    /**
    * 父控件已为子控件指定精确大小,对应于LayoutParams中的 match_parenet和具体数值这两种模式
    */
    public static final int EXACTLY     = 1 << MODE_SHIFT;
    /**
    * 子控件可随意大小,但不可大于父控件大小,对应于LayoutParams中的 wrap_content
    */
    public static final int AT_MOST     = 2 << MODE_SHIFT;
    
    /**
    * 根据提供的测量模式以及大小创建 measure specification
    */
    public static int makeMeasureSpec(int size, int mode) {
                if (sUseBrokenMakeMeasureSpec) {
                    return size + mode;
                } else {
                    return (size & ~MODE_MASK) | (mode & MODE_MASK);
                }
            }
            
    public static int getMode(int measureSpec) {
                return (measureSpec & MODE_MASK);
            }
    
    public static int getSize(int measureSpec) {
                return (measureSpec & ~MODE_MASK);
            }
    
    

    小白科普:<< 是移位运算,3<<30表示的是首先把3变成二进制的11然后右边补30个0所组成的一个二进制的数。

    MearsureSpec和LayoutParams对应关系
    系统内部是通过MeasureSpec进行View测量的,但正常情况下,都是使View指定MeasureSpec。View测量时候系统会将LayoutParams在父容器约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽高。因此,Measure需要由LayoutParams和父容器一起决定。

    另外,对于顶级View(DecorView),其MeasureSpec由窗口尺寸和其自身的LayoutParams共同决定。Measure一旦决定后,onMeasure中in个即可获得View的测量宽高。

    /**
    * DecorView中,MeasureSpec产生过程
    * 根据LayoutParams划分,并产生MeasureSpec
    */
    private static int getRootMeasureSpec(int windowSize, int rootDimension) {
            int measureSpec;
            switch (rootDimension) {
    
            case ViewGroup.LayoutParams.MATCH_PARENT:
                // Window can't resize. Force root view to be windowSize.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
                break;
            case ViewGroup.LayoutParams.WRAP_CONTENT:
                // Window can resize. Set max size for root view.
                measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
                break;
            default:
                // Window wants to be an exact size. Force root view to be that size.
                measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
                break;
            }
            return measureSpec;
        }
    

    对于普通View(布局中的View),View的measure方法需要由ViewGroup传递过来
    再看看ViewGroup中的measureChildWithMargins方法

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

    上述方法,对子元素进行measure,调哟in个子元素measure之前会获取子元素的MeasureSpec。显然,子元素MeasureSpec的创建与父容器的MeasureSpec和子元素本身的LayoutParams有关,还与View的margin以及padding有关(具体需要研究ViewGroup的getChildMeasureSpec方法)。

    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;
            }
            return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
        }
    

    上述方法,主要根据父容器的MeasureSpec同时结合View本身的LayoutParams来确定子元素的MeasureSpec。
    另外注意,子元素可用大小为父容器尺寸减去padding。

    View工作流程

    measure 确定View宽高
    layout确定View最终宽高和四个顶点位置
    draw将View绘制到屏幕

    View的生命周期与工作流程

    View生命周期示意图

    View生命周期

    View工作流程示意图

    View工作流程

    探究Measure过程

    两种情况,若只是一个原始的View,通过measure方法就完成了测量过程;如果是ViewGroup,除了完成自身的measure过程,还需要遍历子元素的measure方法,各个子元素递归去执行这个部分。(如上面的示意图所述)

    View的measure方法是一个final类型的方法 - 意味着子类不能重写该方法,因此仔细研究onMeasure方法的实现效果会更好。

    这里贴出View中Measure方法,有部分注释,供有兴趣的读者阅读研究。

    /**
    * View中的Measure方法
    */
    public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
            boolean optical = isLayoutModeOptical(this);
            //先判断当前Mode是不是特例LAYOUT_MODE_OPTICAL_BOUNDS
            if (optical != isLayoutModeOptical(mParent)) {
                Insets insets = getOpticalInsets();
                int oWidth  = insets.left + insets.right;
                int oHeight = insets.top  + insets.bottom;
                widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
                heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
            }
    
            // Suppress sign extension for the low bytes
            long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
            if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
    
            if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
                    widthMeasureSpec != mOldWidthMeasureSpec ||
                    heightMeasureSpec != mOldHeightMeasureSpec) {
    
                // first clears the measured dimension flag
                mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
    
                resolveRtlPropertiesIfNeeded();
    
                int cacheIndex = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ? -1 :
                        mMeasureCache.indexOfKey(key);
                if (cacheIndex < 0 || sIgnoreMeasureCache) {
                    // measure ourselves, this should set the measured dimension flag back
                    onMeasure(widthMeasureSpec, heightMeasureSpec);
                    mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                } else {
                    long value = mMeasureCache.valueAt(cacheIndex);
                    // Casting a long to int drops the high 32 bits, no mask needed
                    setMeasuredDimensionRaw((int) (value >> 32), (int) value);
                    mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
                }
    
                // flag not set, setMeasuredDimension() was not invoked, we raise
                // an exception to warn the developer
                
                //如果自定义View重写了onMeasure方法而没有调用setMeasureDimension()方法,将会在这里抛出异常
                //判断原理:通过解析状态位mPrivateFlags,setMeasureDimension()方法会将mPrivateFlags设置为已计算状态(PFLAG_MEASURED_DIMENSION_SET),只需要检查mPrivateFlags是否含有PFLAG_MEASURED_DIMENSION_SET即可。
                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 |= PFLAG_LAYOUT_REQUIRED;
            }
    
            mOldWidthMeasureSpec = widthMeasureSpec;
            mOldHeightMeasureSpec = heightMeasureSpec;
            
            //计算出的key作为键,量算结果作为值,将该键值对放入成员变量mMeasureCache中,实现本次计算结果的环缓存
            mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                    (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
        }
    

    好啦,接下来,继续研究onMeasure

    /**
    * View中的onMeasure方法
    */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
        }
    

    setMeasuredDimension()方法会设置View宽高测量值,接下来进一步深入,研究getDefaultSize()方法。

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

    getDefaultSize方法直接就是根据测量模式返回measureSpec中的specSize,而这个specSize就是View测量后的大小

    注意:View测量后大小 与 View最终大小 需要区分,是两个东西,因为View最终大小是在layout阶段确定的,但两者几乎所有情况都是相等的

    接下来,再继续探究getDefaultSize方法的第一个参数,从onMeasure方法中可知,该参数来源于下面两个方法

    protected int getSuggestedMinimumWidth() {
            return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
        }
    protected int getSuggestedMinimumHeight() {
            return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    
        }
    

    上述两个方法实现原理都一致,判断有没有背景,如果有,返回两者较大的宽高,没有则返回自己的宽高(android:minwith这个属性指定的值)。

    那么,问题来了,背景最小宽高原理是什么?

    public int getMinimumWidth() {
            final int intrinsicWidth = getIntrinsicWidth();
            return intrinsicWidth > 0 ? intrinsicWidth : 0;
        }
    

    上述代码中可见,Drawable的原始宽度,如果没有原始宽度,则返回0。

    小白科普:ShapeDrawable无原始宽高,而BimapDrawable有原始宽高(即图片尺寸)

    再谈谈ViewGroup的measure过程
    主要区别:

    1. 除了完成自己measure过程还要遍历调用子元素的measure方法,各个子元素再递归执行该过程。
    2. ViewGroup是抽象类,没有重写View的onMeasure方法,而是提供了一个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);
                }
            }
        }
    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);
        }
    

    View在measure过程中会对每一个子元素进行measure。
    再细说下,measureChild方法的思路:

    1. 取出子元素的LayoutParams
    2. 通过getChildMeasureSpec离开创建子元素的MeasureSpec
    3. 将MeasureSpec传递给View的Measure方法进行测量。

    问题:为什么ViewGroup不像View一样对其onMeasure方法做统一实现?
    因为不同的ViewGroup子类会有不同的特性,因此其中的onMeasure细节不相同。

    获取View宽高方法不当,可能会获取错误。
    原因:View的measure过程Activity生命周期方法执行顺序是不确定的,无法保证Activity执行了onCreate、onStart、onReasume时,View测量完毕。

    如果View还没有完成测量,则获取的宽高会是0
    给出四种方法解决:

    1. onWindowFocusChanged - 该方法被调用时候,View已经测量完毕,能够正确获取View宽高。
      注意:该方法会被调用多次,Activity窗口得到焦点与失去焦点时均会被调用一次(继续执行,暂停执行)。
    public void onWindowFocusChanged(boolean hasWindowFocus) {
            super.onWindowFocusChanged(hasWindowFocus);
            if(hasWindowFocus){
            //获取宽高
            int with = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
            }
        }
    
    • view.post(runnable)
      通过post将一个runnable投递到消息队列队尾,等待Looper调用此runnable时,View已初始化完毕。
    protected void onStart(){
        super.onStart();
        view.post(new Runnable(){
            @override
            public void run(){
            //获取宽高
            int with = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
            }
        })
    }
    
    1. ViewTreeObserver
      该类有众多回调接口,其中的OnGlobalLayoutListener接口,当View树状态发生变化或者View树内部的View的可见性发生改变,该方法都会被回调,利用此特性,可获得宽高。
    protected void onStart(){
        super.onStart();
        viewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new OnGlobalListener(){
            public void onGlobalLayout(){
            view.getViewTreeObserver().removeGlobalOnlayoutListener(this);
            //获取宽高
            int with = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
            }
        })
    }
    
    • view.measure(int widthMeasureSpec,int heightMeasureSpec)
      手动对View进行获取,根据View的LayoutParams不同,而采取不同手段。(因不常用,这里就不详细说明)

    探究layout过程

    layout主要作用是ViewGroup用来确定子元素位置(递归)。

     public void layout(int l, int t, int r, int b) {
            if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
                onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            }
            
            //设定四个顶点位置
            int oldL = mLeft;
            int oldT = mTop;
            int oldB = mBottom;
            int oldR = mRight;
    
            boolean changed = isLayoutModeOptical(mParent) ?
                    setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    
            if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
                onLayout(changed, l, t, r, b);
                mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
    
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnLayoutChangeListeners != null) {
                    ArrayList<OnLayoutChangeListener> listenersCopy =
                            (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                    int numListeners = listenersCopy.size();
                    for (int i = 0; i < numListeners; ++i) {
                        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                    }
                }
            }
    
            mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
            mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
        }
    

    layout方法流程:

    1. setFrame方法设定View四个顶点位置
    2. 调用onLayout方法,父容器确定子元素位置
      另外,与onMeasure方法相似,onLayout方法也是各不相同的。

    onLayout方法(LinearLayout、RelativeLayout等基本控件可自行尝试研究下)

    探究draw过程

    draw主要作用是将View绘制到屏幕上
    绘制过程:

    1. 绘制背景(background.draw(canvas))
    2. 绘制自己(onDraw)
    3. 绘制children(dispatchDraw)
    4. 绘制装饰(onDrawScrollBars)
    /**
    * View中的draw方法
    */
    public void draw(Canvas canvas) {
            final int privateFlags = mPrivateFlags;
            final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                    (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
            mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;
    
            /*
             * Draw traversal performs several drawing steps which must be executed
             * in the appropriate order:
             *
             *      1. Draw the background
             *      2. If necessary, save the canvas' layers to prepare for fading
             *      3. Draw view's content
             *      4. Draw children
             *      5. If necessary, draw the fading edges and restore layers
             *      6. Draw decorations (scrollbars for instance)
             */
    
            // Step 1, draw the background, if needed
            int saveCount;
    
            if (!dirtyOpaque) {
                drawBackground(canvas);
            }
    
            // skip step 2 & 5 if possible (common case)
            final int viewFlags = mViewFlags;
            boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
            boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
            if (!verticalEdges && !horizontalEdges) {
                // Step 3, draw the content
                if (!dirtyOpaque) onDraw(canvas);
    
                // Step 4, draw the children
                dispatchDraw(canvas);
    
                // Overlay is part of the content and draws beneath Foreground
                if (mOverlay != null && !mOverlay.isEmpty()) {
                    mOverlay.getOverlayView().dispatchDraw(canvas);
                }
    
                // Step 6, draw decorations (foreground, scrollbars)
                onDrawForeground(canvas);
    
                // we're done...
                return;
            }
    
            /*
             * Here we do the full fledged routine...
             * (this is an uncommon case where speed matters less,
             * this is why we repeat some of the tests that have been
             * done above)
             */
    
            boolean drawTop = false;
            boolean drawBottom = false;
            boolean drawLeft = false;
            boolean drawRight = false;
    
            float topFadeStrength = 0.0f;
            float bottomFadeStrength = 0.0f;
            float leftFadeStrength = 0.0f;
            float rightFadeStrength = 0.0f;
    
            // Step 2, save the canvas' layers
            int paddingLeft = mPaddingLeft;
    
            final boolean offsetRequired = isPaddingOffsetRequired();
            if (offsetRequired) {
                paddingLeft += getLeftPaddingOffset();
            }
    
            int left = mScrollX + paddingLeft;
            int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
            int top = mScrollY + getFadeTop(offsetRequired);
            int bottom = top + getFadeHeight(offsetRequired);
    
            if (offsetRequired) {
                right += getRightPaddingOffset();
                bottom += getBottomPaddingOffset();
            }
    
            final ScrollabilityCache scrollabilityCache = mScrollCache;
            final float fadeHeight = scrollabilityCache.fadingEdgeLength;
            int length = (int) fadeHeight;
    
            // clip the fade length if top and bottom fades overlap
            // overlapping fades produce odd-looking artifacts
            if (verticalEdges && (top + length > bottom - length)) {
                length = (bottom - top) / 2;
            }
    
            // also clip horizontal fades if necessary
            if (horizontalEdges && (left + length > right - length)) {
                length = (right - left) / 2;
            }
    
            if (verticalEdges) {
                topFadeStrength = Math.max(0.0f, Math.min(1.0f, getTopFadingEdgeStrength()));
                drawTop = topFadeStrength * fadeHeight > 1.0f;
                bottomFadeStrength = Math.max(0.0f, Math.min(1.0f, getBottomFadingEdgeStrength()));
                drawBottom = bottomFadeStrength * fadeHeight > 1.0f;
            }
    
            if (horizontalEdges) {
                leftFadeStrength = Math.max(0.0f, Math.min(1.0f, getLeftFadingEdgeStrength()));
                drawLeft = leftFadeStrength * fadeHeight > 1.0f;
                rightFadeStrength = Math.max(0.0f, Math.min(1.0f, getRightFadingEdgeStrength()));
                drawRight = rightFadeStrength * fadeHeight > 1.0f;
            }
    
            saveCount = canvas.getSaveCount();
    
            int solidColor = getSolidColor();
            if (solidColor == 0) {
                final int flags = Canvas.HAS_ALPHA_LAYER_SAVE_FLAG;
    
                if (drawTop) {
                    canvas.saveLayer(left, top, right, top + length, null, flags);
                }
    
                if (drawBottom) {
                    canvas.saveLayer(left, bottom - length, right, bottom, null, flags);
                }
    
                if (drawLeft) {
                    canvas.saveLayer(left, top, left + length, bottom, null, flags);
                }
    
                if (drawRight) {
                    canvas.saveLayer(right - length, top, right, bottom, null, flags);
                }
            } else {
                scrollabilityCache.setFadeColor(solidColor);
            }
    
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);
    
            // Step 4, draw the children
            dispatchDraw(canvas);
    
            // Step 5, draw the fade effect and restore layers
            final Paint p = scrollabilityCache.paint;
            final Matrix matrix = scrollabilityCache.matrix;
            final Shader fade = scrollabilityCache.shader;
    
            if (drawTop) {
                matrix.setScale(1, fadeHeight * topFadeStrength);
                matrix.postTranslate(left, top);
                fade.setLocalMatrix(matrix);
                p.setShader(fade);
                canvas.drawRect(left, top, right, top + length, p);
            }
    
            if (drawBottom) {
                matrix.setScale(1, fadeHeight * bottomFadeStrength);
                matrix.postRotate(180);
                matrix.postTranslate(left, bottom);
                fade.setLocalMatrix(matrix);
                p.setShader(fade);
                canvas.drawRect(left, bottom - length, right, bottom, p);
            }
    
            if (drawLeft) {
                matrix.setScale(1, fadeHeight * leftFadeStrength);
                matrix.postRotate(-90);
                matrix.postTranslate(left, top);
                fade.setLocalMatrix(matrix);
                p.setShader(fade);
                canvas.drawRect(left, top, left + length, bottom, p);
            }
    
            if (drawRight) {
                matrix.setScale(1, fadeHeight * rightFadeStrength);
                matrix.postRotate(90);
                matrix.postTranslate(right, top);
                fade.setLocalMatrix(matrix);
                p.setShader(fade);
                canvas.drawRect(right - length, top, right, bottom, p);
            }
    
            canvas.restoreToCount(saveCount);
    
            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }
    
            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);
        }
    

    View的绘制过程传递是通过dispatchDraw来实现,dispatchDraw会遍历所有子元素的draw方法,另外,View还有一个特殊的方法setWillNotDraw

    public void setWillNotDraw(boolean willNotDraw) {
            setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
        }
    

    setFlags - 该方法可设置优化标记
    如果View不需要绘制任何内容,那么将设置标记为true,系统会相应优化。默认情况下,View不启用这个标记位,但ViewGroup会默认启动该优化标记。
    而实际开发意义:当我们自定义控件继承于ViewGroup并且本身不具备绘制功能,则开启标记。而如果明确知道一个ViewGroup需要通过onDraw来绘制内容时候,则需要显式关闭WILL_NOT_DRAW这个标记位。

    相关文章

      网友评论

      本文标题:Android进阶 - View 工作原理探究

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