View的工作原理

cryAllen發表於2017-01-22

在Android的知識體系中,View扮演著很重要的角色,簡單來理解,View是Android在視覺上的呈現。在介面上Android提供了一套GUI庫,裡面有很多控制元件,但是很多時候我們並不滿足於系統提供的控制元件,因為這樣就意味著這應用介面的同類比較嚴重,如何做出與眾不同的效果呢,就是自定義View。

初始ViewRoot和DecorView

首先,要先了解下View的一些基本概念,這樣才能更好理解View的measure、layout和draw過程。

ViewRoot對應於ViewRootImpl類,它是連線WindowManager和DecorView的紐帶,View的三大流程是通過VeiwRoot來完成的。在ActivityThread中,當Activity物件被建立完畢後,會將DecorVeiw新增到Window中,同時會建立ViewRootImpl物件,並將ViewRootImpl物件和DecorView建立關聯。

View的繪製流程是從ViewRoot的performTraversals方法開始的,它經過measure、layout和draw三個過程才能最終將一個View繪製出來。如圖:

View繪製流程圖

從中,我們可以看到,performTraversals會依次呼叫performMeasure、performLayout、performDraw三個方法,這個三個方法分別完成頂級View的measure、layout、draw這三大方法,在onMeasure方法中則會對所有的子元素進行measure過程,這個時候measure流程就從父容器傳遞到子元素中了,這樣就完成一次measure過程,接著子元素會重複父容器的過程,如此反覆就完成了整個View樹的遍歷。同理,其他兩個步驟也是類似的過程。

measure過程決定了View的寬和高,Measure完成以後,可以通過getMeasureWidth和getMeasureHeight方法來獲取到View的測量後的寬和高。

理解MeasureSpec

確切來說,MeasureSpec在很大程度上決定了一個View的尺寸規格,之所以說是很大程度上是因為這個過程還是受父容器的影響,因為父容器影響View的MeasureSpec的建立過程。在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後再根據這個measureSpec來測量出View的寬和高。

MeasureSpec代表一個32位int值,高2位代表SpecMode,低30位代表SpecSize,SpecMode是指測量模式,而SpecSize是指在某種測量模式下的規格大小。MeasureSpec通過將SpecMode和SpecSize打包成一個int值來避免過多的物件記憶體分配,為了方便操作,其提供了打包和解包的方法。

SpecMode有三類,每一類都表示了特殊的含義。

  • UNSPECIFIED。父容器不對View有任何限制,要多大就給多大,這種情況一般用於系統內部,表示一種測量的狀態。
  • EXACTLY。父容器已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應於LayoutParams中的match_parent和具體的數值這兩種模式。
  • AT_MOST。父容器指定了一個可用大小即SpecSize,View的大小不能大於這個值,具體是什麼值要看不同的View的具體實現。它對應於LayoutParams中的wrap_content。

簡單來說,當View採用固定寬/高的時候,不管父容器的MeasureSpec是什麼,View的MeasureSpec都是EXACTLY模式並且大小遵循LayoutParams中的大小,當View的寬/高是match_parent時,如果父容器的模式是EXACTLY,那麼View也是精確模式並且其大小是父容器的剩餘空間,如果父容器是最大模式,那麼View也是最大模式並且其大小不會超過父容器的剩餘空間,當View的寬/高是wrap_content時,不管父容器的模式是精確還是最大化,View的模式總是最大化並且大小不能超過父容器的剩餘空間。

View的工作流程

View的工作流程主要是指measure、layout、draw這三大流程,即測量、佈局、繪製,其中measure確定View的測量寬/高,layout確定View的最終寬/高和四個頂點的位置,而draw則將View繪製到螢幕上。

measure過程

measure過程要分情況來看,如果是一個原始的View,那麼通過measure方法就完成了其測量過程,如果是一個ViewGroup,除了完成自己的測量過程外,還會遍歷去呼叫所有子元素的measure方法,各個子元素再遞迴去執行這個流程。

1,View的measure過程

measure方法是一個final型別的方法,這意味著子類不能重寫此方法,在View的measure方法中會去呼叫View的onMeasure方法,所以只需看onMeasure方法即可。

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

上面的程式碼很簡潔,但是簡潔並不代表簡單,setMeasuredDimension方法會設定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的最終大小是在layout階段確定的,所以這裡必須要加以區分,但是幾乎所有情況下的View的測量大小和最終大小是相等的。

同時,直接繼承View的自定義控制元件需要重寫onMeasure方法並設定wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於使用match_parent。為什麼呢,如果View在佈局中使用wrap_content,那麼它的specMode是AT_MOST模式,在這種模式下,它的寬/高等於specSize,也就是說,這種情況下的View的specSize是parentSize,而parentSize是父容器中目前當前剩餘使用的大小,也就是父容器當前剩餘的空間大小。

那麼該如何該解決這個問題,很簡單,程式碼如下:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, 200);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(200, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSpecSize, 200);
        }
    }

給wrap_content設定一個預設值,比如都是寬/高都是200px。

2,ViewGroup的measure過程

對於ViewGroup來說,除了完成自己的measure過程以外,還會遍歷去呼叫所有子元素的measure方法,各個子元素再去遞迴執行這個過程。和View不同的是,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);
            }
        }
    }

從上述程式碼來看,ViewGroup在measure時,會對每一個子元素進行measure。具體measureChild這個方法的實現也很好理解,如下所示:

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

很明顯,measureChild的思想就是取出子元素的LayoutParams,然後再通過getChildMeasureSpec來建立子元素的MeasureSpec,接著將MeasureSpec直接傳遞給View的measure方法進行測量。

Layout過程

Layout的作用是ViewGroup用來確定子元素的位置,當ViewGroup的位置被確定後,它在onLayout中會遍歷所有的子元素並呼叫其layout方法,在layout方法中onLayout方法又會被呼叫。先看下View中的layout方法

    @SuppressWarnings({"unchecked"})
    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方法的大致流程:首先會通過setFrame方法來設定View的四個頂點的位置,即初始化mLeft、mRight、mTop和mBottom這四個值,View的四個頂點一旦確定,那麼View在父容器中的位置也就確定了,接著就會呼叫onLayout方法,這個方法用途是父容器確定子元素的位置,和onMeasure方法類似,onLayout的具體實現同樣和具體的佈局有關,所以View和ViewGroup均沒有真正實現onLayout方法。

draw過程

draw過程就比較簡單了,它的作用是將View繪製到螢幕上面,View的繪製過程循序以下幾步:

  1. 繪製背景background.draw(canvas)
  2. 繪製自己(onDraw)
  3. 繪製children(dispatchDraw)
  4. 繪製裝飾(onDrawScrollBars)

這一點看程式碼,就能看出來。

public void draw(Canvas canvas) {
        if (mClipBounds != null) {
            canvas.clipRect(mClipBounds);
        }
        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) {
            final Drawable background = mBackground;
            if (background != null) {
                final int scrollX = mScrollX;
                final int scrollY = mScrollY;

                if (mBackgroundSizeChanged) {
                    background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
                    mBackgroundSizeChanged = false;
                }

                if ((scrollX | scrollY) == 0) {
                    background.draw(canvas);
                } else {
                    canvas.translate(scrollX, scrollY);
                    background.draw(canvas);
                    canvas.translate(-scrollX, -scrollY);
                }
            }
        }

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

            // Step 6, draw decorations (scrollbars)
            onDrawScrollBars(canvas);

            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(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);
            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);
            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);
            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);
            canvas.drawRect(right - length, top, right, bottom, p);
        }

        canvas.restoreToCount(saveCount);

        // Step 6, draw decorations (scrollbars)
        onDrawScrollBars(canvas);

        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
    }

View的繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷呼叫所有子元素的draw方法,如此draw事件就一層層地傳遞下去。

自定義View

自定義View是一個綜合的技術體系,它涉及View的層次結構、事件分發機制和View的工作原理等技術細節。

自定義View的分類

  1. 繼承View重寫onDraw方法。這種方法主要用於實現一些不規則的效果,即這種效果不方便通過佈局的組合方式來達到,往往需要靜態或者動態地顯示一些不規則的圖形。這種方式需要重寫onDraw方法,同時需要自己支援wrap_content,並且padding也需要自己處理。
  2. 繼承ViewGroup派生特殊的Layout。這種方法主要用於實現自定義的佈局,即除了LinearLayout、RelativeLayout、FrameLayout這幾種系統的佈局之外,我們需要重新定義一種新的佈局。
  3. 繼承特定的View(比如TextView)。這種方法比較常見,一般是用於擴充套件某種已有的View的功能,比如TextView。
  4. 繼承特定的ViewGroup(比如LinearLayout)。這種效果看起來很像幾種View組合在一起的時候,可以採用這種方法實現。

自定義View須知

一些具體的注意事項。

  • 讓View支援wrap_content
  • 如果有必要,讓你的View支援padding
  • 儘量不要在View中使用Handler,沒必要
  • View中如果有執行緒或者動畫,需要及時停止,參考View#onDetachedFromWindow
  • View帶有滑動巢狀情形時,需要處理好滑動衝突

閱讀擴充套件

源於對掌握的Android開發基礎點進行整理,羅列下已經總結的文章,從中可以看到技術積累的過程。
1,Android系統簡介
2,ProGuard程式碼混淆
3,講講Handler+Looper+MessageQueue關係
4,Android圖片載入庫理解
5,談談Android執行時許可權理解
6,EventBus初理解
7,Android 常見工具類
8,對於Fragment的一些理解
9,Android 四大元件之 " Activity "
10,Android 四大元件之" Service "
11,Android 四大元件之“ BroadcastReceiver "
12,Android 四大元件之" ContentProvider "
13,講講 Android 事件攔截機制
14,Android 動畫的理解
15,Android 生命週期和啟動模式
16,Android IPC 機制
17,View 的事件體系
18,View 的工作原理
19,理解 Window 和 WindowManager
20,Activity 啟動過程分析
21,Service 啟動過程分析
22,Android 效能優化
23,Android 訊息機制
24,Android Bitmap相關
25,Android 執行緒和執行緒池
26,Android 中的 Drawable 和動畫
27,RecylerView 中的裝飾者模式
28,Android 觸控事件機制
29,Android 事件機制應用
30,Cordova 框架的一些理解
31,有關 Android 外掛化思考
32,開發人員必備技能——單元測試

相關文章