Android源碼分析之View的繪製流程

對於Android開發者來說,View無疑是開發中經常接觸的,包括它的事件分發機制、測量、佈局、繪製流程等,如果要自定義一個View,那麼應該對以上流程有所瞭解、研究。本系列文章將會爲大家帶來View的工作流程詳細解析。在深入接觸View的測量、佈局、繪製這三個流程之前,我們從Activity入手,看看從Activity創建後到View的正式工作之前,所要經歷的步驟,包括Activity、DecorView、Window的關聯部分,解釋View怎麼從Window->DecorView->Activity->View的繪製整個線性流程。如下圖,需要繪製的View需要在Acvitity的View創建起來之後纔開始走測量、佈局、繪製等生命流程,Activity的View實際上就是圖示的ContentView,即Activity調用setContentView(layoutID)設置的View。ContentView屬於DecorView的子View,DecorView屬於PhoneWindow的子View,PhoneWindow是Window的一個實例,用於顯示一個窗體信息,整個流程顯示的明明白白,下面將會詳細分析。

在這裏插入圖片描述

1、Window

Window表示一個窗口的概念,Android手機中所有的視圖都是通過Window來呈現的,像常用的Activity,Dialog,PopupWindow,Toast,他們的視圖都是附加在Window上的,所以可以這麼說:Window是View的直接管理者。

分類

  • Window有三種類型,分別是應用Window、子Window、系統window。應用Window對應着一個Activity。子Window不能單獨存在,他需要附屬在特定的父Window之中,比如常見的一些Dialog。系統Window是要聲明權限才能創建的Window,比如Toast和系統狀態欄。
  • Window是分層的,每個Window都有對應的z-ordered,層級大的會覆蓋在層級小的Window上面。在三類window中,應用Window的層級範圍是1-99,子Window的層級範圍是1000-1999,系統Window的層級範圍是2000-2999,這些層級範圍對應這WindowManager.LayoutParam的type參數。當我們需要使用系統Window時,需要聲明權限。

PhoneWindow

  • Window的唯一實現類是PhoneWindow ,在啓動Activity的attach方法中被創建,Activity中setContentView實際上是調用 PhoneWindow 的setContentView 方法。並且 PhoneWindow 中包含着成員變量 DecorView。如上圖所以的頁面,實際上你看到的是一個PhoneWindow,它內部負責顯示的View就是DecorView,下面我們將揭開這個DecorView的真面目。

2、DecorView

通過上面的解釋我們知道,在Activity創建過程中,調用setContentView(layoutID)設置的View,最後將會以子View的形式設置給DecorView的某個子View,這三個DecorView究竟是什麼樣的結構?什麼是DecorView?

什麼是DecorView

  • DecorView是整個ViewTree的最頂層View,它是一個FrameLayout佈局,代表了整個應用的界面,一般包括TitleBar和mContentParent。
  • DecorView的子元素mContentParent就是ActivitysetContentView(layoutID)設置的View。
  • PhoneWindow中創建了一個DecroView,其中創建的過程中可能根據Theme不同,加載不同的佈局格式,例如有沒有Title,或有沒有ActionBar等,然後再向mContentParent中加入子View,即Activity中設置的佈局。

DecorView的佈局

我們先從源碼級別分析Activity的setContentView(layoutID)都做了啥?

一、setContentView的顯示過程

1、Activity的setContentView調用了PhoneWindow的setContentView

//PhoneWindow  --> setContentView() 
@Override 
public void setContentView(int layoutResID) { 
    if (mContentParent == null) { 
		//1.初始化 , 創建DecorView對象和mContentParent對象  
        installDecor(); 
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { 
        mContentParent.removeAllViews();//Activity轉場動畫相關 
    } 
	//2.填充Layout 
    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { 
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,getContext()); 
        transitionTo(newScene);//Activity轉場動畫相關 
    } else { 
    	//將Activity設置的佈局文件,加載到mContentParent中 
        mLayoutInflater.inflate(layoutResID, mContentParent); 
    } 
	...
}

2、installDecor():創建DecorView對象和mContentParent對象

//PhoneWindow --> setContentView()  --> installDecor() 
private void installDecor() { 
  if (mDecor == null) { 
    	//調用該方法創建new一個DecorView 
        mDecor = generateDecor(); 
    }else { 
        mDecor.setWindow(this); 
    } 
    if (mContentParent == null) { 
	    //根據主題theme設置對應的xml佈局文件以及Feature(包括style,layout,轉場動畫,屬性等)到DecorView中。 
	    //並將mContentParent綁定至id爲ID_ANDROID_CONTENT(com.android.internal.R.id.content)的ViewGroup 
	    //mContentParent在DecorView添加的xml文件中 
	    mContentParent = generateLayout(mDecor); 
	    ...     
    } 
} 

3、generateDecor()—創建DecorView

//PhoneWindow --> setContentView()  --> generateDecor() 
protected DecorView generateDecor(){
    return new DecorView(getContext(), -1);
}

4、generateLayout(mDecor);—創建mContentParent

//PhoneWindow --> setContentView()  -->generateLayout()  
 protected ViewGroup generateLayout(DecorView decor) { 
   //獲取當前的主題,加載默認資源和佈局 
    TypedArray a = getWindowStyle(); 
    ... 
    //根據theme的設定,找到對應的Feature(包括style,layout,轉場動畫,屬性等) 
    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) { 
        requestFeature(FEATURE_NO_TITLE);//無titleBar 
    }  
    ... 
    if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) { 
        setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));//設置全屏 
    } 
    if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,false)) { 
        setFlags(FLAG_TRANSLUCENT_STATUS, FLAG_TRANSLUCENT_STATUS & (~getForcedWindowFlags()));//透明狀態欄 
    } 
    //根據當前主題,設定不同的Feature 
    ... 
    int layoutResource; 
    int features = getLocalFeatures(); 
    ...
    if ((features & (1 << FEATURE_NO_TITLE)) == 0) { 
         layoutResource = R.layout.screen_title; 
    }  else if(){
...
} else {//無titleBar 
        layoutResource = R.layout.screen_simple; 
    } 
    mDecor.startChanging(); 
    //將佈局layout,添加至DecorView中 
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); 
    //從佈局中獲取`ID_ANDROID_CONTENT`,並關聯至contentParent 
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); 
    ... 
    //配置完成,DecorView根據已有屬性調整佈局狀態 
    mDecor.finishChanging(); 
    return contentParent; 
}

二、過程分析

從上面的分析我們知道,DecorView會根據每個Activity都是有自己資源ID形式的佈局。在填充資源layout時候,會根據不同的feature來選擇不同的佈局。
大概有如下幾種。

R.layout.screen_title_icons
R.layout.screen_progress
R.layout.screen_custom_title
R.layout.screen_action_bar
R.layout.screen_simple_overlay_action_mode;
R.layout.screen_simple

下面我們拿兩個佈局參考進行分析

  • screen_simple.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
              
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
         
</LinearLayout>
  • screen_simple_overlay_action_mode.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">
    
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
         
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
              
</FrameLayout>

無一例外, 無論看幾個都有一個id爲content 的 FrameLayout。ViewStub用於懶加載actionBar,而id爲@android:id/content的FrameLayout,此FrameLayout就是contentView。我們在Activity中調用setContentView方法,設置佈局,最終就是添加到該FrameLayout中。分析到這裏,整個Activity需要顯示的View創建好了,那麼如何顯示?

  • ActivityThread的handleResumeActivit
if (r.window == null && !a.mFinished && willBeVisible) {
	r.window = r.activity.getWindow();
	View decor = r.window.getDecorView();
	decor.setVisibility(View.INVISIBLE);
	ViewManager wm = a.getWindowManager();
	WindowManager.LayoutParams l = r.window.getAttributes();
	a.mDecor = decor;
	l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
	l.softInputMode |= forwardBit;
	if (a.mVisibleFromClient) {
	   a.mWindowAdded = true;
	   wm.addView(decor, l);
	}
}

我們可以看到DecorView通過wm添加到系統中顯示出來。到此,DecorView的分析就完了。看到這裏,不知道你有沒有一種豁然開朗或者有沒有一些想法?比如下面的代碼我們可以拿到視圖最頂級的View,也知道contentView 實際上是個FrameLayout,我們可以給他添加各種View,實現類似Dialog的邏輯,不過動畫要自己實現。這種做法是可以替代重量型的Dialog的,因爲Dialog使用崩潰概率會增加,維護難度會有所困難。目前大部分開源框架StatusBar的沉浸方案就是使用contentView 中add一個狀態欄的方案實現最終效果。更多的想法,需要自己去挖掘了~~

ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
ViewGroup contentView = activity.findViewById(android.R.id.content);        

3、View的繪製

由上面分析可知:id爲“content”的FrameLayout是我們的佈局文件加載顯示的區域,更確切地說是我們activity的setContentView()方法設置的視圖顯示的區域。他是個ViewGroup,是個ViewTree,它的繪製將依賴每個子View的繪製。Android中的任何一個佈局、任何一個控件包括我們自定義的控件其實都是直接或間接繼承自View實現的,所以說這些View應該都具有相同的繪製流程與機制才能顯示到屏幕上(可能每個控件的具體繪製邏輯有差異, 但是主流程都是一樣的)。每一個View的繪製過程都必須經歷三個最主要的過程,也就是measure()、layout()和draw()。那麼,整個Android的UI繪製機制是從哪裏開始的即入口在哪裏呢?答案就是ViewRootImpl類的performTraversals()方法。

回顧DecorView的添加流程

  • Activity的attach方法會構造一個PhoneWindow實例
  • 我們在onCreate裏通過setContentView將我們的xml添加到了DecorView
  • ActivityThread在後續過程中會將DecorView添加到Activity的窗口中,也就是添加到PhoneWindow
  • WindowManagerGlobal通過ViewRootImpl的setView方法將DecorView傳遞到ViewRootImpl進行繪製

我們從源碼的角度來看下setView方法

//遍歷的接口回調
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
//ViewRootImpl  ->  setView()
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
     synchronized (this) {
         if (mView == null) {
             mView = view;
             ……
             requestLayout();;
         }
     }
 }
 
 @Override
 public void requestLayout() {
     if (!mHandlingLayoutInLayoutRequest) {
         checkThread();
         mLayoutRequested = true;
         scheduleTraversals();
     }
 }
 
 //遍歷Activity的根佈局DecorView裏的每一個View。
 void scheduleTraversals() {
     if (!mTraversalScheduled) {
         mTraversalScheduled = true;
         mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
         mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
         if (!mUnbufferedInputDispatch) {
             scheduleConsumeBatchedInput();
         }
         notifyRendererOfFramePending();
         pokeDrawLockIfNeeded();
     }
 }

可以看到這裏post了一個mTraversalRunnable,我們看看這個runnable做了啥事

final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
//開始遍歷
void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }
        //正式繪製的入口
        performTraversals();
        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

performTraversals()

首先我們看這個方法名,perform是執行的意思,而Traversals是遍歷循環的意思;所以這個方法看方法名就知道他是在遍歷Activity的根佈局DecorView裏(或者其它窗口比如Dialog)的每一個View。

private void performTraversals() {
    ...
    if (!mStopped) {
        int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);  // 1
        int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
        performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); 
    } 
    if (didLayout) {
        performLayout(lp, desiredWindowWidth, desiredWindowHeight);
        ...
    }
    if (!cancelDraw && !newSurface) {
        if (!skipDraw || mReportNextDraw) {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).startChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            performDraw();
        }
    } 
    ...
}

我們看到它裏面主要執行了三個方法,分別是performMeasure、performLayout、performDraw這三個方法,在這三個方法內部又會分別調用measure、layout、draw這三個方法來進行不同的流程。我們先來看看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)這個方法,它傳入兩個參數,分別是childWidthMeasureSpec和childHeightMeasure,那麼這兩個參數代表什麼意思呢?要想了解這兩個參數的意思,我們就要先了解MeasureSpec。

MeasureSpec

官方文檔對MeasureSpec類的描述:A MeasureSpec encapsulates the layout requirements passed from parent to child. Each MeasureSpec represents a requirement for either the width or the height. A MeasureSpec is comprised of a size and a mode.它的意思就是說,該類封裝了一個View的規格尺寸,包括View的寬和高的信息。

要注意,MeasureSpec並不是指View的測量寬高,是根據MeasueSpec而測出測量寬高。在Measure流程中,系統會將View的LayoutParams根據父容器所施加的規則轉換成對應的MeasureSpec,然後在onMeasure方法中根據這個MeasureSpec來確定View的測量寬高。MeasureSpec代表一個32位的int值,高2位表示SpecMode,低30位表示SpecSize,而SpecSize是指在某種SpecMode下的規格大小。

SpecMode三種模式

  • UNSPECIFIED = 0 << MODE_SHIFT:即: 00000000 00000000 00000000 00000000 父容器不對子View有任何限制,子View要多大給多大,也就是說子View的大小可以超過父容器的大小,例如ListView、ScrollView。
  • EXACTLY =1<< MODE_SHIFT:即: 01000000 00000000 00000000 00000000父容器已經測量出子View所需要的固定大小,不會再變了,即MeasureSpec中封裝的SpecSize,對應於LayoutParams中的match_parent屬性和設置的固定dp值。
  • AT_MOST =2 << MODE_SHIFT:即: 10000000 00000000 00000000 00000000父窗口限定了一個最大值給子View即SpecSize,對應於LayoutParams中的wrap_content,自適應大小。
//用戶自定義View - > 獲取SpecSize和SpecMode
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //寬度的模式
    int mWidthModle = MeasureSpec.getMode(widthMeasureSpec);
    //寬度大小
    int mWidthSize = MeasureSpec.getSize(widthMeasureSpec);
    //高度的模式
    int mHeightModle = MeasureSpec.getMode(heightMeasureSpec);
    //高度大小
    int mHeightSize = MeasureSpec.getSize(heightMeasureSpec);
    //如果明確大小,直接設置大小
    if (mWidthModle == MeasureSpec.EXACTLY) {
        ...
    }
    ...
}

View的測量流程(Measure)

我們在使用View時是直接設置LayoutParams,但是在View測量的時候,系統會將LayoutParams在父容器的約束下進行相對應的MeasureSpec,然後在根據這個MeasureSpec來確定View的測量後的寬高,由此可見,MeasureSpec不是LayoutParams唯一決定的,還需要父容器一起來決定,在進一步決定View的寬高。但是頂級View,也就是上文我們分析到的DecorView和普通的View的MeasureSpec計算有些區別,對於DecorView,其MeasureSpec是由屏幕的尺寸和LayoutParams決定的,而DecorView的默認LayoutParams就是match_parent(在初始化DecorView時可知),對於普通View來說,其MeasureSpec是由父容器的MeasureSpec和自身的LayoutParams決定。在 performTraversals() 方法中有如下一段:

int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

我們再來看下getRootMeasureSpec方法的實現。

/**
 * @param windowSize  The available width or height of the window
 * @param rootDimension The layout params for one dimension (width or height) of the window.
 * 
 * @return The measure spec to use to measure the root view.
 */
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
	    case ViewGroup.LayoutParams.MATCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break;
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
        }
    }
    return measureSpec;
}

通過上述代碼,對於DecorView來說就是走第一個case,也就是屏幕的尺寸。對於普通View來說,也就是我們Activity顯示佈局的根View是一個ViewGroup,我們再來看下performMeasure方法。

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

方法很簡單,直接調用了mView.measure,這裏的mView就是DecorView,也就是說,從頂級View開始了測量流程,那麼我們直接進入measure流程。

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
	boolean optical = isLayoutModeOptical(this);
	if (optical != isLayoutModeOptical(mParent)) {
		...
		if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT ||
		    widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) {
			...
			if (cacheIndex < 0 || sIgnoreMeasureCache) {
			   // measure ourselves, this should set the measured dimension flag back
			   onMeasure(widthMeasureSpec, heightMeasureSpec);
			   mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
			} 
			...
		}
	}
}

可以看到,它在內部調用了onMeasure方法,由於DecorView是FrameLayout子類,因此它實際上調用的是DecorView#onMeasure方法。在該方法內部,主要是進行了一些判斷,這裏不展開來看了,到最後會調用到super.onMeasure方法,即FrameLayout#onMeasure方法。由於不同的ViewGroup有着不同的性質,那麼它們的onMeasure必然是不同的,因此這裏選擇了FrameLayout的onMeasure方法來進行分析。

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //獲取當前佈局內的子View數量
    int count = getChildCount();
    //判斷當前佈局的寬高是否是match_parent模式或者指定一個精確的大小,如果是則置measureMatchParent爲false.
    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();
    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    //遍歷所有類型不爲GONE的子View
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //對每一個子View進行測量
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //尋找子View中寬高的最大者,因爲如果FrameLayout是wrap_content屬性,那麼它的大小取決於子View中的最大者
            maxWidth = Math.max(maxWidth, child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight, child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            //如果FrameLayout是wrap_content模式,那麼往mMatchParentChildren中添加
            //寬或者高爲match_parent的子View,因爲該子View的最終測量大小會受到FrameLayout的最終測量大小影響
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }
    // 最大最小寬高還要加上padding的值
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
    // 檢查我們的最小高度和寬度
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
    // 檢查我們前景的最小高度和寬度
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }
    //保存測量結果
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    //子View中設置爲match_parent的個數
    count = mMatchParentChildren.size();
    //只有FrameLayout的模式爲wrap_content的時候纔會執行下列語句
    if (count > 1) {
        for (int i = 0; i < count; i++) {
            final View child = mMatchParentChildren.get(i);
            final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
            //對FrameLayout的寬度規格設置,因爲這會影響子View的測量
            final int childWidthMeasureSpec;
            /**
              * 如果子View的寬度是match_parent屬性,那麼對當前FrameLayout的MeasureSpec修改:
              * 把widthMeasureSpec的寬度規格修改爲:總寬度 - padding - margin,這樣做的意思是:
              * 對於子View來說,如果要match_parent,那麼它可以覆蓋的範圍是FrameLayout的測量寬度
              * 減去padding和margin後剩下的空間。
              * 以下兩點的結論,可以查看getChildMeasureSpec()方法:
              * 如果子View的寬度是一個確定的值,比如50dp,那麼FrameLayout的widthMeasureSpec的寬度規格修改爲:
              * SpecSize爲子View的寬度,即50dp,SpecMode爲EXACTLY模式
              * 如果子View的寬度是wrap_content屬性,那麼FrameLayout的widthMeasureSpec的寬度規格修改爲:
              * SpecSize爲子View的寬度減去padding減去margin,SpecMode爲AT_MOST模式
              */
            if (lp.width == LayoutParams.MATCH_PARENT) {
                final int width = Math.max(0, getMeasuredWidth()
                        - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
                        - lp.leftMargin - lp.rightMargin);
                childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                        width, MeasureSpec.EXACTLY);
            } else {
                childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                        getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
                        lp.leftMargin + lp.rightMargin,
                        lp.width);
            }
            //同理對高度進行相同的處理,這裏省略...
            //對於這部分的子View需要重新進行measure過程
            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
        }
    }
}

這裏簡單總結一下:首先,FrameLayout根據它的MeasureSpec來對每一個子View進行測量,即調用measureChildWithMargin方法。對於每一個測量完成的子View,會尋找其中最大的寬高,那麼FrameLayout的測量寬高會受到這個子View的最大寬高的影響(wrap_content模式),接着調用setMeasureDimension方法,把FrameLayout的測量寬高保存。最後則是特殊情況的處理,即當FrameLayout爲wrap_content屬性時,如果其子View是match_parent屬性的話,則要重新設置FrameLayout的測量規格,然後重新對該部分View測量。最後執行代碼:child.measure方法,然後在measure方法,會調用onMeasure方法。我們自定義View需要操作的就是這個onMeasure。到此,繪製流程已經從ViewGroup轉移到子View中了,具體的測量過程大同小異,讀者自定查看源碼。
最後簡單概括一下整個流程:測量始於DecorView,通過不斷的遍歷子View的measure方法,根據ViewGroup的MeasureSpec及子View的LayoutParams來決定子View的MeasureSpec,進一步獲取子View的測量寬高,然後逐層返回,不斷保存ViewGroup的測量寬高。

View的佈局流程(Layout)

前面提到,三大流程始於ViewRootImpl#performTraversals方法,在該方法內通過調用performMeasure、performLayout、performDraw這三個方法來進行measure、layout、draw流程,那麼我們就從performLayout方法開始說,我們先看它的源碼。

ViewRootImpl --> performLayout

private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    mLayoutRequested = false;
    mScrollMayChange = true;
    mInLayout = true;
    final View host = mView;
    if (DEBUG_ORIENTATION || DEBUG_LAYOUT) {
        Log.v(TAG, "Laying out " + host + " to (" +host.getMeasuredWidth() + ", " + host.getMeasuredHeight() + ")");
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "layout");
    try {
    	//DecorView開始layout,從左上角(0,0)座標開始
        host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); 
        //省略...
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
    mInLayout = false;
}

由上面的代碼可以看出,直接調用了host.layout方法,host也就是DecorView,屬於FrameLayout,那麼對於DecorView來說,調用layout方法,就是對它自身進行佈局,最終確定自身的位置以及子View(如果有)的位置。顯然,DecorView的左上位置爲0,然後寬高爲它的測量寬高。DecorView繼承View,最終走的是View的layout邏輯。

View --> layout

/**
 * 通過這個方法爲View及其所有的子View分配位置
 * 派生類不應該重寫這個方法,而應該重寫onLayout方法
 * 並且應該在重寫的onLayout方法中完成對子View的佈局
 * @param l Left position, relative to parent
 * @param t Top position, relative to parent
 * @param r Right position, relative to parent
 * @param b Bottom position, relative to parent
 */
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;
    // ① 通過setOpticalFrame或setFrame爲View設置座標,並判斷位置是否發生改變
    boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    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;
        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);
            }
        }
    }
    .......
}

①號代碼,調用了setFrame方法(setOpticalFrame最終也是調用setFrame),並把四個位置信息傳遞進去,這個方法用於確定View的四個頂點的位置,即初始化mLeft,mRight,mTop,mBottom這四個值,這個結束之後,DecorView就確定位置了,下一步要確定子View的位置了,也就是onLayout的邏輯。

View --> setFrame

protected boolean setFrame(int left, int top, int right, int bottom) {
    //省略...
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;
    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
    //省略...
    return changed;
}

可以看出,它對mLeft、mTop、mRight、mBottom這四個值進行了初始化,對於每一個View,包括ViewGroup來說,以上四個值保存了View的位置信息,所以這四個值是最終寬高,也即是說,如果要得到View的位置信息,那麼就應該在layout方法完成後調用getLeft()、getTop()等方法來取得最終寬高,如果是在此之前調用相應的方法,只能得到0的結果,所以一般我們是在onLayout方法中獲取View的寬高信息。

onLayout

View基類的layout方法和measure不同,並沒有使用final修飾,但註釋中也清清楚楚地寫着View的派生類不應該重寫這個方法,而應該重寫onLayout方法,來處理子View的佈局方式。如果子類是View則可以不用處理onLayout,交給父類處理即可。但是如果子類是ViewGroup,是必須重寫的onLayout方法中完成對子View的佈局邏輯,因爲ViewGroup中的onLayout是abstract修飾的。

  • View --> onLayout
/**
     * 當此視圖應爲其每個子視圖指定大小和位置時,從佈局調用
     * 帶有子級的派生類應重寫此方法並對其每個子級調用佈局。
     * @param changed This is a new size or position for this view
     * @param left Left position, relative to parent
     * @param top Top position, relative to parent
     * @param right Right position, relative to parent
     * @param bottom Bottom position, relative to parent
     */
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

當派生View集成的是View的話,onLayout是空方法,無需處理子View的佈局,因爲沒有子View。

  • ViewGroup --> onLayout
//abstract 修飾,讓派生類必須要重寫這個方法,在內部處理子View的佈局
@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

當派生View繼承的是ViewGroup的話,onLayout是abstract 方法,必須要重寫而且處理子View。由於不同的佈局容器的onMeasure方法均有不同的實現,因此不可能對所有佈局方式都說一次,另外上一篇文章是用FrameLayout#onMeasure進行講解的,那麼現在也對FrameLayout#onLayout方法進行講解。

//FrameLayout.java
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    //把父容器的位置參數傳遞進去
    layoutChildren(left, top, right, bottom, false /* no force left gravity */);
}

void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
    final int count = getChildCount();
    //以下四個值會影響到子View的佈局參數
    //parentLeft由父容器的padding和Foreground決定
    final int parentLeft = getPaddingLeftWithForeground();
    //parentRight由父容器的width和padding和Foreground決定
    final int parentRight = right - left - getPaddingRightWithForeground();
    final int parentTop = getPaddingTopWithForeground();
    final int parentBottom = bottom - top - getPaddingBottomWithForeground();
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (child.getVisibility() != GONE) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //獲取子View的測量寬高
            final int width = child.getMeasuredWidth();
            final int height = child.getMeasuredHeight();
            int childLeft;
            int childTop;
            int gravity = lp.gravity;
            if (gravity == -1) {
                gravity = DEFAULT_CHILD_GRAVITY;
            }
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
            //當子View設置了水平方向的layout_gravity屬性時,根據不同的屬性設置不同的childLeft
            //childLeft表示子View的 左上角座標X值
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                /* 水平居中,由於子View要在水平中間的位置顯示,因此,要先計算出以下:
                 * (parentRight - parentLeft -width)/2 此時得出的是父容器減去子View寬度後的
                 * 剩餘空間的一半,那麼再加上parentLeft後,就是子View初始左上角橫座標(此時正好位於中間位置),
                 * 假如子View還受到margin約束,由於leftMargin使子View右偏而rightMargin使子View左偏,所以最後
                 * 是 +leftMargin -rightMargin .
                 */
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
                    lp.leftMargin - lp.rightMargin;
                    break;
                //水平居右,子View左上角橫座標等於 parentRight 減去子View的測量寬度 減去 margin
                case Gravity.RIGHT:
                    if (!forceLeftGravity) {
                        childLeft = parentRight - width - lp.rightMargin;
                        break;
                    }
                //如果沒設置水平方向的layout_gravity,那麼它默認是水平居左
                //水平居左,子View的左上角橫座標等於 parentLeft 加上子View的magin值
                case Gravity.LEFT:
                default:
                    childLeft = parentLeft + lp.leftMargin;
            }
            //當子View設置了豎直方向的layout_gravity時,根據不同的屬性設置同的childTop
            //childTop表示子View的 左上角座標的Y值
            //分析方法同上
            switch (verticalGravity) {
                case Gravity.TOP:
                    childTop = parentTop + lp.topMargin;
                    break;
                case Gravity.CENTER_VERTICAL:
                    childTop = parentTop + (parentBottom - parentTop - height) / 2 +
                    lp.topMargin - lp.bottomMargin;
                    break;
                case Gravity.BOTTOM:
                    childTop = parentBottom - height - lp.bottomMargin;
                    break;
                default:
                    childTop = parentTop + lp.topMargin;
            }
            //對子元素進行佈局,左上角座標爲(childLeft,childTop),右下角座標爲(childLeft+width,childTop+height)
            child.layout(childLeft, childTop, childLeft + width, childTop + height);
        }
    }
}

先梳理一下以上邏輯:首先先獲取父容器的padding值,然後遍歷其每一個子View,根據子View的layout_gravity屬性、子View的測量寬高、父容器的padding值、來確定子View的佈局參數,然後調用child.layout方法,把佈局流程從父容器傳遞到子元素。那麼到目前爲止,View的佈局流程就已經全部分析完了。

View的繪製流程(Draw)

前面提到,三大工作流程始於ViewRootImpl#performTraversals,在這個方法內部會分別調用performMeasure,performLayout,performDraw三個方法來分別完成測量,佈局,繪製流程。那麼我們現在先從performDraw方法看起。

//ViewRootImpl.java
private void performDraw() {
	...
	final boolean fullRedrawNeeded = mFullRedrawNeeded;
	try {
		//判斷是否需要重新繪製全部視圖,如果是第一次繪製視圖,那麼顯然應該繪製所以的視圖
		//如果由於某些原因,導致了視圖重繪,那麼就沒有必要繪製所有視圖
	    draw(fullRedrawNeeded);
	} finally {
	    ...
	}
	...
}

private void draw(boolean fullRedrawNeeded) {
    ...
    //獲取mDirty,該值表示需要重繪的區域
    final Rect dirty = mDirty;
    ...
    //如果fullRedrawNeeded爲真,則把dirty區域置爲整個屏幕,表示整個視圖都需要繪製
    //第一次繪製流程,需要繪製所有視圖
    if (fullRedrawNeeded) {
        mAttachInfo.mIgnoreDirtyState = true;
        dirty.set(0, 0, (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    }
    ...
    //把相關參數傳遞進去,包括dirty區域
    if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
        return;
    }
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff, boolean scalingRequired, Rect dirty) {
    // 用軟件渲染器繪製。
    final Canvas canvas;
    try {
        final int left = dirty.left;
        final int top = dirty.top;
        final int right = dirty.right;
        final int bottom = dirty.bottom;
        //鎖定canvas區域,由dirty區域決定
        canvas = mSurface.lockCanvas(dirty);
        // The dirty rectangle can be modified by Surface.lockCanvas()
        //noinspection ConstantConditions
        if (left != dirty.left || top != dirty.top || right != dirty.right || bottom != dirty.bottom) {
            attachInfo.mIgnoreDirtyState = true;
        }
        canvas.setDensity(mDensity);
    } 
    try {
        if (!canvas.isOpaque() || yoff != 0 || xoff != 0) {
            canvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }
        dirty.setEmpty();
        mIsAnimating = false;
        attachInfo.mDrawingTime = SystemClock.uptimeMillis();
        mView.mPrivateFlags |= View.PFLAG_DRAWN;
        try {
            canvas.translate(-xoff, -yoff);
            if (mTranslator != null) {
                mTranslator.translateCanvas(canvas);
            }
            canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
            attachInfo.mSetIgnoreDirtyState = false;
            //正式開始繪製
            mView.draw(canvas);
        }
    } 
    return true;
}

首先是實例化了Canvas對象,然後鎖定該canvas的區域,由dirty區域決定,接着對canvas進行一系列的屬性賦值,最後調用了mView.draw(canvas)方法,前面分析過,mView就是DecorView,也就是說從DecorView開始繪製,前面所做的一切工作都是準備工作,而現在則是正式開始繪製流程。

View的繪製

由於ViewGroup沒有重寫draw方法,因此所有的View都是調用View -> draw方法,因此,我們直接看它的源碼:

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;
    /*
     * 繪製遍歷執行必須按適當順序執行的幾個繪製步驟
     *      1. 繪製背景
     *      2. 如有必要,保存畫布層以備褪色
     *      3. 繪圖視圖的內容
     *      4. 繪製子對象
     *      5. 如有必要,繪製淡入淡出的邊並恢復層
     *      6. 繪製裝飾(例如滾動條)
     */
    // 步驟1,如果需要,繪製背景
    int saveCount;
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    // 如果可能,跳過步驟2和5(常見情況)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // 第三步,畫出內容
        if (!dirtyOpaque) onDraw(canvas);
        // 第四步,畫子View
        dispatchDraw(canvas);
        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
        // 步驟6,繪製裝飾(前景,滾動條)
        onDrawForeground(canvas);
        // we're done...
        return;
    }
    ...
}

可以看到,draw過程比較複雜,但是邏輯十分清晰,而官方註釋也清楚地說明了每一步的做法。我們首先來看一開始的標記位dirtyOpaque,該標記位的作用是判斷當前View是否是透明的,如果View是透明的,那麼根據下面的邏輯可以看出,將不會執行一些步驟,比如繪製背景、繪製內容等。這樣很容易理解,因爲一個View既然是透明的,那就沒必要繪製它了。接着是繪製流程的六個步驟,這裏先小結這六個步驟分別是什麼,然後再展開來講。

繪製流程的六個步驟
1、對View的背景進行繪製
2、保存當前的圖層信息(可跳過)
3、繪製View的內容
4、對View的子View進行繪製(如果有子View)
5、繪製View的褪色的邊緣,類似於陰影效果(可跳過)
6、繪製View的裝飾(例如:滾動條)

其中第2步和第5步是可以跳過的,我們這裏不做分析,我們重點來分析其它步驟。

Step 1 :繪製背景
private void drawBackground(Canvas canvas) {
    //mBackground是該View的背景參數,比如背景顏色
    final Drawable background = mBackground;
    if (background == null) {
        return;
    }
    //根據View四個佈局參數來確定背景的邊界
    setBackgroundBounds();
    // 硬件加速繪製
    ...
    //獲取當前View的mScrollX和mScrollY值
    final int scrollX = mScrollX;
    final int scrollY = mScrollY;
    if ((scrollX | scrollY) == 0) {
        background.draw(canvas);
    } else {
        //如果scrollX和scrollY有值,則對canvas的座標進行偏移,再繪製背景
        canvas.translate(scrollX, scrollY);
        background.draw(canvas);
        canvas.translate(-scrollX, -scrollY);
    }
}

可以看出,這裏考慮到了view的偏移參數,scrollX和scrollY,繪製背景在偏移後的view中繪製。

Step 3:繪製內容
/**
*  執行此操作來繪製圖形。
* @param canvas 將在其上繪製背景的畫布
*/
protected void onDraw(Canvas canvas) {
}

這裏調用了View -> onDraw方法,View中該方法是一個空實現,因爲不同的View有着不同的內容,這需要我們自己去實現,即在自定義View中重寫該方法來實現。

Step 4: 繪製子View
//View
/**
 * 由draw調用以繪製子視圖。這可能被派生類重寫,
 * 以便在繪製其子級之前(但在繪製其自己的視圖之後)獲得控制權。
 * @param canvas 將在其上繪製背景的畫布
 */
protected void dispatchDraw(Canvas canvas) {
}

View中該方法是空實現,因爲他沒有子視圖,而在ViewGroup中重寫了這個方法,下面我們來看看。

//ViewGroup
protected void dispatchDraw(Canvas canvas) {
    boolean usingRenderNodeProperties = canvas.isRecordingFor(mRenderNode);
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    int flags = mGroupFlags;
    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) {
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }
        int childIndex = customOrder ? getChildDrawingOrder(childrenCount, i) : i;
        final View child = (preorderedList == null)
                ? children[childIndex] : preorderedList.get(childIndex);
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ..
}

這裏簡單說明一下,裏面主要遍歷了所以子View,每個子View都調用了drawChild這個方法,我們找到這個方法。

//View -> drawChild
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
     return child.draw(canvas, this, drawingTime);
}
//View -> draw
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
   ...
    if (!drawingWithDrawingCache) {
        if (drawingWithRenderNode) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
        } else {
            // Fast path for layouts with no backgrounds
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchDraw(canvas);
            } else {
                draw(canvas);
            }
        }
    } else if (cache != null) {
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        if (layerType == LAYER_TYPE_NONE) {
            // no layer paint, use temporary paint to draw bitmap
            Paint cachePaint = parent.mCachePaint;
            if (cachePaint == null) {
                cachePaint = new Paint();
                cachePaint.setDither(false);
                parent.mCachePaint = cachePaint;
            }
            cachePaint.setAlpha((int) (alpha * 255));
            canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
        } else {
            // use layer paint to draw the bitmap, merging the two alphas, but also restore
            int layerPaintAlpha = mLayerPaint.getAlpha();
            mLayerPaint.setAlpha((int) (alpha * layerPaintAlpha));
            canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            mLayerPaint.setAlpha(layerPaintAlpha);
        }
    }
}

我們主要來看核心部分,首先判斷是否已經有緩存,即之前是否已經繪製過一次了,如果沒有,則會調用draw(canvas)方法,開始正常的繪製,即上面所說的六個步驟,否則利用緩存來顯示。這一步也可以歸納爲ViewGroup繪製過程,它對子View進行了繪製,而子View又會調用自身的draw方法來繪製自身,這樣不斷遍歷子View及子View的不斷對自身的繪製,從而使得View樹完成繪製。

Step 6: 繪製裝飾

所謂的繪製裝飾,就是指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);
    }
}

和一般的繪製流程非常相似,都是先設定繪製區域,然後利用canvas進行繪製。那麼,到目前爲止,View的繪製流程也講述完畢了,希望這篇文章對你們起到幫助作用,謝謝你們的閱讀。

結尾

有句話說的不錯,好記憶不如爛筆頭。Android學習過程中,最好的方式就是記錄,記多了看多了不知不覺就成專家了。而寫博客就是幫自己梳理知識點的最好的方式,你可以嘗試去多讀幾篇類似的文章,然後自己梳理記下來,寫出自己的博客,你會受益匪淺而且極易深刻,不信你試試!!共勉吧~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章