Android進階知識:繪製流程(中)

一、前言

Android進階知識:繪製流程(上)中主要是關於繪製流程中會遇到的基礎知識,這一篇開始來看具體View繪製的流程。前篇中講過View分爲ViewGroup和通常的ViewViewGroup中能夠包含其餘ViewViewGroup,而且ViewGroup繼承了View,因此繪製流程中ViewGroup相比通常View除了要繪製自身還要繪製其子ViewView的繪製流程分爲三個階段:測量measure、佈局layout、繪製draw,接下來就開始對ViewViewGroup繪製流程的這三個階段一個個開始研究。android

二、View的繪製流程

2.1 View的measure流程

View的測量流程從Viewmeasure方法開始,因此先來看它的measure方法。canvas

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
        //optical用來判斷是否使用了光學邊界佈局的ViewGroup
        boolean optical = isLayoutModeOptical(this);
        //判斷當前View的mParent是否與本身同樣使用了光學邊界佈局的ViewGroup
        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);
        }
        //生成一個key做爲測量結果緩存的key
        // Suppress sign extension for the low bytes
        long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
        //緩存爲空就new一個新的LongSparseLongArray
        if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
        //是否強制layout
        final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
        //上次測量和此次結果是否相同
        final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
                || heightMeasureSpec != mOldHeightMeasureSpec;
        //寬高測量模式是否皆爲EXACTLY模式
        final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
                && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
        //MeasureSpec中的size測量大小是否相等
        final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
                && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
        //是否須要layout
        final boolean needsLayout = specChanged
                && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);
        //判斷是否須要layout或者強制layout
        if (forceLayout || needsLayout) {
            //清除已測量的flag
            mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
            resolveRtlPropertiesIfNeeded();
            //測量以前還有先判斷緩存若是是強制layout則爲-1,不然去緩存中根據以前生成的key找對應的value
            int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
            //若是cacheIndex小於0即強制layout或者忽略緩存
            if (cacheIndex < 0 || sIgnoreMeasureCache) {
                // measure ourselves, this should set the measured dimension flag back
                //就調用onMeasure方法測量寬高
                onMeasure(widthMeasureSpec, heightMeasureSpec);
                //onMeasure測量結束後再修改狀態flag
                mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
            } else {
                //不然就用緩存中的值
                long value = mMeasureCache.valueAt(cacheIndex);
                //經過setMeasuredDimensionRaw方法將緩存值賦給成員變量
                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
            //這裏對複寫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 |= PFLAG_LAYOUT_REQUIRED;
        }
        //更新mOldWidthMeasureSpec,mOldHeightMeasureSpec
        mOldWidthMeasureSpec = widthMeasureSpec;
        mOldHeightMeasureSpec = heightMeasureSpec;
        //更新緩存
        mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
                (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
    }
複製代碼

註釋已經很詳細了,這裏再梳理總結一下measure方法作了哪些事。其實measure方法主要功能就是調用onMeasure方法測量大小,可是不是每次都調用,這裏作了優化。先要判斷是不是須要layout或者是強制layout。判斷經過以後還要再判斷是不是強制測量或者忽略緩存,若是強制測量或者忽略緩存纔會去調用onMeasure方法區測量,不然直接用緩存中上一次測量的緩存就行。數組

接下來看onMeasure測量方法:緩存

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }
複製代碼

onMeasure方法裏就調用了setMeasuredDimension方法。安全

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);
    }
複製代碼

setMeasuredDimension方法裏又調用了setMeasuredDimensionRaw方法,這裏就和以前從緩存中取數據調用方法是同樣的了,這裏將測量好的寬高賦給成員變量。回頭再看onMeasure方法裏的setMeasuredDimension方法傳參,這裏傳參都先調用了getDefaultSize方法,而getDefaultSize方法又調用了getSuggestedMinimumWidthgetSuggestedMinimumHeight方法。一個一個來看,這裏WidthHeight方法是相似的,因此就看getSuggestedMinimumWidth方法便可。bash

protected int getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
    }
複製代碼

這個方法裏判斷了View是否有mBackground,沒有背景就返回View的最小寬度,有背景就從View的最小寬度和背景的最小寬度中選較大的返回。這裏的最小寬度能夠在xml裏經過相關屬性設置或者調用setMinimumWidth方法設置。app

<View
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:minWidth="100dp"
        android:minHeight="200dp"
       />
複製代碼
public void setMinimumWidth(int minWidth) {
        mMinWidth = minWidth;
        requestLayout();
    }
複製代碼

接下來看getDefaultSize方法:ide

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;
    }
複製代碼

這裏傳遞的參數size就是getSuggestedMinimumWidth方法得到View自身想要的大小,measureSpecView的父佈局的MeasureSpec信息。而後根據父佈局MeasureSpec信息中的specModespecSize來返回測量結果result。父佈局specModeUNSPECIFIED即不限制大小因此result就爲View想要的大小sizespecModeAT_MOST或者EXACTLY,則都返回父佈局的specSize大小.佈局

從這裏就能夠看出View中默認AT_MOST或者EXACTLY模式返回測量大小同樣的,因此在自定義View的時,單單隻繼承了View以後,默認寬高設置對應的MATCH_PARENTWRAP_CONTENT屬性返回的結果也是同樣的,即默認的ViewWRAP_CONTENT是無效的,須要咱們本身複寫onMeasure方法設置AT_MOST模式下的最小測量值。至此Viewmeasure流程就結束了,下面用一張流程圖來梳理下View的測量流程。post

View的measure流程

2.2 View的layout流程

接下來看View的佈局流程,從layout方法開始。

public void layout(int l, int t, int r, int b) {
        //先判斷在layout以前是否須要先調用onMeasure測量
        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;
        //這裏仍是根據isLayoutModeOptical方法判斷mParent是否使用光學邊界佈局,分別調用setOpticalFrame和setFrame方法
        //setOpticalFrame方法中計算Insets後一樣調用了setFrame方法
        //最終返回一個布爾值changed表示佈局是否發生變化
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //若是發生了改變或者須要進行layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //就調用onLayout方法
            onLayout(changed, l, t, r, b);
            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            //layout以後調用onLayoutChange方法
            //獲取ListenerInfo 
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                //獲取ListenerInfo不爲空且mOnLayoutChangeListeners就克隆一份OnLayoutChangeListener集合
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                //循環調用OnLayoutChangeListener的onLayoutChange方法
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        final boolean wasLayoutValid = isLayoutValid();
        //修改強制layout的flag
        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()) {
                //ViewRootImpl爲空或者ViewRootImpl不在layout
                //這裏是一種奇怪的狀況,多是用戶而不是ViewRootImpl調用layout方法,最安全的作法在這裏仍是清除焦點
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
                clearParentsWantFocus();
            } else if (!hasParentWantsFocus()) {
                // original requestFocus was likely on this view directly, so just clear focus
                clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
            }
            // otherwise, we let parents handle re-assigning focus during their layout passes.
        } 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);
        }
    }
複製代碼

總結一下layout方法中最主要的就是調用setFrame方法返回一個佈局是否發生變化的結果,判斷髮生了變化就會調用onLayout方法從新計算佈局位置,其它還有一些特殊狀況的處理、監聽的調用和焦點的處理。下面就來看setFrame方法。

protected boolean setFrame(int left, int top, int right, int bottom) {
        //默認change爲false
        boolean changed = false;
        if (DBG) {
            Log.d(VIEW_LOG_TAG, this + " View.setFrame(" + left + "," + top + ","
                    + right + "," + bottom + ")");
        }
        //比較新舊left、top、right、bottom
        if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
            //只要有一個不相等就說明有改變
            changed = true;

            // Remember our drawn bit
            int drawn = mPrivateFlags & PFLAG_DRAWN;
            //分別計算舊的寬高和新的寬高
            int oldWidth = mRight - mLeft;
            int oldHeight = mBottom - mTop;
            int newWidth = right - left;
            int newHeight = bottom - top;
            //新舊有不相等sizeChanged就爲true
            boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
            // Invalidate our old position
            invalidate(sizeChanged);
            //更新新的left、top、right、bottom的值
            mLeft = left;
            mTop = top;
            mRight = right;
            mBottom = bottom;
            //設置更新視圖顯示列表
            mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
            mPrivateFlags |= PFLAG_HAS_BOUNDS;
            //若是發生了改變調用sizeChange方法
            if (sizeChanged) {
                sizeChange(newWidth, newHeight, oldWidth, oldHeight);
            }

            if ((mViewFlags & VISIBILITY_MASK) == VISIBLE || mGhostView != null) {
                //若是是可見的,就強制加上PFLAG_DRAWN狀態位
                //防止在調用setFrame方法前PFLAG_DRAWN狀態爲被清除
                //保證invalidate方法執行
                mPrivateFlags |= PFLAG_DRAWN;
                invalidate(sizeChanged);
                // parent display list may need to be recreated based on a change in the bounds
                // of any child
                invalidateParentCaches();
            }
            // Reset drawn bit to original value (invalidate turns it off)
            mPrivateFlags |= drawn;
            mBackgroundSizeChanged = true;
            mDefaultFocusHighlightSizeChanged = true;
            if (mForegroundInfo != null) {
                mForegroundInfo.mBoundsChanged = true;
            }
            notifySubtreeAccessibilityStateChangedIfNeeded();
        }
        return changed;
    }
複製代碼

setFrame方法中會比較新舊mLeftmTopmRightmBottom,這四個值就是第一篇文章中提到的View的四個get方法獲取的值,表示本身相對於父佈局的位置。只要有一個不一樣就說明發生了變化,而後調用mRenderNode.setLeftTopRightBottom方法設置更新視圖顯示列表,返回change結果爲true。接下來進入查看onLayout方法。

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  }
複製代碼

ViewonLayout方法是一個空方法。由於單個普通View是沒有子View的,他自身的位置計算在layout方法中已經計算好了。若是是ViewGroup則必須要複寫onLayout方法,根據自身要求設置子View的位置。

View的layout流程

2.3 View的draw流程

一樣先從Viewdraw方法開始閱讀。

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遍歷執行的幾個步驟:
         *
         *      1. 繪製背景
         *      2. 若是有必要,保存Canvas圖層
         *      3. 繪製內容
         *      4. 繪製子元素
         *      5. 若是有必要,繪製邊緣和恢復圖層
         *      6. 繪製裝飾 (例如滾動條scrollbars)
         */
        // Step 1, draw the background, if needed
        int saveCount;
        if (!dirtyOpaque) {
            drawBackground(canvas);
        }
        ......
        // Step 2, save the canvas's layers int paddingLeft = mPaddingLeft; final boolean offsetRequired = isPaddingOffsetRequired(); if (offsetRequired) { paddingLeft += getLeftPaddingOffset(); } ...... // 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; ...... // Step 6, draw decorations (foreground, scrollbars) onDrawForeground(canvas); } 複製代碼

View中的draw方法註釋和邏輯就比較清晰了,一共主要分爲六個步驟繪製:

  1. 繪製背景
  2. 若是有必要,保存Canvas圖層
  3. 繪製內容
  4. 繪製子元素
  5. 若是有必要,繪製邊緣和恢復圖層
  6. 繪製裝飾 (例如滾動條scrollbars)

首先是drawBackground方法:

private void drawBackground(Canvas canvas) {
        final Drawable background = mBackground;
        if (background == null) {
            return;
        }
        //設置背景邊界
        setBackgroundBounds();
        ......
        final int scrollX = mScrollX;
        final int scrollY = mScrollY;
        if ((scrollX | scrollY) == 0) {
            //直接繪製背景
            background.draw(canvas);
        } else {
            //若是 mScrollX 和 mScrollY 有值 則將畫布canvas平移
            canvas.translate(scrollX, scrollY);
            //以後在調用draw方法繪製背景
            background.draw(canvas);
            canvas.translate(-scrollX, -scrollY);
        }
    }
複製代碼

drawBackground方法中主要是作了兩個操做,一個是設置背景邊界,另外一個是繪製背景。若是有mScrollXmScrollY有值說明有發生滾動,就先移動畫布以後再進行繪製。接下來看一下onDraw方法:

protected void onDraw(Canvas canvas) {
  }
複製代碼

onLayout同樣也是一個空方法,不一樣View須要根據需求實現不一樣內容的繪製。再下來是dispatchDraw方法。

protected void dispatchDraw(Canvas canvas) {
  }
複製代碼

dispatchDraw一樣仍是空方法,仍是由於單獨普通的View是沒有子View的因此不須要繪製子View。最後來看onDrawForeground方法繪製裝飾。

public void onDrawForeground(Canvas canvas) {
        //繪製滾動指示器
        onDrawScrollIndicators(canvas);
        //繪製滾動條
        onDrawScrollBars(canvas);
        final Drawable foreground = mForegroundInfo != null ? mForegroundInfo.mDrawable : null;
        if (foreground != null) {
            if (mForegroundInfo.mBoundsChanged) {
                mForegroundInfo.mBoundsChanged = false;
                final Rect selfBounds = mForegroundInfo.mSelfBounds;
                final Rect overlayBounds = mForegroundInfo.mOverlayBounds;
                if (mForegroundInfo.mInsidePadding) {
                    selfBounds.set(0, 0, getWidth(), getHeight());
                } else {
                    selfBounds.set(getPaddingLeft(), getPaddingTop(),
                            getWidth() - getPaddingRight(), getHeight() - getPaddingBottom());
                }
                final int ld = getLayoutDirection();
                Gravity.apply(mForegroundInfo.mGravity, foreground.getIntrinsicWidth(),
                        foreground.getIntrinsicHeight(), selfBounds, overlayBounds, ld);
                foreground.setBounds(overlayBounds);
            }
             //繪製前景
            foreground.draw(canvas);
        }
    }
複製代碼

onDrawForeground方法中主要是繪製了滾動指示器、滾動條和前景相關內容。Viewdraw流程就是這些,流程圖以下。

View的draw流程

三、ViewGroup的繪製流程

3.1 ViewGroup的measure流程

ViewGroup的測量流程的起始和通常的View是同樣的,也是從measure開始,可是ViewGroup類中並無複寫measure方法,因此調用的也就是其父類View中的measure方法,因此這一部分ViewGroup與普通View是相同的。以後咱們知道在measure方法中會調用onMeasure方法進行測量。可是默認ViewGroup類中依舊沒有複寫onMeasure方法,這是由於每一個ViewGroup的特性不一樣需求不一樣,須要根據要求本身複寫onMeasure方法。onMeasure方法的複寫邏輯是這樣的:首先在onMeasure方法中須要測量每一個子View的大小,接着計算全部子View總的大小,最後經過setMeasuredDimension方法,將獲得的總大小設置保存到成員變量中,完成測量。

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        //全部子View數組
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                //測量每一個子View大小,傳入子View和父佈局大小
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }
複製代碼

ViewGroup提供有measureChildren這個用來測量子View的方法。在measureChildren方法中得到了全部子View的數組,而後調用measureChild方法,測量每一個子View

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
         //得到子View的LayoutParams
        final LayoutParams lp = child.getLayoutParams();
        // 根據父佈局的MeasureSpec和子View的LayoutParams,計算子View的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        //調用子View的measure方法
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
複製代碼

measureChild方法中獲取了子ViewLayoutParam佈局參數。而後經過getChildMeasureSpec方法傳入父佈局的MeasureSpec和子ViewLayoutParams獲取到子ViewMeasureSpec,最後調用了子Viewmeasure方法,完成最後的測量。那麼下面進入getChildMeasureSpec方法,看一下實現。

/**
   * 這三個參數分別爲:
   * spec:父佈局的MeasureSpec
   * padding:內邊距
   * childDimension:子View的寬高尺寸
   */
  public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        //獲取父佈局的specMode和specSize
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);
        //計算父佈局大小:用父佈局的specSize減去內邊距
        int size = Math.max(0, specSize - padding);
        int resultSize = 0;
        int resultMode = 0;
        //根據父佈局的specMode判斷
        switch (specMode) {
        case MeasureSpec.EXACTLY:
            //父佈局的specMode爲EXACTLY時
            if (childDimension >= 0) {
                //childDimension大於0即子View的寬或高有具體的值
                //返回的大小就爲childDimension大小,mode爲EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //若是子View寬或高爲MATCH_PARENT
                //返回的大小就爲父佈局大小,mode爲EXACTLY
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //若是子View寬或高WRAP_CONTENT
                //返回的大小就由子View本身肯定,最大不能超過父佈局大小,mode因此爲AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        //父佈局的specMode爲AT_MOST時
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                //childDimension大於0即子View的寬或高有具體的值
                //返回的大小就爲childDimension大小,mode爲EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //若是子View寬或高爲MATCH_PARENT
                //返回的大小就爲父佈局大小,mode爲AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //若是子View寬或高WRAP_CONTENT
                //返回的大小就由子View本身肯定,最大不能超過父佈局大小,mode因此爲AT_MOST
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;
        //父佈局的specMode爲UNSPECIFIED時
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                //childDimension大於0即子View的寬或高有具體的值
                //返回的大小就爲childDimension大小,mode爲EXACTLY
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                //若是子View寬或高爲MATCH_PARENT
                //返回的大小根據View.sUseZeroUnspecifiedMeasureSpec判斷,mode爲UNSPECIFIED
                //sUseZeroUnspecifiedMeasureSpec是View類的成員變量由targetSdkVersion版本決定
                //sUseZeroUnspecifiedMeasureSpec = targetSdkVersion < Build.VERSION_CODES.M
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                //若是子View寬或高爲WRAP_CONTENT
                //返回的大小根據View.sUseZeroUnspecifiedMeasureSpec判斷,mode爲UNSPECIFIED
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //調用makeMeasureSpec方法建立一個MeasureSpec返回
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
複製代碼

從這個方法能夠看出來一個子ViewMeasureSpec是由其父佈局的MeasureSpec和自身的LayoutParams共同決定的,根據不一樣的組合最終返回不一樣的結果。在測量完全部的子View以後,須要實現計算ViewGroup總大小,最後調用setMeasuredDimension方法存儲測量的結果就能夠了。ViewGroupmeasure流程圖以下。

ViewGroup的measure流程

3.2 ViewGroup的layout流程

和單個普通View同樣,ViewGrouplayout流程一樣從layout方法開始。

@Override
    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 {
            mLayoutCalledWhileSuppressed = true;
        }
    }
    @Override
    protected abstract void onLayout(boolean changed,
            int l, int t, int r, int b);
複製代碼

ViewGroup中複寫了layout方法,添加了一些動畫過分效果的判斷,核心仍是調用了super.layout方法,繼而會調用onLayout方法,onLayout方法是一個抽象方法,這是由於ViewGroup一樣要根據自身要求特性實現不一樣的layout邏輯,因此須要由不一樣的ViewGroup自身來實現onLayout方法。接下來看一下具體的一個ViewGroup中的onLayout方法,LinearLayout中的onLayout方法。

@Override
    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);
        }
    }
複製代碼

根據不一樣方向分爲兩個方法,這裏來看layoutVertical方法:

void layoutVertical(int left, int top, int right, int bottom) {
        final int paddingLeft = mPaddingLeft;
        int childTop;
        int childLeft;
        // 計算子View最右的位置
        final int width = right - left;
        int childRight = width - mPaddingRight;
        // 子View的空間
        int childSpace = width - paddingLeft - mPaddingRight;
        // 得到子View個數
        final int count = getVirtualChildCount();
        ......
        // 循環遍歷子View
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {
                // 可見性不爲GONE的子View得到其寬高
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();
                // 得到子View的LayoutParams
                final LinearLayout.LayoutParams lp =
                        (LinearLayout.LayoutParams) child.getLayoutParams();
                ......
                //計算topMargin
                childTop += lp.topMargin;
                //調用setChildFrame方法,該方法中調用了子View的layout方法來肯定位置
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);
                // childTop增長子View的高度和bottomMargin,使下一個子View垂直在下方放置
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

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

複製代碼

這裏省略了Gravity屬性的相關代碼,來看主要的流程,首先是去除內邊距計算好子View的放置空間,而後遍歷子View得到子View的寬高,再經過setChildFrame方法肯定位置,而後遞增childTop,這樣接計算好了子View的位置,使子View一個個垂直排列下去。LinearLayoutonLayout方法主要實現方法就是這樣,其餘不一樣的ViewGroup也都實現了本身的onLayout方法。

ViewGroup的layout流程

3.3 ViewGroup的draw流程

ViewGroup繪製流程一樣會調用Viewdraw方法,一樣會繪製背景,繪製自身內容等,不一樣的是dispatchDraw方法,View中該方法是一個空方法,可是ViewGroup中就須要複寫這個方法,來看ViewGroup中的dispatchDraw方法。

@Override
    protected void dispatchDraw(Canvas canvas) {
        boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
        //得到全部子View個數和子View數組
        final int childrenCount = mChildrenCount;
        final View[] children = mChildren;
        int flags = mGroupFlags;

        ......
        
        //循環遍歷子View
        for (int i = 0; i < childrenCount; i++) {
            while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
                final View transientChild = mTransientViews.get(transientIndex);
                if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                        transientChild.getAnimation() != null) {
                    // 調用drawChild方法繪製子View
                    more |= drawChild(canvas, transientChild, drawingTime);
                }
                transientIndex++;
                if (transientIndex >= transientCount) {
                    transientIndex = -1;
                }
            }

       ......
    }
複製代碼

dispatchDraw方法裏核心邏輯仍是循環遍歷了子View,而後調用了drawChild繪製子View,而drawChild中又調用了子Viewdraw方法繪製子View

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
  }
複製代碼

ViewGroup的draw流程

四、總結

這一篇主要講了ViewViewGroup的繪製流程,不管ViewViewGroup都經歷了measurelayoutdraw這三個階段。之間主要的區別就是View沒有子View的問題,而ViewGroup的繪製流程中還須要調用其子View的對應流程,完成子View的繪製。那麼在瞭解了ViewViewGroup的繪製流程以後,新的問題又來了。View樹中,子View的了measurelayoutdraw方法是由其父級的ViewGroup調用的,父ViewGroup繪製又是由它的父級調用的,那麼根節點的繪製流程是由誰調用開啓的呢?繪製好的界面又是怎樣顯示的呢?最後一篇文章就來看看這些內容。

相關文章
相關標籤/搜索