View的工作原理


在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,開發人員必備技能——單元測試


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM