對於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學習過程中,最好的方式就是記錄,記多了看多了不知不覺就成專家了。而寫博客就是幫自己梳理知識點的最好的方式,你可以嘗試去多讀幾篇類似的文章,然後自己梳理記下來,寫出自己的博客,你會受益匪淺而且極易深刻,不信你試試!!共勉吧~