View的工作原理

View的工作原理

ViewRoot和DecorView

ViewRoot對應于ViewRootImpl,連接WindowManager和DecorView的紐帶。
View的繪制流程從ViewRoot的performTraversals方法開始,經過以下三個過程:

  1. measure
  2. layout
  3. draw

理解MeasureSpec

MeasureSpec是一個會影響到View測量過程的參數。在測量View的寬高的過程中,系統會將View的LayoutParams根據父View的規則轉換成對應的MeasureSpec,在進行寬高測量。

MeasureSpec

MeasureSpec是一個32位的int值,高兩位代表SpecMode,低30位代表SpecSize。即模式+尺寸。Android里將這兩個參數打包成了一個int值來避免過都的內存分配,可以通過get方法解包得到mode和size的分別值。源碼里主要是一些“位操作”,類似:

        private static final int MODE_SHIFT = 30;
        private static final int MODE_MASK  = 0x3 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has not imposed any constraint
         * on the child. It can be whatever size it wants.
         */
        public static final int UNSPECIFIED = 0 << MODE_SHIFT;

        /**
         * Measure specification mode: The parent has determined an exact size
         * for the child. The child is going to be given those bounds regardless
         * of how big it wants to be.
         */
        public static final int EXACTLY     = 1 << MODE_SHIFT;

        /**
         * Measure specification mode: The child can be as large as it wants up
         * to the specified size.
         */
        public static final int AT_MOST     = 2 << MODE_SHIFT;

如源碼里給出的,SpecMode有三類:

  • UNSPECIFIED:父容器不對View有任何限制,一般系統內部使用。
  • EXACTLY:父容器已經檢測出View的精確大小,由SpecSize指定。
  • AT_MOST:父容器指定一個可用大小SpecSize,子View的大小不能超過這個值。

子View Mode會被父View的specMode所影響,在getChildMeasureSpec方法中,給出了這種影響的具體過程,其流程圖如下:

子View會根據父View的Spec不同模式,得到不同的結果。

從流程圖和表格可以總結出:

  1. View的MesureSpec由父View的MesureSpec和自身的LayoutParams共同決定;
  2. 若View指定了大小,則不管父View的MeasureSpec如何,其Spec將總是ECACTLY,而大小為其指定的大小;
  3. 子View的LayoutParams為Wrap_content時,無論父類為何種模式,子View總是AT_MOST。因此,對于自定義控件來說,當指定view為wrap_content時,需要指定自身的大小,否則子View會在AT_MOST的模式下,最大程度的利用父View的空間。
  4. getMeasureSpec方法返回的是一個打包后的MesureSpec,子View的Mode將由其前2位確定,而后30位事實上代表了父View的可用大小,子View將參考這一值,但并不是最終子View的大小(事實上,View的最終大小是在layout階段被確定的,但是一般情況下,View的測量大小和最終大小相等)。

View的工作流程

View的工作流程主要有:measure、layout、draw,即測量,布局和繪制。

View的measure過程

對于View來說,measure過程就是測量自身尺寸的過程;對于ViewGroup來說,measure過程除了測量自身尺寸外,還要遞歸的去測量所有children的尺寸。

View的measure過程比較簡單:

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

基本上,所有的onMeasure都要干一件事:計算好自己的寬高,然后調用setMeasuredDimension方法保存。對于自定義的View,我們要自己計算width和height數值。這里就不貼getDefaultSize的代碼了,也比較簡單,就是根據SpecMode的值,來判斷應該使用什么樣的size。

ViewGroup的measure過程

ViewGroup的measure過程除了繪制自身外,還要繪制其children。ViewGroup本身是個抽象類,并沒有去實現View的onMeasure方法,其通過一個measureChildren的方法對所有的Children進行測量。

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

其中調用了measureChild對每個child進行測量:


測量過程本質上和View是一致的,外部傳入了需要測量的child視圖和父View的MeasureSpec,在調用View中getChildMeasureSpec方法創建MeasureSpec,而測量結果傳遞到View的measure方法中進行測量。接下去就是一個遞歸遍歷的過程。

由于ViewGroup本身是抽象類,沒有實現onMeasure方法,因此需要其具體的實現類,來完成這個方法。典型如LinearLayout、RelativeLayout等。事實上,每個ViewGroup的onMeasure方法考慮的東西很多,Android里LinearLayout源碼還比較長,值得一看,可以了解下具體的測量過程。


View 的Measure過程和Activity的生命周期方法并不同步,往往在onCreate方法中去獲取View的尺寸,得到的值并不是最終View的尺寸大小,為了在Activity啟動時獲取一個View的尺寸,有四種方法。

(1) Activity/View#onWindowsFocusChanged

當Activity窗口獲得焦點時會被調用,并且這個方法表示View已經初始化完畢。

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        if (hasFocus) {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
    }

(2) view.post(Runnable)

    @Override
    protected void onStart() {
        super.onStart();
        view.post(new Runnable() {
            @Override
            public void run() {
                int width = view.getMeasuredWidth();
            }
        });
    }

(3) ViewTreeObserver

    @Override
    protected void onStart() {
        super.onStart();
        ViewTreeObserver observer = view.getViewTreeObserver();
        observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @SuppressLint("NewApi")
            @Override
            public void onGlobalLayout() {
                view.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                int widt = view.getMeasuredHeight();
            }
        });
    }

layout過程

layout是在Measure結束后的步驟,將用來確定子View的位置。對于ViewGroup來說,layout方法確定本身的位置,然后調用onlayout方法確定所有子view的位置。對于View而言,其layout過程如下:

    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本身的四個頂點位置,在調用onLayout方法去測量子View的位置,而onlayout是一個抽象方法,對于一個view而言,將不會有什么作用,對于一個ViewGroup而言,將會去確定其中所有子view的位置;同樣的,在子view內,也會再調用layout方法確定自身和onlayout方法確定子子view,因此通過一層一層的傳遞,完成整個view樹的layout過程。

ViewGroup的一個實現類是LinearLayout,在LinearLayout中,會重寫onlayout方法,來完成自身和子View的布局位置確定:

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

LinearLayout布局可以選擇橫向排列或者縱向排列內部的子View,兩者實現邏輯類似,看看layoutVertical(l,t,r,b)的一些代碼:

void layoutVertical(int left, int top, int right, int bottom) {
  ......
  for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
    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);
  }
}

在layoutVertical中,通過Gravity的屬性,來判斷child的left、right、top等參數如何計算,這里省略貼代碼了。在對一個子view計算好四個坐標后,通過setChildFrame函數記錄。注意到在setChildFrame后,childTop會加上這個chil自身的高度,這就意味著下一個child的視圖位置一定會在當前child下面,實現垂直排列的效果。而在setChildFrame中,實際上也是調用了view的layout方法:

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

draw過程

在Measure和layout之后,意味著每一個view在屏幕上的最終大小和位置都被確定了,這時候就通過draw過程將其繪制到屏幕上,其步驟:

  1. 繪制背景background.draw(canvas)
  2. 繪制自己(onDraw)
  3. 繪制Children(dispatchDraw)
  4. 繪制裝飾(onDrawScrollBars)
/*
* 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)
*/

View有一個特殊的方法setWillNotDraw,它表示如果一個View不需要繪制本身,可以把這個標志位設為true,以便于系統對其進行優化。顯然,一個普通的view一般不會去設置這個標志位,但是在某些ViewGroup中,可能本身并不需要經行繪制,那么可以通過這個方法設置從而優化性能。

    /**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

自定義View

按照Android開發藝術探索里的分類,有如下四種情況:

  1. 繼承View重寫onDraw 方法
  2. 繼承ViewGroup派生
  3. 繼承已有的實體View,如TextView
  4. 繼承已有的實體ViewGroup,如LinearLayout

總結一下就是:自定義View,毫無疑問都需要直接或者間接繼承于View,然后根據具體需要實現的功能,來決定是利用現有的View來擴展,還是從底層開始重寫。繼承的層次越少,自定義空間就越大,同時難度也越高。因此,自定義View時,需要我們找到一種cost最小的方法去實現我們需要的功能。

自定義View時,一些注意事項:

  • View需要去支持wrap_content $$ wrap_content對應的MeasureSpec是AT_MOST,如果View不對wrap_content進行處理,會最大限度的利用父view的空間
  • View需要去處理padding 和margin $$ 從之前的三大過程來看,padding和margin是參與到了view的繪制計算中的,如果不處理,則這些屬性會無效
  • View中盡量不使用Handler $$ 因為View本身提供了post方法來發送消息
  • View中如果有線程或者動畫,需要及時停止 $$ 一般在onDetachedFromWindow中處理,否則可能造成內存泄露
  • View如果帶有滑動嵌套,需要處理滑動沖突 $$ 有外部攔截發和內部攔截法

重寫onDraw方法

public class CircleView extends View {    
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        final int paddingLeft = getPaddingLeft();
        final int paddingRight = getPaddingRight();
        final int paddingTop = getPaddingTop();
        final int paddingBottom = getPaddingBottom();

        int width = getWidth()-paddingLeft-paddingRight;
        int height = getHeight()-paddingTop-paddingBottom;

        int radus = Math.min(width,height)/2;
        canvas.drawCircle(paddingLeft+width/2,paddingTop+height/2,radus,mPaint);
    }
}

通過自定義了一個CircleView,重寫其onDraw方法,來實現畫圓。可以看到,onDraw方法中,我對padding屬性經行了處理,使得自定義View才能夠對xml文件里的padding屬性經行支持;另外,在onMeasure方法里,也對wrap_content的默認屬性進行了設置。為了使一個自定義View支持我們需要的自定義屬性,需要在values目錄下創建一個自定義屬性是xml文件:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <declare-styleable name="CircleView">
        <attr name="circle_color" format="color"/>
    </declare-styleable>
</resources>

之后,在CircleView的構造函數里對屬性進行解析:

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
        mColor = array.getColor(R.styleable.CircleView_circle_color, Color.RED);
        array.recycle();
        init();
    }

最后,在xml布局文件中,正常使用即可。需要注意的是,要對命名空間進行聲明,類似:

xmlns:app="http://schemas.android.com/apk/res-auto"
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,963評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,348評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,083評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,706評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,442評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,802評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,795評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,983評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,542評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,287評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,486評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,030評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,710評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,116評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,412評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,224評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,462評論 2 378

推薦閱讀更多精彩內容