Android 重學系列 View的繪制流程(四) onLayout

前言

上一篇文章和大家聊了onMeasure的原理,本文繼續(xù)和大家聊聊onLayout的核心原理。

如果遇到問題歡迎來到本文來討論http://www.itdecent.cn/p/577afa53ce97

正文

onLayout的原理

文件:/frameworks/base/core/java/android/view/ViewRootImpl.java

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout) {
            performLayout(lp, mWidth, mHeight);

            if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) {
                host.getLocationInWindow(mTmpLocation);
                mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1],
                        mTmpLocation[0] + host.mRight - host.mLeft,
                        mTmpLocation[1] + host.mBottom - host.mTop);

                host.gatherTransparentRegion(mTransparentRegion);
                if (mTranslator != null) {
                    mTranslator.translateRegionInWindowToScreen(mTransparentRegion);
                }

                if (!mTransparentRegion.equals(mPreviousTransparentRegion)) {
                    mPreviousTransparentRegion.set(mTransparentRegion);
                    mFullRedrawNeeded = true;
                    try {
                        mWindowSession.setTransparentRegion(mWindow, mTransparentRegion);
                    } catch (RemoteException e) {
                    }
                }
            }

        }

        if (triggerGlobalLayoutListener) {
            mAttachInfo.mRecomputeGlobalAttributes = false;
            mAttachInfo.mTreeObserver.dispatchOnGlobalLayout();
        }
//分發(fā)內(nèi)部的insets,這里我們暫時不去關心省略的邏輯
...
        }

接下來performTraversals后續(xù)事情分為如下幾個方面:

  • 1.就是判斷當前的View是否需要重新擺放位置。如果通過requestLayout執(zhí)行performTraversals方法,則layoutRequested為true;此時需要調(diào)用performLayout進行重新的擺放。

  • 2.判斷到調(diào)用了requestTransparentRegion方法,需要重新計算透明區(qū)域,則會調(diào)用gatherTransparentRegion方法重新計算透明區(qū)域。如果發(fā)現(xiàn)當前的和之前的透明區(qū)域發(fā)生了變化,則通過WindowSession更新WMS那邊的區(qū)域。

這種情況通常是指存在SurfaceView的情況。因為SurfaceView本身就擁有自己的一套體系溝通到SF體系中進行渲染。Android沒有必要把SurfaceView納入到層級中處理,需要把這部分當作透明,當作不必要的層級進行優(yōu)化。

整個核心我還是回頭關注performLayout究竟做了什么?

ViewRootImpl performLayout

文件:/frameworks/base/core/java/android/view/ViewRootImpl.java

    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        mLayoutRequested = false;
        mScrollMayChange = true;
        mInLayout = true;

        final View host = mView;
        if (host == null) {
            return;
        }


        try {
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

            mInLayout = false;
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {

                ArrayList<View> validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters,
                        false);
                if (validLayoutRequesters != null) {

                    mHandlingLayoutInLayoutRequest = true;

                    int numValidRequests = validLayoutRequesters.size();
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        view.requestLayout();
                    }
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

                    mHandlingLayoutInLayoutRequest = false;


                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    if (validLayoutRequesters != null) {
                        final ArrayList<View> finalRequesters = validLayoutRequesters;

                        getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);

                                    view.requestLayout();
                                }
                            }
                        });
                    }
                }

            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }

其實整個核心還是這一段代碼:

            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());

這一段代碼將會開啟遍歷View樹的layout的流程,也就是View的擺放的流程。

當處理完layout流程之后,就會繼續(xù)檢查是否有View在測量,擺放的流程請求
中是否有別的View請求進行刷新,如果請求則把這個View保存在mLayoutRequesters對象中。此時取出重新進行測量和擺放。

記住此時的根布局是DecorView是layout方法.由于DecorView和FrameLayout都有重寫layout,我們來看看ViewGroup的layout.

ViewGroup layout

文件:/frameworks/base/core/java/android/view/ViewGroup.java

    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

能看到,layout走到View的layout方法的條件有2:

  • 1.mSuppressLayout為false,也就是不設置抑制Layout方法
  • 2.mTransition LayoutTransition 布局動畫為空或者沒有改變才可以。

在Android中動畫api中提供了LayoutTransition,用于對子View的加入和移除添加自定義的屬性動畫。有一篇文章寫的挺好的可以看看:LayoutTransition的使用介紹

記住此時從DecorView傳進來的layout四個參數(shù),分別代表該View可以擺放的左部,頂部,右部,底部四個位置。但是不代表該View就是擺放到這個位置

View layout

文件:/frameworks/base/core/java/android/view/View.java

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

            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }

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

        final boolean wasLayoutValid = isLayoutValid();

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

        if (!wasLayoutValid && isFocused()) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            if (canTakeFocus()) {
                clearParentsWantFocus();
            } else if (getViewRootImpl() == null || !getViewRootImpl().isInLayout()) {

                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                clearParentsWantFocus();
            } else if (!hasParentWantsFocus()) {

                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            }

        } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
            mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
            View focused = findFocus();
            if (focused != null) {
                // Try to restore focus as close as possible to our starting focus.
                if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
                    focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                }
            }
        }

        if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
            mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
            notifyEnterOrExitForAutoFillIfNeeded(true);
        }
    }

在這里可以分為如下幾個步驟:

  • 1.判斷PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT是否開啟了。這個標志位打開的時機是在onMeasure步驟發(fā)現(xiàn)原來父容器傳遞下來的大小不變,就會設置老的測量結果在View中。在layout的步驟會先調(diào)用一次onMeasure繼續(xù)遍歷測量底層的子View的大小。

  • 2.判斷isLayoutModeOptical是否開啟了光學邊緣模式。打開了則setOpticalFrame進行四個方向的邊緣設置,否則則setFrame處理。用于判斷是否需要更新四個方向的數(shù)值。

  • 3.如果發(fā)生了大小或者擺放的位置變化,則進行onLayout的回調(diào)。一般子類都會重寫這個方法,進行進一步的擺放設置。

  • 4.如果需要顯示滑動塊,則初始化RoundScrollbarRenderer對象。這個對象實際上就一個封裝好如何繪制繪制一個滑動塊的自定義View。

  • 5.回調(diào)已經(jīng)進行了Layout變化監(jiān)聽的OnLayoutChangeListener回調(diào)。

  • 6.當前View的layout的行為進行的同時沒有另一個layout進行,說明當前的Layout行為是有效的。如果layout的行為是無效的,此時的View又獲取了焦點則清除。如果此時是想要請求焦點,則清空焦點。

  • 7.通知AllFillManager進行相關的處理。

這里我們著重看看setFrame方法做了什么。

setFrame

    protected boolean setFrame(int left, int top, int right, int bottom) {
        boolean changed = false;

        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            changed = true;
            int drawn = mPrivateFlags & PFLAG_DRAWN;

            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);

            invalidate(sizeChanged);

            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

            mPrivateFlags |= PFLAG_HAS_BOUNDS;


            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {

                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);

                invalidateParentCaches();
            }

            mPrivateFlags |= drawn;

            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }

            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }

就以setFrame為例子來看看其核心思想。實際上很簡單:

  • 1.比較左上右下四個方向的數(shù)值是否發(fā)生了變化。如果發(fā)生了變化,則更新四個方向的大小,并判斷整個需要繪制的區(qū)域是否發(fā)生了變化,把sizechange作為參數(shù)調(diào)用invalidate進行onDraw的刷新。

  • 2.獲取mRenderNode這個硬件渲染的對象,并且設置這個渲染點的位置。

  • 3.調(diào)用sizeChange方法進行onSizeChange的回調(diào):

    private void sizeChange(int newWidth, int newHeight, int oldWidth, int oldHeight) {
        onSizeChanged(newWidth, newHeight, oldWidth, oldHeight);
        if (mOverlay != null) {
            mOverlay.getOverlayView().setRight(newWidth);
            mOverlay.getOverlayView().setBottom(newHeight);
        }

        if (!sCanFocusZeroSized && isLayoutValid()
                // Don't touch focus if animating
                && !(mParent instanceof ViewGroup && ((ViewGroup) mParent).isLayoutSuppressed())) {
            if (newWidth <= 0 || newHeight <= 0) {
                if (hasFocus()) {
                    clearFocus();
                    if (mParent instanceof ViewGroup) {
                        ((ViewGroup) mParent).clearFocusedInCluster();
                    }
                }
                clearAccessibilityFocus();
            } else if (oldWidth <= 0 || oldHeight <= 0) {
                if (mParent != null && canTakeFocus()) {
                    mParent.focusableViewAvailable(this);
                }
            }
        }
        rebuildOutline();
    }

如果當前不是ViewGroup且新的寬高小于0焦點則清除焦點,并且通知AccessibilityService。如果新的寬高大于0,則通知父容器焦點可集中。最后重新構建外框。

  • 4.如果判斷到mGhostView不為空,且當前的View可見。則對mGhostView發(fā)出draw的刷新命令。并通知父容器也刷新。這里mGhostView實際上是一層覆蓋層,作用和ViewOverLay相似。

到這里View和ViewGroup的onLayout似乎就看完了。但是還沒有完。記得我們現(xiàn)在分析的是DecorView。因此我們看看DecorView在onLayout中做了什么。

DecorView onLayout

文件:/frameworks/base/core/java/com/android/internal/policy/DecorView.java

    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        getOutsets(mOutsets);
        if (mOutsets.left > 0) {
            offsetLeftAndRight(-mOutsets.left);
        }
        if (mOutsets.top > 0) {
            offsetTopAndBottom(-mOutsets.top);
        }
        if (mApplyFloatingVerticalInsets) {
            offsetTopAndBottom(mFloatingInsets.top);
        }
        if (mApplyFloatingHorizontalInsets) {
            offsetLeftAndRight(mFloatingInsets.left);
        }

        updateElevation();
        mAllowUpdateElevation = true;

        if (changed && mResizeMode == RESIZE_MODE_DOCKED_DIVIDER) {
            getViewRootImpl().requestInvalidateRootRenderNode();
        }
    }
  • 1.先調(diào)用了FrameLayout的onLayout的方法后,確定每一個子View的擺放位置。

  • 2.getOutsets方法獲取mAttachInfo中的mOutSet區(qū)域。從上一篇文章的打印看來,mOutsets的區(qū)域實際上就是指當前屏幕最外層的四個padding大小。如果左右都大于0,則調(diào)用offsetLeftAndRight和offsetTopAndBottom進行設置。

  • 3.如果mApplyFloatingVerticalInsets或者mApplyFloatingHorizontalInsets為true,說明DecorView自己需要處理一次onApplyWindowInsets的回調(diào)。如果關閉FLAG_LAYOUT_IN_SCREEN標志位也就是非全屏模式,且寬或高是WRAP_CONTENT模式,則給橫軸或者縱軸兩端增加systemwindowInset的padding數(shù)值。

    public WindowInsets onApplyWindowInsets(WindowInsets insets) {
        final WindowManager.LayoutParams attrs = mWindow.getAttributes();
        mFloatingInsets.setEmpty();
        if ((attrs.flags & FLAG_LAYOUT_IN_SCREEN) == 0) {

            if (attrs.height == WindowManager.LayoutParams.WRAP_CONTENT) {
                mFloatingInsets.top = insets.getSystemWindowInsetTop();
                mFloatingInsets.bottom = insets.getSystemWindowInsetBottom();
                insets = insets.inset(0, insets.getSystemWindowInsetTop(),
                        0, insets.getSystemWindowInsetBottom());
            }
            if (mWindow.getAttributes().width == WindowManager.LayoutParams.WRAP_CONTENT) {
                mFloatingInsets.left = insets.getSystemWindowInsetTop();
                mFloatingInsets.right = insets.getSystemWindowInsetBottom();
                insets = insets.inset(insets.getSystemWindowInsetLeft(), 0,
                        insets.getSystemWindowInsetRight(), 0);
            }
        }
        mFrameOffsets.set(insets.getSystemWindowInsets());
        insets = updateColorViews(insets, true /* animate */);
        insets = updateStatusGuard(insets);
        if (getForeground() != null) {
            drawableChanged();
        }
        return insets;
    }
  • 4.當所有都Layout好之后,則調(diào)用updateElevation更新窗體的陰影面積。
    // The height of a window which has focus in DIP.
    private final static int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
    // The height of a window which has not in DIP.
    private final static int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;

     private void updateElevation() {
        float elevation = 0;
        final boolean wasAdjustedForStack = mElevationAdjustedForStack;
        final int windowingMode =
                getResources().getConfiguration().windowConfiguration.getWindowingMode();
        if ((windowingMode == WINDOWING_MODE_FREEFORM) && !isResizing()) {
            elevation = hasWindowFocus() ?
                    DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP : DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP;

            if (!mAllowUpdateElevation) {
                elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
            }
            elevation = dipToPx(elevation);
            mElevationAdjustedForStack = true;
        } else if (windowingMode == WINDOWING_MODE_PINNED) {
            elevation = dipToPx(PINNED_WINDOWING_MODE_ELEVATION_IN_DIP);
            mElevationAdjustedForStack = true;
        } else {
            mElevationAdjustedForStack = false;
        }

        if ((wasAdjustedForStack || mElevationAdjustedForStack)
                && getElevation() != elevation) {
            mWindow.setElevation(elevation);
        }
    }

能看到這個過程中,如果窗體模式是freedom模式(也就是更像電腦中可以拖動的窗體一樣)且不是正在拖拽變化大小,則會根據(jù)是否窗體聚焦了來決定陰影的的四個方向的大小。注意如果是沒有焦點則為5,有焦點則為20.這里面的距離并非是measure的時候增加當前測量的大小,而是在測量好的大小中繼續(xù)占用內(nèi)容空間,也就是相當于設置了padding數(shù)值。

如果窗體是WINDOWING_MODE_PINNED模式或者WINDOWING_MODE_FREEFORM模式,且elevation發(fā)生了變化則通過PhoneWindow.setElevation設置Surface的Insets數(shù)值。

  • 5.最后調(diào)用requestInvalidateRootRenderNode,通知ViweRootImpl中的硬件渲染對象ThreadRenderer進行刷新繪制。

整個過程中,有一系列函數(shù)用于更新擺放的偏移量,就以offsetLeftAndRight比較重要,看看是如何計算的。

offsetLeftAndRight

    public void offsetLeftAndRight(int offset) {
        if (offset != 0) {
            final boolean matrixIsIdentity = hasIdentityMatrix();
            if (matrixIsIdentity) {
                if (isHardwareAccelerated()) {
                    invalidateViewProperty(false, false);
                } else {
                    final ViewParent p = mParent;
                    if (p != null && mAttachInfo != null) {
                        final Rect r = mAttachInfo.mTmpInvalRect;
                        int minLeft;
                        int maxRight;
                        if (offset < 0) {
                            minLeft = mLeft + offset;
                            maxRight = mRight;
                        } else {
                            minLeft = mLeft;
                            maxRight = mRight + offset;
                        }
                        r.set(0, 0, maxRight - minLeft, mBottom - mTop);
                        p.invalidateChild(this, r);
                    }
                }
            } else {
                invalidateViewProperty(false, false);
            }

            mLeft += offset;
            mRight += offset;
            mRenderNode.offsetLeftAndRight(offset);
            if (isHardwareAccelerated()) {
                invalidateViewProperty(false, false);
                invalidateParentIfNeededAndWasQuickRejected();
            } else {
                if (!matrixIsIdentity) {
                    invalidateViewProperty(false, true);
                }
                invalidateParentIfNeeded();
            }
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
    }

這個過程會判斷offset如果不等于0才會進行計算。如果從RenderNode判斷到存在單位變化矩陣(關于這個矩陣我們暫時不去聊,涉及到了硬件渲染的機制)。

  • 1.判斷到如果有硬件加速,則直接調(diào)用invalidateViewProperty方法刷新。
  • 2.沒有硬件加速,軟件渲染的邏輯本質上也是一樣的。

在這里能看到如果offset小于0,則把minLeft的大小增加。如果offset大于0,則增加maxRight的數(shù)值。計算出刷新的區(qū)域:

offset>0 maxRight = maxRight + mRight
offset<0 minLeft = minLeft + mLeft
刷新橫向范圍:maxRight - minLeft

計算出需要刷新的區(qū)域通過獲取父布局的invalidateChild發(fā)送刷新命令。換算成圖就是如下原理:


Layout刷新區(qū)域.jpg
  • 3.如果沒有單位變換矩陣,則調(diào)用invalidateViewProperty發(fā)送刷新命令

  • 4.最后mLeft和mRight增加offset。并把left和right同步數(shù)據(jù)到mRenderNode中。

  • 5.如果打開了硬件加速,又一次調(diào)用了invalidateViewProperty,并且調(diào)用invalidateParentIfNeededAndWasQuickRejected拒絕遍歷刷新。

  • 6.關閉硬件加速,如果沒有單位變換矩陣,則調(diào)用invalidateViewProperty。接著調(diào)用invalidateParentIfNeeded。

能看到這個過程中有幾個方法被頻繁的調(diào)用:

  • 1.invalidate
  • 2.invalidateChild
  • 3.invalidateViewProperty
  • 4.invalidateParentIfNeededAndWasQuickRejected
  • 5.invalidateParentIfNeeded

這幾個方法決定了繪制需要更新的區(qū)域。這里我們先不管,我們放到后面來聊聊。我們來繼續(xù)看看DecorView的父類FrameLayout中做了什么

FrameLayout onLayout

文件:/frameworks/base/core/java/android/widget/FrameLayout.java

 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
    }

    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
        final int count = getChildCount();

        final int parentLeft = getPaddingLeftWithForeground();
        final int parentRight = right - left - getPaddingRightWithForeground();

        final int parentTop = getPaddingTopWithForeground();
        final int parentBottom = bottom - top - getPaddingBottomWithForeground();

        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                final LayoutParams lp = (LayoutParams) child.getLayoutParams();

                final int width = child.getMeasuredWidth();
                final int height = child.getMeasuredHeight();

                int childLeft;
                int childTop;

                int gravity = lp.gravity;
                if (gravity == -1) {
                    gravity = DEFAULT_CHILD_GRAVITY;
                }

                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;

                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                        lp.leftMargin - lp.rightMargin;
                        break;
                    case Gravity.RIGHT:
                        if (!forceLeftGravity) {
                            childLeft = parentRight - width - lp.rightMargin;
                            break;
                        }
                    case Gravity.LEFT:
                    default:
                        childLeft = parentLeft + lp.leftMargin;
                }

                switch (verticalGravity) {
                    case Gravity.TOP:
                        childTop = parentTop + lp.topMargin;
                        break;
                    case Gravity.CENTER_VERTICAL:
                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                        lp.topMargin - lp.bottomMargin;
                        break;
                    case Gravity.BOTTOM:
                        childTop = parentBottom - height - lp.bottomMargin;
                        break;
                    default:
                        childTop = parentTop + lp.topMargin;
                }

                child.layout(childLeft, childTop, childLeft + width, childTop + height);
            }
        }
    }

FrameLayout的onLayout方法很簡單。實際上就是遍歷每一個可見的子View處理其gravity。
可以分為橫軸和豎軸兩個方向進行處理:
橫軸的處理方向:

  • 1.是判斷到gravity是CENTER_HORIZONTAL,說明要橫向居中:

每一個孩子左側 = 父View的左側位置 - (父親的寬度 - 孩子的寬度)/ 2 + 孩子的marginLeft - 孩子的marginRight

保證孩子位置的居中。

  • 2.Gravity.RIGHT:

孩子的左側= 父View的右側 - 孩子寬度 - 孩子的marginRight

保證了孩子是從右邊還是擺放位置。

  • 3.Gravity.LEFT

孩子的左側= 父View的左側 + 孩子的marginLeft

豎直方向上同理。

最后把每一個擺放好的孩子位置通過child.layout進行迭代執(zhí)行子View的layout流程。

LinearLayout onLayout

文件:/frameworks/base/core/java/android/widget/LinearLayout.java

我們來看看LinearLayout中做了什么。

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }

我們只看豎直方向上的邏輯。

    void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;

        int childTop;
        int childLeft;

        // Where right end of child should go
        final int width = right - left;
        int childRight = width - mPaddingRight;

        // Space available for child
        int childSpace = width - paddingLeft - mPaddingRight;

        final int count = getVirtualChildCount();

        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;

        switch (majorGravity) {
           case Gravity.BOTTOM:
               childTop = mPaddingTop + bottom - top - mTotalLength;
               break;

           case Gravity.CENTER_VERTICAL:
               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
               break;

           case Gravity.TOP:
           default:
               childTop = mPaddingTop;
               break;
        }

        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
               ...
            } else if (child.getVisibility() != GONE) {
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();

                int gravity = lp.gravity;
                if (gravity < 0) {
                    gravity = minorGravity;
                }
                final int layoutDirection = getLayoutDirection();
                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                    case Gravity.CENTER_HORIZONTAL:
                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                                + lp.leftMargin - lp.rightMargin;
                        break;

                    case Gravity.RIGHT:
                        childLeft = childRight - childWidth - lp.rightMargin;
                        break;

                    case Gravity.LEFT:
                    default:
                        childLeft = paddingLeft + lp.leftMargin;
                        break;
                }

                if (hasDividerBeforeChildAt(i)) {
                    childTop += mDividerHeight;
                }

                childTop += lp.topMargin;
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

                i += getChildrenSkipCount(child, i);
            }
        }
    }

    private void setChildFrame(View child, int left, int top, int width, int height) {
        child.layout(left, top, left + width, top + height);
    }

能看到這里的邏輯和FrameLayout十分相似,也是處理gravity。在這個方法之前已經(jīng)通過在父容器的layout方法測量好LinearLayout的四個位置的基礎上進一步擺放LinearLayout中的子View。

在豎直擺放的邏輯中,分別處理兩個方向的Gravity。
首先看在豎直方向擺放中進行豎直方向上的gravity的處理:

  • 1.Gravity.BOTTOM

每一個孩子的頂部 = paddingTop + LinearLayout的bottom - LinearLayout的top - LinearLayout的總高度

通過從LinearLayout的底部開始向上擺放子View。

  • 2.Gravity.CENTER_VERTICAL

每一個孩子的頂部 = mPaddingTop + (LinearLayout的bottom - LinearLayout的top - mTotalLength) / 2

通過計算LinearLayout居中的位置設置好LinearLayout的子View。

  • 3.Gravity.TOP

每一個孩子的頂部 = mPaddingTop

能看到在豎直方向擺放,處理豎直方向的Gravity只會統(tǒng)一處理所有的子View。而不會進行累加處理。

豎直方向擺放中進行橫向方向上的gravity的處理:

每一個孩子的寬度childSpace = LinearLayout的寬度 - paddingLeft - mPaddingRight

  • 1.Gravity.CENTER_HORIZONTAL:

每一個孩子的左側 = paddingLeft + ((childSpace - childWidth) / 2)
+ lp.leftMargin - lp.rightMargin;

  • 2.Gravity.RIGHT:

每一個孩子的左側 = childRight - childWidth - lp.rightMargin

  • 3.Gravity.LEFT:

每一個孩子的左側 = paddingLeft + lp.leftMargin;

在處理橫向的過程中,不斷的累加每一個子View的topMargin,并且調(diào)用子View的layout方法進行子View的擺放流程,當測定好子View后則累加子View的高度,bottomMargin以及偏移量。

RelativeLayout onLayout

文件:/frameworks/base/core/java/android/widget/RelativeLayout.java

    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        final int count = getChildCount();

        for (int i = 0; i < count; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                RelativeLayout.LayoutParams st =
                        (RelativeLayout.LayoutParams) child.getLayoutParams();
                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
            }
        }
    }

至于RelativeLayout這里面,由于在onMeasure的7次遍歷中已經(jīng)處理好了對應的擺放邏輯了。因此這里指簡單的遍歷一次每一個子View的layout方法即可。

invalidate方法的家族

能看到在onLayout的過程中使用了很多次帶有invalidate含義的方法。

  • 1.invalidate
  • 2.invalidateChild
  • 3.invalidateViewProperty
  • 4.invalidateParentIfNeededAndWasQuickRejected
  • 5.invalidateParentIfNeeded

實際上這是一個告訴Android渲染系統(tǒng),某一段區(qū)域是設置為無效區(qū)也就是發(fā)生了變化的臟區(qū),需要進行重新繪制的意思。我們先來看看invalidate方法。

View invalidate

    public void invalidate() {
        invalidate(true);
    }

    public void invalidate(boolean invalidateCache) {
        invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    }

    private boolean skipInvalidate() {
        return (mViewFlags & VISIBILITY_MASK) != VISIBLE && mCurrentAnimation == null &&
                (!(mParent instanceof ViewGroup) ||
                        !((ViewGroup) mParent).isViewTransitioning(this));
    }

    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
            boolean fullInvalidate) {
        if (mGhostView != null) {
            mGhostView.invalidate(true);
            return;
        }

        if (skipInvalidate()) {
            return;
        }

        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
            if (fullInvalidate) {
                mLastIsOpaque = isOpaque();
                mPrivateFlags &= ~PFLAG_DRAWN;
            }

            mPrivateFlags |= PFLAG_DIRTY;

            if (invalidateCache) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }

            final AttachInfo ai = mAttachInfo;
            final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }

            if (mBackground != null && mBackground.isProjected()) {
                final View receiver = getProjectionReceiver();
                if (receiver != null) {
                    receiver.damageInParent();
                }
            }
        }
    }

    private boolean isProjectionReceiver() {
        return mBackground != null;
    }

    private View getProjectionReceiver() {
        ViewParent p = getParent();
        while (p != null && p instanceof View) {
            final View v = (View) p;
            if (v.isProjectionReceiver()) {
                return v;
            }
            p = p.getParent();
        }

        return null;
    }
  • 1.判斷到mGhostView不為空,則調(diào)用mGhostView的invalidate。

  • 2.如果當前的View不可見,且無動畫,且父容器不是ViewGroup(可能是ViewRootImpl)則跳過該方法的執(zhí)行。

  • 3.如果打開了PFLAG_DRAWN 和 PFLAG_HAS_BOUNDS;或者PFLAG_DRAWING_CACHE_VALID;或者PFLAG_INVALIDATED;或者fullInvalidate為true(此時為true)且透明發(fā)生了變化,則執(zhí)行下面的邏輯:

  • 4.關閉PFLAG_DRAWN標志位,給當前的View打上PFLAG_DIRTY標志位需要重新繪制,如果invalidateCache為true,說明要基于緩存繪制,打開PFLAG_INVALIDATED,關閉PFLAG_DRAWING_CACHE_VALID。

  • 5.給當前的rect設置為當前View的四個邊緣的位置,說明這個位置下所有的View必須重新繪制,通過invalidateChild傳遞給子View進行進一步的處理。

  • 6.如果mBackground背景drawable設置了,則不斷的向頂部容器遍歷找到另一個包含背景drawable的父容器,調(diào)用父容器的damageInParent方法進行兩者的刷新。

    protected void damageInParent() {
        if (mParent != null && mAttachInfo != null) {
            mParent.onDescendantInvalidated(this, this);
        }
    }

ViewGroup invalidateChild 刷新子布局內(nèi)容

傳遞下來的參數(shù)child是當前的View,dirty區(qū)域是上面計算出來的Layout發(fā)生變化臟區(qū)。

    public final void invalidateChild(View child, final Rect dirty) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null && attachInfo.mHardwareAccelerated) {
            onDescendantInvalidated(child, child);
            return;
        }

        ViewParent parent = this;
        if (attachInfo != null) {

            final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;

            Matrix childMatrix = child.getMatrix();
            final boolean isOpaque = child.isOpaque() && !drawAnimation &&
                    child.getAnimation() == null && childMatrix.isIdentity();
            int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;

            if (child.mLayerType != LAYER_TYPE_NONE) {
                mPrivateFlags |= PFLAG_INVALIDATED;
                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
            }

            final int[] location = attachInfo.mInvalidateChildLocation;
            location[CHILD_LEFT_INDEX] = child.mLeft;
            location[CHILD_TOP_INDEX] = child.mTop;
            if (!childMatrix.isIdentity() ||
                    (mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
                RectF boundingRect = attachInfo.mTmpTransformRect;
                boundingRect.set(dirty);
                Matrix transformMatrix;
                if ((mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
                    Transformation t = attachInfo.mTmpTransformation;
                    boolean transformed = getChildStaticTransformation(child, t);
                    if (transformed) {
                        transformMatrix = attachInfo.mTmpMatrix;
                        transformMatrix.set(t.getMatrix());
                        if (!childMatrix.isIdentity()) {
                            transformMatrix.preConcat(childMatrix);
                        }
                    } else {
                        transformMatrix = childMatrix;
                    }
                } else {
                    transformMatrix = childMatrix;
                }
                transformMatrix.mapRect(boundingRect);
                dirty.set((int) Math.floor(boundingRect.left),
                        (int) Math.floor(boundingRect.top),
                        (int) Math.ceil(boundingRect.right),
                        (int) Math.ceil(boundingRect.bottom));
            }

            do {
                View view = null;
                if (parent instanceof View) {
                    view = (View) parent;
                }

                if (drawAnimation) {
                    if (view != null) {
                        view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                    } else if (parent instanceof ViewRootImpl) {
                        ((ViewRootImpl) parent).mIsAnimating = true;
                    }
                }

                if (view != null) {
                    if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
                            view.getSolidColor() == 0) {
                        opaqueFlag = PFLAG_DIRTY;
                    }
                    if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
                        view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
                    }
                }

                parent = parent.invalidateChildInParent(location, dirty);
                if (view != null) {
                    // Account for transform on current parent
                    Matrix m = view.getMatrix();
                    if (!m.isIdentity()) {
                        RectF boundingRect = attachInfo.mTmpTransformRect;
                        boundingRect.set(dirty);
                        m.mapRect(boundingRect);
                        dirty.set((int) Math.floor(boundingRect.left),
                                (int) Math.floor(boundingRect.top),
                                (int) Math.ceil(boundingRect.right),
                                (int) Math.ceil(boundingRect.bottom));
                    }
                }
            } while (parent != null);
        }
    }
  • 1.會判斷當前的是否打開了硬件加速,如果打開了則把事件委托給onDescendantInvalidated完成。

  • 2.如果沒有打開則是軟件渲染。如果發(fā)現(xiàn)是當前的布局的子View不存在單位變換矩陣或者打開了FLAG_SUPPORT_STATIC_TRANSFORMATIONS。則把之前計算出來的臟區(qū)設置到boundingRect,并且遍歷孫子View中的變換矩陣一起通過矩陣計算合并起來。最后通過這個變換矩陣對boundingRect區(qū)域進行一個變換,或變大或縮小。最后把經(jīng)過變換的boundingRect臟區(qū)設置會dirty。

  • 3.判斷當前的ViewParent是否是View(一般是View,也有可能是ViewRootImpl)。判斷當前的View是否正在執(zhí)行動畫,如果執(zhí)行動畫那就要不斷的向父布局遍歷打開PFLAG_DRAW_ANIMATION標志位。如果遍歷到頂層的ViewRootImpl則mIsAnimating為true。

  • 4.如果當前的View對應的父容器的節(jié)點是不透明的或者透明部分發(fā)生了變化,則向頂層父容器遍歷打上PFLAG_DIRTY標志位。

  • 5.調(diào)用invalidateChildInParent根據(jù)當前的臟區(qū)在父容器進行處理和變化。

在這里出現(xiàn)了invalidateChildInParent方法。我們來看看這個方法做了什么。

ViewGroup invalidateChildInParent

    public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
            // either DRAWN, or DRAWING_CACHE_VALID
            if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
                    != FLAG_OPTIMIZE_INVALIDATE) {
                dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                        location[CHILD_TOP_INDEX] - mScrollY);
                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                    dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
                }

                final int left = mLeft;
                final int top = mTop;

                if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                    if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
                        dirty.setEmpty();
                    }
                }

                location[CHILD_LEFT_INDEX] = left;
                location[CHILD_TOP_INDEX] = top;
            } else {

....
            return mParent;
        }

        return null;
    }

首先判斷,是否打開了PFLAG_DRAWN標志位且PFLAG_DRAWING_CACHE_VALID。PFLAG_DRAWING_CACHE_VALID標志位是等到在繪制流程draw中updateDisplayListIfDirty更新臟區(qū)調(diào)用的,說明此時是基于上次一次繪制結果進行繪制。PFLAG_DRAWN標志位是draw方法的時候打開的。

Android在繪制的時候,會默認走基于上一次繪制的結果,進行繪制區(qū)域的設置。

繪制緩存原理如下:
其中FLAG_OPTIMIZE_INVALIDATE的標志位打開時機是在dispatchDraw分發(fā)繪制行為給子View流程中,發(fā)現(xiàn)設置了LayoutAnimation(也就是擺放動畫),并且延時比例mDelay小于1。

這個過程必定出現(xiàn)繪制重疊的區(qū)域,因此需要進行一次優(yōu)化。但是需要注意,校驗FLAG_OPTIMIZE_INVALIDATE標志位的同時需要校驗FLAG_ANIMATION_DONE標志位。這個標志位是動畫執(zhí)行完畢之后,調(diào)用notifyAnimationListener打開了,當然ViewGroup默認初始化也會打開。這樣的校驗就能比較精準的計算出需要更新的繪制區(qū)域。

分為下面三步:

  • 1.加入滑動x軸和y軸的偏移量:

臟區(qū)x軸偏移量:location[CHILD_LEFT_INDEX] - mScrollX
臟區(qū)y軸的偏移量:location[CHILD_TOP_INDEX] - mScrollY

  • 2.如果打開了FLAG_CLIP_CHILDREN,也就是設置clipChild標志位,用來確定子元素是否可以超出父元素的邊界。一般都是true,不允許子元素超過父元素。如果打開了,只需要更新父元素和子元素之間的交集。如果失敗說明無交集,臟區(qū)設置為空。

  • 3.更新location數(shù)組的最左和最頂部的數(shù)值。

記住在invalidateChild執(zhí)行過程中調(diào)用invalidateChildInParent是不斷的向頂部進行遍歷的,因此會遍歷到DecorView以及ViewRootImpl。

ViewRootImpl invalidateChild

    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();

        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }
...

        invalidateRectOnScreen(dirty);

        return null;
    }

實際上核心調(diào)用的是invalidateRectOnScreen。

invalidateRectOnScreen
    private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
            mAttachInfo.mSetIgnoreDirtyState = true;
            mAttachInfo.mIgnoreDirtyState = true;
        }

        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);

        final float appScale = mAttachInfo.mApplicationScale;
        final boolean intersected = localDirty.intersect(0, 0,
                (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
        if (!intersected) {
            localDirty.setEmpty();
        }
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }
    }

能看到這里記錄當前傳遞過來的臟區(qū)記錄到了全局變量mDirty中,直接調(diào)用scheduleTraversals,進行View的繪制流程。那么和requestLayout有什么區(qū)別?我們來看看:

    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

能看到requestLayout會打上mLayoutRequested標志位之后才執(zhí)行scheduleTraversals方法。

我們抽取performTraversals中核心方法看一下:

        final boolean windowRelayoutWasForced = mForceNextWindowRelayout;
        if (mFirst || windowShouldResize || insetsChanged ||
                viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
            mForceNextWindowRelayout = false;
...
        } else {
            maybeHandleWindowMove(frame);
        }

        final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
        boolean triggerGlobalLayoutListener = didLayout
                || mAttachInfo.mRecomputeGlobalAttributes;
        if (didLayout){
....

能看到實際上onMeasure是第一個if中進行處理的,onLayout是在didLayou為true處理的。這幾個流程中,如果不是第一次渲染,且窗體,insets,可見度等因素沒有變化是不會走measure的流程,而layout依賴與requestLayout設置的標志位。

invalidate的方法在正常情況下都不會直接觸發(fā)這兩個流程,只會直接進行這個區(qū)域的重新繪制流程。這也是為什么invalidate更新新添加的View在Android屏幕無法渲染的原因了。

這種方式比較適合如LottieView這種在一個區(qū)域內(nèi)不斷執(zhí)行的動畫的情景。

View invalidateViewProperty

    void invalidateViewProperty(boolean invalidateParent, boolean forceRedraw) {
        if (!isHardwareAccelerated()
                || !mRenderNode.isValid()
                || (mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
            if (invalidateParent) {
                invalidateParentCaches();
            }
            if (forceRedraw) {
                mPrivateFlags |= PFLAG_DRAWN; // force another invalidation with the new orientation
            }
            invalidate(false);
        } else {
            damageInParent();
        }
    }

這個方法本質上比起invalidate更加關注View刷新的性能。注意這個方法從offsetLeftAndRight方法中調(diào)用的。invalidateViewProperty兩個參數(shù)都是false,只有非硬件加速且不是單位矩陣的變換矩陣,第二個才是true。

沒有硬件加速,或者mRenderNode是無效的,就會走invalidate流程。否則則走damageInParent,把事件委托給父容器的onDescendantInvalidated方法中。

先來看看軟件渲染的邏輯:

  • 1.如果參數(shù)invalidateParent是true,則調(diào)用invalidateParentCaches方法渲染父容器的緩存
  • 2.forceRedraw 參數(shù)是true,則PFLAG_DRAWN標志位。這個標志位判斷了draw的行為是否已經(jīng)完成了,然后調(diào)用invalidate方法。不過參數(shù)是false,也就是不基于上一次渲染的結果進行運算,找出前后兩次繪制的交集區(qū)域。

如果是硬件渲染,則交給onDescendantInvalidated方法處理。這個方法出現(xiàn)了多次了,在invalidateChild中也出現(xiàn)了。

那么我們來看看invalidateParentCaches和onDescendantInvalidated都做了什么?

View invalidateParentCaches

    protected void invalidateParentCaches() {
        if (mParent instanceof View) {
            ((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
        }
    }

實際上這個方法就是給父容器打上PFLAG_INVALIDATED標志位。

ViewGroup onDescendantInvalidated

    public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
        mPrivateFlags |= (target.mPrivateFlags & PFLAG_DRAW_ANIMATION);

        if ((target.mPrivateFlags & ~PFLAG_DIRTY_MASK) != 0) {

            mPrivateFlags = (mPrivateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;

            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }

        if (mLayerType == LAYER_TYPE_SOFTWARE) {
            mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
            target = this;
        }

        if (mParent != null) {
            mParent.onDescendantInvalidated(this, target);
        }
    }

能看到實際上在是一個不斷向頂層View遞歸的過程,如果PFLAG_DIRTY_MASK打開了,先關閉PFLAG_DIRTY_MASK,再給頂層的View打上PFLAG_DIRTY。最后關閉PFLAG_DRAWING_CACHE_VALID。

這是什么意思呢?

    static final int PFLAG_DIRTY_MASK                  = 0x00600000;
    static final int PFLAG_DIRTY                       = 0x00200000;
    static final int PFLAG_DIRTY_OPAQUE                = 0x00400000;

能看到PFLAG_DIRTY_MASK控制的兩位高一位是透明,低一位是是否無效。

所以這里的意思是關閉dirty和opaque兩個標志位之后再打開dirty標志位。說明該View區(qū)域發(fā)生了變化,但是不是透明的,可以回調(diào)onDraw方法。

如果mLayerType是LAYER_TYPE_SOFTWARE模式,還需要多打開一個PFLAG_INVALIDATED標志位。

其實這里面的邏輯和invalidateParentCaches有點相似。

ViewRootImpl onDescendantInvalidated
    public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {
        if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {
            mIsAnimating = true;
        }
        invalidate();
    }

    void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            scheduleTraversals();
        }
    }

能看到這里實際上做的事情很簡單,就是把臟區(qū)設置為全局,進行全屏幕的刷新。雖然是全局的刷新臟區(qū),實際上已經(jīng)從底層的View到上層的View都標記了臟區(qū)。

這么做有什么好處呢?這樣就能知道onDraw的繪制需要從頂層開始一直到底層的哪個層級的子View。當然這種模式是專門提供給硬件加速,因為在硬件加速中是一個個RenderNode才是繪制的核心,每一個RenderNode是來自父容器的RenderNode中的DisplayLIst這個View tree中。因此需要從父容器開始向下遍歷找到真正需要重新渲染的對象。

PFLAG_INVALIDATED 又是做什么的呢?下一篇就會和大家揭露,實際上就是告訴硬件渲染對象,從哪個層級開始重新構造View tree。

View invalidateParentIfNeededAndWasQuickRejected

    protected void invalidateParentIfNeededAndWasQuickRejected() {
        if ((mPrivateFlags2 & PFLAG2_VIEW_QUICK_REJECTED) != 0) {
            invalidateParentIfNeeded();
        }
    }

實際上就是判斷PFLAG2_VIEW_QUICK_REJECTED是否開啟,開啟了則走invalidateParentIfNeeded方法。而這個標志位的開啟實際上,幾乎是由Canvas的quickReject方法進行判斷,是否拒絕當前的繪制。如果是則打上PFLAG2_VIEW_QUICK_REJECTED標志,不回調(diào)onDraw直接返回。

而這個方法的調(diào)用時機就是offsetLeftAndTop方法中,完成了offset偏移量的變化后最后調(diào)用的。

再來看看invalidateParentIfNeeded。

View invalidateParentIfNeeded

    protected void invalidateParentIfNeeded() {
        if (isHardwareAccelerated() && mParent instanceof View) {
            ((View) mParent).invalidate(true);
        }
    }

很簡單,實際上就是判斷到是硬件加速之后,調(diào)用invalidate方法進行View的draw刷新。

總結

到這里就完成了onLayout的解析了。

慣例總結,onLayout實質上就是在onMeasure的測量結果的基礎上對每一個子View進行擺放。

onMeasure的執(zhí)行條件

我們從頭開始回顧以下,onMeasure是否進行測量是由如下幾個因素組成的。

  • 1.第一次渲染
  • 2.窗口發(fā)生了變更
  • 3.如果Inset的區(qū)域發(fā)生了變化
  • 4.如果窗體的頂層ViewDecorView可見情況發(fā)生了變化
  • 5.如果mWindowAttributes不為空
  • 6.每一次更新了整個ViewRootImpl的Configuration,如橫豎屏切換,資源主題的切換等。

只要6個前置因素完成之后,才會進行下一步的判斷。

  • 1.原來DecorView的寬高和WindowFrame的寬高發(fā)生了變化
  • 2.觸點模式發(fā)生了變化
    1. contentInsetsChanged 也就是內(nèi)容區(qū)域的inset發(fā)生了變化
  • 4.更新了Configuration

只有這樣才會執(zhí)行onMeasure。

onLayout的執(zhí)行條件

有如下3個條件決定:

  • 1.requestLayout 調(diào)用的。
  • 2.onMeasure 經(jīng)過測量后,說明有View的大小發(fā)生變化因此需要重新進行onLayout的擺放。

前兩點滿足其中一點后,同時滿足View沒有Stop或者調(diào)用需要強制調(diào)用Draw方法,必定會執(zhí)行performLayout。

  • 3.performLayout中如果ViewGroup判斷到關閉擺放抑制,且沒有Layout動畫或者Layout的動畫沒有變化才會傳遞給子View進行執(zhí)行擺放流程。

達到這些情況后才會調(diào)用onLayout方法。

onLayout的優(yōu)化

為了優(yōu)化整個onMeasure和onLayout遍歷的邏輯。onMeasure做了緩存處理,如果判斷到父容器的大小不變,則不會遍歷到底層的子View中進行測量。

然而在這個過程中如果子View發(fā)生了變化呢?所以在onLayout的過程中會提前進行一次onMeasure保證在擺放onLayout之前,每一個子View測量的大小都是正常的。

通過這種類似與狀態(tài)轉移表的方式記錄哪一個層級可以斷開遍歷,極大的降低了整個View的onMeasure和onLayout的遍歷層級。

onLayout執(zhí)行流程

ViewRootImpl將performLayout作為View的擺放流程onLayout的全局入口。這個過程中:

  • 1.首先會通過DecorView的layout方法進行全局的View樹,進行所有的View的擺放處理。
  • 2.檢測是否有在onMeasure和onLayout的過程中進行了需要重新繪制,擺放的請求。如果有就把這些請求對象重新進行measure和layout進行處理。

整個核心流程在View的layout方法中。layout每一次在執(zhí)行onLayout進行每一個子View真正的擺放動作之前:

  • 1.會調(diào)用setFrame或者setOpticalFrame方法確定當前的容器在擺放位置是否因為父容器發(fā)生了變化,一旦發(fā)生了變化則需要調(diào)用子View的onLayout遍歷。

  • 2.或者判斷是否是從調(diào)用requestLayout進來,如果是則會強制進行一次子View的onLayout遍歷。

在這個過程中,setFrame方法還做了另一件十分重要的事情,記錄了該View左邊,右邊,頂部,底部的坐標。同時刷新到View的renderNode中,并且回到用sizeChange方法。如果發(fā)現(xiàn)該View可是則調(diào)用invalidate發(fā)送局部刷新繪制命令,以及調(diào)用invalidateParentCaches更新View的父布局的臟區(qū)的標志位。

DecorView onLayout

DecorView作為根布局當處理完父類的onLayout放啊后,會調(diào)用offsetLeftAndRight等方法進行窗體層級上的擺放偏移處理。其根據(jù)就是在每一個窗體設置的Inset四個方位的大小。

FrameLayout onLayout

FrameLayout在onLayout中只進行了一次所有子View的遍歷循環(huán),處理gravity。以及子View的layout方法

LinearLayout onLayout

LinearLayout在onLayout中只進行了一次所有子View的遍歷循環(huán),處理gravity。以及子View的layout方法

RelativeLayout onLayout

只進行一次遍歷,遍歷每一個子View的layout方法。

invalidate的作用

在View的繪制流程中不需要時時刻刻都進行整個View樹的遍歷onDraw方法進行繪制。實際上onDraw這個過程中才是最為消耗的性能,因此Android就通過invalidate這個命令進行優(yōu)化,以做到局部刷新的能力。

invalidate工作流程如下圖:


invalidate流程.jpg

當上dirty標志位有什么好處呢?好處就是知道需要遍歷到哪一個層級就終止onDraw的調(diào)用,縮減Android繪制時間。

在這個過程中會根據(jù)clipChild的標志位設置父容器和子View之間的刷新區(qū)域關系。如下:


invalidate刷新區(qū)域.jpg

當執(zhí)行完整個整個View tree的onLayout方法之后,將會到DecorView判斷是否執(zhí)行offsetTopAndLeft方法。實際上這個方法也很常用,當我們需要改變一個View的為止的時候,可以調(diào)用這個方法快速改變該View中擺放位置,其原理很除了修改了該View四個邊緣的偏移量,還通過invalidateChild進行局部刷新偏移整個偏移的區(qū)域,這樣就能極大的避免了大量的遍歷循環(huán)處理,原理圖如下:


Layout刷新區(qū)域.jpg

后話

接下來就來看看onDraw中做了什么工作了。

原本打算一周一更的,但是因為有林林種種不可抗力的因素,導致這短時間閑暇的時候無法在電腦邊。估計這段時間更新的速度都會下降。

這段時間經(jīng)歷了一些事情,我直到今時今日才體驗到以前學的那個古詩中令人動容的精神是多么難能可貴。

咬定青山不放松,立根原在破巖中。
千磨萬擊還堅勁,任爾東西南北風。

希望我能保住初心,不管遇到什么困難都能像古詩中說的竹子一樣,咬定青山不放松,還能千磨萬擊還堅勁。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容