《Android藝術開發探索》學習筆記之View的工作原理

鋸齒流沙發表於2017-12-26

初識ViewRoot和DecorView

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

root = new ViewRootImpl(view,getContext(),display);
root.addView(view,wparams,panelParentView);
複製程式碼

View的繪製流程是從RootView的performTraverslas方法開始的,它經過measure,layout和draw三個過程才能最終將一個View繪製出來。 measure:測量View的寬和高 layout:確定View在父容器中的放置位置 draw:負責將View繪製在螢幕上

針對performTraversals的大致流程,如下圖所示:

performTraversals

performTraversals依次呼叫preformMeasure、preformLayout和performDraw三個方法,這個三個方法分別完成頂級View的measure、layout和draw。其中preformMeasure中會呼叫measure方法,在measure方法中又會呼叫onMeasure方法,在onMeasure方法中則會對所有的子元素進行measure過程,這個時候measure流程就從父容器傳遞到子元素中了,這樣就完成了一次measure過程。接著子元素會重複父容器的measure過程,如此反覆就完成了整個View樹的遍歷。同理,preformLayout和performDraw的傳遞流程和preformMeasure是類似的,唯一不同的是,preformDraw的傳遞過程是在draw方法中通過dispatchDraw來實現的,不過並沒有本質區別。

measure過程決定了View的寬和高,measure完成以後,可以通過getMeasureWidth和getMeasureHeight方法獲得View測量後的寬/高,在幾乎所有的情況下它都等同於View最終的寬/高。

layout過程決定了View的四個頂點的座標和實際VIew的寬/高,完成以後,可以通過getTop、getBottom、getLeft和getRight來拿到View的四個頂點的位置。並可以通過getWidth和getHeight方法拿到View的最終寬/高。

Draw過程決定了View的顯示,只有通過draw方法完成以後View的內容才能呈現在螢幕上。

DecorView作為頂級View,一般情況下它內部會包含一個豎直方向的LinearLayout,在這個LinearLayout裡面又上下兩個部分(具體情況和Android版本以及主題有關),上面是標題欄,下面是內容欄,在Activity中我們通過setContentView所設定的佈局其實就是被加到內容欄中的,而內容欄的id是content,因此可以理解為Activity指定的佈局方法不叫setView而叫setContentView,因為我們的佈局的確加到了id為content的FrameLayout中。 獲取content:ViewGroup content = findViewById(R.android.content)。 獲取我們設定的VIew:content.getChildAt(0). 從原始碼中獲知,DecorView其實是一個FrameLayout,View層的事件都先經過DecorView,然後才傳遞給我們的View。

理解MeasureSpec

MeasureSpec很大程度上決定 了一個View的尺寸規格,之所以說是很大程度上是因為這個過程還受父容器影響,因為父容器影響View的MeasureSpec的建立過程。

在測量過程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後根據這個MeasureSpec來測量出View的寬/高。

MeasureSpec

MeasureSpec代表一個32位int值,高2位代表SpecMode,低30位代表SpecSize。 SpecMode:測量模式 SpecSize:某種測量模式下的規格大小

SpecMode有三類: 1)UNSPECIFIED:父容器不對View有任何限制,要多大給多大,這種情況一般用於系統內部,表示一種測量的狀態。

EXACTLY:父容器已經檢測出View所需要的精確大小,這個時候View的最終大小就是SpecSize所指定的值。它對應於LayoutParams中的match_parent和具體的數值這兩種模式。

AT_MOST:父容器指定一個可用大小,即SpecSize,View的大小不能超過這個值,具體是什麼值要看不同的View的具體實現。它對應於LayoutParams中的wrap_content。

MeasureSpec和LayoutParams的對應關係

MeasureSpec不是唯一由LayoutParams決定的,LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬/高。另外,對於頂級View(即DecorView)和普通View來說,MeasureSpec的轉換過程略有不同。

DecorView:其MeasureSpec由視窗的尺寸和自身的LayoutParams來共同決定。DecorView的MeasureSpec產生過程,具體遵守如下規則,根據它的LayoutParams中的寬/高引數來劃分。 1)LayoutParams.MATCH_PARENT:精確模式,大小就是視窗的大小;

2)LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超過視窗大小;

3)固定大小(比如100dp):精確模式,大小為LayoutParams中指定的大小。

普通View:其MeasureSpec由父容器的MeasureSpec和自身的LayoutParams來共同決定,MeasureSpec一旦確定後,onMeasure中就可以確定View的測量寬/高。其建立規則如下圖所示

普通View的MeasureSpec規則

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

UPSPECIFIE模式:主要作用於系統內部多次measure的情形,一般來說,我們不需要關注此模式。

View的工作流程

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

measure過程

原始的View:通過measure方法就完成了其測量過程; ViewGroup:除了完成自己的測量過程外,還會遍歷去呼叫所有子元素的measure方法,各個子元素再遞迴去執行這個流程。

1、View的Measure過程 View的measure過程由其measure方法來完成,measure是一個final型別的方法,這意味著子類不能重寫此方法,在View的measure方法中會去呼叫View的onMeasure方法,因此只需要看onMeasure的實現即可。

View的onMeasure方法如下:

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

setMeasuredDimension方法會這是View的寬/高的測量值

    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,而這個spectSize就是View測量後的大小,是因為View的最終大小是在layout階段確定的,所以這裡必須加以區分,但是幾乎所有情況下View的測量大小和最終大小是相等的。

至於UNSPECIFIED這種情況,一般用於系統內部的測量過程,在這種情況下,View的大小為getDefaultSize的第一個引數size,即寬/高分別為getSuggestedMinimumWidth和getSuggestedMinimumHeight這兩個方法的返回值。

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

    protected int getSuggestedMinimumHeight() {
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());

    }
複製程式碼

getSuggestedMinimumWidth和getSuggestedMinimumHeight實現原理一樣,getSuggestedMinimumWidth分析:如果View沒有設定背景,那麼View的寬度為mMinWidth,而mMinWidth對應於Android:minWidth這個屬性所指定的值,因此View的寬度即為Android:minWidth屬性所指定的值。這個屬性如果不指定,那麼mMinWidth則預設為0;如果View指定了背景,則View的寬度為max(mMinWidth,mBackground.getMinimumWidth()),即Android:minWidth和背景的最小寬度這兩者中的最大值。

從getDefaultSize方法的實現來看,View的寬/高是由spectSize決定,所以我們可以得出:直接繼承View的自定義控制元件需要重寫onMeasure方法並設定wrap_content時的自身大小,否則在佈局中使用wrap_content就相當於使用match_parent。造成的原因:上述程式碼和普通View的MeasureSpec規則表中分析得出,如果View在佈局中使用wrap_content,那麼它的specMode是AT_MOST模式,在這種模式下,它的寬/高等於specSize,在這模式下,它的寬/高等於specSize,從表中可知,這種情況下View的specSize是parentSize,而parentSize是父容器中目前可以使用的大小,也就是父容器當前剩餘的空間大小,很顯然,View的寬/高就等於父容器當前剩餘空間大小,這種效果和在佈局中使用match_parent完全一致。 解決辦法:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(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);
        }
    }
複製程式碼

從上面程式碼中得知,只需要給View指定一個預設的內部寬/高,並在wrap_content時設定寬/高即可。對於非wrap_content情形,我們沿用系統的測量值即可,至於預設的內部寬/高的大小如何指定,這個沒有固定的依據,根據需要靈活指定即可,如果檢視TextView、ImageView等原始碼就可以知道,針對wrap_content情形,他們的onMeasure方法均做了特殊處理。

ViewGroup的onMeasure過程

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


    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方法來進行測量。

View的measure過程是三大流程中最複雜的一個,measure完成以後,通過getMeasureWidth/height方法就可以正確地獲取到View的測量寬/高。需要注意的是,在某些極端情況下,系統可能需要多次measure才能正確最終的測量寬/高,在這種情形下,在onMeasure方法中拿到的測量寬/高可能是不準確的。一個比較好的習慣是在onLayout方法中去獲取View的測量寬/高或者最終寬/高。

Activity啟動時獲取某個View的寬/高:由於onCreate、onStart、onResume中均無法正確得到某個View的寬/高資訊,這是因為View的measure過程和Activity的生命週期方法不是同步執行的,因此無法保證Activity執行了onCreate、onStart、onResume時某個View已經測量完畢了,如果View還沒有測量完畢,那麼獲取的寬/高就是0。解決方法:

1)Activity/View#onWindowFocusChanged onWindowFocusChanged這個方法的含義是:View已經初始化完畢了,寬/高已經準備好了,這個時候去獲取寬/高是沒問題的。需要注意的是,onWindowFocusChanged會被呼叫很多次,當Activity的視窗得到焦點和失去焦點時均被呼叫一次。具體來說,當Activity繼續執行和暫停執行時,onWindowFocusChanged均會被呼叫,如果頻繁地進行onResume和onPause,那麼onWindowFocusChanged也會頻繁地呼叫。 使用onWindowFocusChanged的典型程式碼:

	@Override
	public void onWindowFocusChanged(boolean hasWindowFocus) {
		super.onWindowFocusChanged(hasWindowFocus);
		if (hasWindowFocus){
			int width = view.getMeasuredWidth();
			int height = view.getMeasuredHeight();
		}
	}
複製程式碼

2)view.post(runnable) 通過post 可以講一個runnable投遞到訊息佇列的尾部,然後等待Looper呼叫此runnable的時候,View也已經初始化好了。 程式碼如下:

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

	}
複製程式碼

3)ViewTreeObserver 當View樹的狀態發生改變或者View樹內部的View的可見性發生改變時,onGlobalLayout方法將被回撥,因此這是獲取View的寬/高一個很好的時機,需要注意的是,伴隨著View樹的狀態改變等,onGlobalLayout會被呼叫多次。 典型程式碼如下:

	@Override
	protected void onStart() {
		super.onStart();
		ViewTreeObserver observer = view.getViewTreeObserver();
		observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
			@Override
			public void onGlobalLayout() {
				view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
				int width = mListView.getMeasuredWidth();
				int height = mListView.getMeasuredHeight();
			}
		});
	}
複製程式碼

4)view.measure(int widthMeasureSpec,int heightMeasureSpec) 通過手動對View進行measure來得到View的寬/高。這種情況比較複雜,這裡要分情況處理,根據View的LayoutParams來分:

match_parent:直接放棄,無法measure出具體的寬/高。 具體的數值(dp/px):

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec(100,MeasureSpec.EXACTLY);
view.measure(widthMeasureSpec ,heightMeasureSpec );

複製程式碼

wrap_content:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30)-1,MeasureSpec.AT_MOST);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((1 << 30)-1,MeasureSpec.AT_MOST);
view.measure(widthMeasureSpec ,heightMeasureSpec );
複製程式碼

(1 << 30)-1,通過分析MeasureSpec的實現可以知道,View的尺寸使用30位二進位制表示,也就是說最大是30個1(即2^30)-1,在最大模式下,我們使用view理論上能支援的最大值去構造MeasureSpec是合理的。

錯誤用法: 第一種錯誤用法:

int widthMeasureSpec = MeasureSpec.makeMeasureSpec(-1,MeasureSpec.UNSPECIFIE);
int heightMeasureSpec = MeasureSpec.makeMeasureSpec((-1,MeasureSpec.UNSPECIFIE);
view.measure(widthMeasureSpec ,heightMeasureSpec );
複製程式碼

第二種錯誤用法: view.measure(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);

layout過程

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

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

draw過程

View的繪製過程遵循如下幾步:

1)繪製背景background.draw(canvas)

2)繪製自己

3)繪製children(dispatchDraw)

4)繪製裝飾(onDrawScrollBars)

View的繪製過程的傳遞是通過dispatchDraw來實現的,dispatchDraw會遍歷呼叫所有子元素的draw方法,如此draw事件就一層層地傳遞下去。View有一個特殊的方法setWillNotDraw:如果一個View不需要繪製任何內容,那麼設定這個標誌位為true後,系統會進行相應的優化。預設情況下,View沒有啟用這個優化標誌位,但是ViewGroup會預設啟用這個優化標誌位。這個標誌位對實際開發的意義是:當我們自定義控制元件繼承於ViewGroup並且本身不具備繪製功能時,就可以開啟這個標誌位從而便於系統進行後續的優化。當然,當明確知道一個ViewGroup需要通過onDraw來繪製內容時,我們需要顯式地關閉WILL_NOT_DRAW這個標誌位。

相關文章