平時在開發(fā)安卓的過程中,View是我們用的非常非常多的東西.用戶所看到的一切關于UI的,都是通過View繪制出來展示到屏幕上的.大多數情況下我們僅僅了解基本控件的使用方法,我們是無法做出非常復雜炫酷的自定義View的.我們需要掌握View的工作原理:測量、布局闹炉、繪制流程,掌握了這幾個基本的流程我們才能做出更加完美的自定義View.做起來也更加得心應手.當然,View的工作原理,也是大多數面試所必問的知識點.要想了解工作原理,就只能read the fucking code.
/***
* ,s555SB@@&
* :9H####@@@@@Xi
* 1@@@@@@@@@@@@@@8
* ,8@@@@@@@@@B@@@@@@8
* :B@@@@X3hi8Bs;B@@@@@Ah,
* ,8i r@@@B: 1S ,M@@@@@@#8;
* 1AB35.i: X@@8 . SGhr ,A@@@@@@@@S
* 1@h31MX8 18Hhh3i .i3r ,A@@@@@@@@@5
* ;@&i,58r5 rGSS: :B@@@@@@@@@@A
* 1#i . 9i hX. .: .5@@@@@@@@@@@1
* sG1, ,G53s. 9#Xi;hS5 3B@@@@@@@B1
* .h8h.,A@@@MXSs, #@H1: 3ssSSX@1
* s ,@@@@@@@@@@@@Xhi, r#@@X1s9M8 .GA981
* ,. rS8H#@@@@@@@@@@#HG51;. .h31i;9@r .8@@@@BS;i;
* .19AXXXAB@@@@@@@@@@@@@@#MHXG893hrX#XGGXM@@@@@@@@@@MS
* s@@MM@@@hsX#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@&,
* :GB@#3G@@Brs ,1GM@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@B,
* .hM@@@#@@#MX 51 r;iSGAM@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@8
* :3B@@@@@@@@@@@&9@h :Gs .;sSXH@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@:
* s&HA#@@@@@@@@@@@@@@M89A;.8S. ,r3@@@@@@@@@@@@@@@@@@@@@@@@@@@r
* ,13B@@@@@@@@@@@@@@@@@@@5 5B3 ;. ;@@@@@@@@@@@@@@@@@@@@@@@@@@@i
* 5#@@#&@@@@@@@@@@@@@@@@@@9 .39: ;@@@@@@@@@@@@@@@@@@@@@@@@@@@;
* 9@@@X:MM@@@@@@@@@@@@@@@#; ;31. H@@@@@@@@@@@@@@@@@@@@@@@@@@:
* SH#@B9.rM@@@@@@@@@@@@@B :. 3@@@@@@@@@@@@@@@@@@@@@@@@@@5
* ,:. 9@@@@@@@@@@@#HB5 .M@@@@@@@@@@@@@@@@@@@@@@@@@B
* ,ssirhSM@&1;i19911i,. s@@@@@@@@@@@@@@@@@@@@@@@@@@S
* ,,,rHAri1h1rh&@#353Sh: 8@@@@@@@@@@@@@@@@@@@@@@@@@#:
* .A3hH@#5S553&@@#h i:i9S #@@@@@@@@@@@@@@@@@@@@@@@@@A.
*
*
* 又看源碼煌妈,看你妹呀影钉!
*/
1. View是從什么時候開始繪制的?
1.1 先簡單來個demo
新建一個自定義View,名字叫MyView,分別在MyView的onMeasure(),onLayout(),onDraw()打上log.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
Log.e(TAG, "onMeasure: ---MyView");
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
Log.e(TAG, "onLayout: ---MyView");
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.e(TAG, "onDraw: ---MyView");
}
然后在Activity的布局中添加這個MyView,并在Activity的onCreate(),onStart(),onResume()打上log.
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e(TAG, "onCreate: ");
}
@Override
protected void onStart() {
super.onStart();
Log.e(TAG, "onStart: ");
}
@Override
protected void onResume() {
super.onResume();
Log.e(TAG, "onResume: ");
}
運行demo,看一下log:
05-13 22:35:37.130 17346-17346/com.xfhy.demo E/xfhy: onCreate:
05-13 22:35:37.130 17346-17346/com.xfhy.demo E/xfhy: onStart:
05-13 22:35:37.130 17346-17346/com.xfhy.demo E/xfhy: onResume:
05-13 22:35:37.155 17346-17346/com.xfhy.demo E/xfhy: onMeasure: ---MyView
05-13 22:35:37.295 17346-17346/com.xfhy.demo E/xfhy: onLayout: ---MyView
05-13 22:35:37.315 17346-17346/com.xfhy.demo E/xfhy: onMeasure: ---MyView
05-13 22:35:37.315 17346-17346/com.xfhy.demo E/xfhy: onLayout: ---MyView
05-13 22:35:37.315 17346-17346/com.xfhy.demo E/xfhy: onDraw: ---MyView
好了,我們從log中就可以看出,View的繪制其實是從Activity的onResume()之后才開始的.
1.2 從源碼層看看工作過程
在Activity的啟動過程中,我們知道,在最后那里,ActivityThread中的handleLaunchActivity帝簇,performLaunchActivity独令,handleResumeActivity,這3個主要的方法完成了Activity的創(chuàng)建到啟動工作.
ps: 如果有不清楚的在網上查閱一下資料,這里推薦剛哥的書籍:安卓開發(fā)藝術探索 第九章
1.2.1 handleLaunchActivity()
下面看一下handleLaunchActivity()方法:
private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
if (localLOGV) Slog.v(
TAG, "Handling launch of " + r);
//分析1 : 這里是創(chuàng)建Activity,并調用了Activity的onCreate()和onStart()
Activity a = performLaunchActivity(r, customIntent);
if (a != null) {
r.createdConfig = new Configuration(mConfiguration);
Bundle oldState = r.state;
//分析2 : 這里調用Activity的onResume()
handleResumeActivity(r.token, false, r.isForward,
!r.activity.mFinished && !r.startsNotResumed);
}
....
}
handleLaunchActivity()主要是為了調用performLaunchActivity()和handleResumeActivity()
1.2.2 performLaunchActivity()
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
......
//分析1 : 這里底層是通過反射來創(chuàng)建的Activity實例
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
//底層也是通過反射構建Application,如果已經構建則不會重復構建,畢竟一個進程只能有一個Application
Application app = r.packageInfo.makeApplication(false, mInstrumentation);
if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
//分析2 : 在這里實例化了PhoneWindow,并將該Activity設置為PhoneWindow的Callback回調,還初始化了WindowManager
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config);
//分析3 : 間接調用了Activity的performCreate方法,間接調用了Activity的onCreate方法.
mInstrumentation.callActivityOnCreate(activity, r.state);
//分析4: 這里和上面onCreate過程差不多,調用Activity的onStart方法
if (!r.activity.mFinished) {
activity.performStart();
r.stopped = false;
}
....
}
}
主要過程:
- 通過反射來創(chuàng)建的Activity實例
- 在這里實例化了PhoneWindow,并將該Activity設置為PhoneWindow的Callback回調.建立起Activity與PhoneWindow之間的聯系.
- 調用了Activity的onCreate方法
- 調用Activity的onStart方法
對于分析1中的代碼:
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
//反射->實例化
return (Activity)cl.loadClass(className).newInstance();
}
對于分析2中的代碼:
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
......
//實例化PhoneWindow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
.....
//還有一些其他的配置代碼
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
mWindowManager = mWindow.getWindowManager();
....
}
對于分析3中的代碼:
//首先是來到Instrumentation的callActivityOnCreate方法
public void callActivityOnCreate(Activity activity, Bundle icicle) {
prePerformCreate(activity);
activity.performCreate(icicle);
postPerformCreate(activity);
}
//然后就來到Activity的performCreate方法
final void performCreate(Bundle icicle) {
performCreate(icicle, null);
}
final void performCreate(Bundle icicle, PersistableBundle persistentState) {
....
if (persistentState != null) {
onCreate(icicle, persistentState);
} else {
onCreate(icicle);
}
......
}
對于分析4中的代碼:
//Activity
final void performStart() {
......
mInstrumentation.callActivityOnStart(this);
......
}
//Instrumentation
public void callActivityOnStart(Activity activity) {
activity.onStart();
}
1.2.3 handleResumeActivity()
final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
.....
//分析1 : 在其內部調用Activity的onResume方法
r = performResumeActivity(token, clearHide, reason);
.....
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
//獲取WindowManager
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
if (a.mVisibleFromClient) {
.....
//分析2 : WindowManager添加DecorView
wm.addView(decor, l);
...
}
.....
}
細看分析2中的邏輯:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}
我們看到,方法里面將邏輯交給了mGlobal,mGlobal是WindowManagerGlobal,WindowManagerGlobal是全局單例.WindowManagerImpl的方法都是由WindowManagerGlobal完成的.我們跟著來到了WindowManagerGlobal的addView方法.
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
....
ViewRootImpl root;
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
.....
root.setView(view, wparams, panelParentView);
}
}
在這里我們看到了,實例化ViewRootImpl,然后建立ViewRootImpl與View的聯系.跟著進入setView方法
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
.....
requestLayout();
.....
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
//檢查線程合法性
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
......
}
}
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
void doTraversal() {
if (mTraversalScheduled) {
....
performTraversals();
....
}
}
一路下來,我們來到了熟悉的方法面前:performTraversals方法.
1.2.4 performTraversals()
performTraversals()方法相信大家都已經非常熟悉啦,它是整個View繪制的核心,從measure到layout,再從layout到draw,全部在這個方法里面完成了,所以這個方法里面的代碼非常長,這是肯定的.由于本人水平有限,就不每句代碼逐行分析了,我們需要學習的是一個主要的流程.
下面的performTraversals()方法的超精簡代碼,里面的代碼真的超級超級多,下面是主要流程,也是今天的主角
private void performTraversals() {
//分析1 : 這里面會調用performMeasure開始測量流程
measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
//分析2 : 開始布局流程
performLayout(lp, mWidth, mHeight);
//分析3 : 開始繪畫流程
performDraw();
}
首先,來個主要的流程圖,這個是performTraversals()的大致流程.
如上面的代碼所示,performTraversals()會依次調用performMeasure赶促、performLayout、performDraw方法,而這三個方法是View的繪制流程的核心所在.
- performMeasure : 在performMeasure里面會調用measure方法,然后measure會調用onMeasure方法,而在onMeasure方法中則會對所有的子元素進行measure過程.這相當于完成了一次從父元素到子元素的measure傳遞過程,如果子元素是一個ViewGroup,那么繼續(xù)向下傳遞,直到所有的View都已測量完成.測量完成之后,我們可以根據getMeasureHeight和getMeasureWidth方法獲取該View的高度和寬度.
- performLayout : performLayout的原理其實是和performMeasure差不多,在performLayout里面調用了layout方法,然后在layout方法會調用onLayout方法,onLayout又會對所有子元素進行l(wèi)ayout過程.由父元素向子元素傳遞,最終完成所有View的layout過程.確定View的4個點: left+top+right+bottom,layout完成之后可以通過getWidth和getHeight獲取View的最終寬高.
- performDraw : 也是和performMeasure差不多,從父元素從子元素傳遞.在performDraw里面會調用draw方法,draw方法再調用drawSoftware方法,drawSoftware方法里面回調用View的draw方法,然后再通過dispatchDraw方法分發(fā),遍歷所有子元素的draw方法,draw事件就這樣一層層地傳遞下去.
2. View 測量流程
2.1 MeasureSpec
在開始進行理解View的測量流程之前,需要先理解MeasureSpec.
MeasureSpec代表的是32位的int值,它的高2位是SpecMode(也是一個int),低30位是SpecSize(也是一個int),SpecMode是測量模式,SpecSize是測量大小. MeasureSpec相當于是兩者的結合. 系統(tǒng)封裝了如何從MeasureSpec中提取SpecMode和SpecSize,也封裝了用SpecMode和SpecSize組合成MeasureSpec.
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size,int mode) {
if (sUseBrokenMakeMeasureSpec) {
return size + mode;
} else {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
SpecMode有3種,分別是
-
UNSPECIFIED
父容器對View不會有任何限制,要多大給多大,一般是用在系統(tǒng)內部使用,我們開發(fā)的APP用不到. -
EXACTLY
這種情況對應于match_parent
和具體數值這兩種模式,父容器已經檢測出View需要的精確大小. -
AT_MOST
這種情況對應于wrap_content
,父容器指定了一個最大值,View不能超過這個值.
一般來說,View的MeasureSpec由父容器的MeasureSpec和自己的LayoutParams共同決定.因為有了MeasureSpec才可以在onMeasure中確定測量寬高.
下面是從源碼中提取出來的摘要信息,后面會詳細看源碼分析,這里先提取出來
- 如果View的寬高是固定的值,那么不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY
- 如果View的寬高是
wrap_content
,那么不管父容器的MeasureSpec是EXACTLY還是AT_MOST
,最終View的MeasureSpec都是AT_MOST
,這里暫時不用管UNSPECIFIED(我們用不到).而且View最終的大小不能超過父容器的剩余空間 - 如果View的寬高是
match_parent
,那么要分兩種情況- 如果父容器是EXACTLY,那么View就是EXACTLY
- 如果父容器是
AT_MOST
,那么View也是AT_MOST
.
這里不得不引用一張剛哥書籍里面的經典表格來表示一下:
ps:這里必須要強烈推薦一波剛哥的安卓開發(fā)藝術探索這本書.沒看過這本書,都不敢說自己是學安卓的.去年我作為萌新買了這本書看了一遍,覺得受益匪淺.今年打算再看一遍,重新梳理一下.好書是值得反復推敲琢磨的.
2.2 從performMeasure()開始View測量
我們從ViewRootImpl的performTraversals()開始著手,仔細觀察
private void performTraversals() {
//host為根視圖,即DecorView
//desiredWindowWidth是Window的寬度
measureHierarchy(host, lp, res,
desiredWindowWidth, desiredWindowHeight);
}
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp,
final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) {
//分析1 : desiredWindowWidth就是Window的寬度,desiredWindowHeight是Window的高度
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
//分析2 : 開始測量流程
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
measure是從上往下執(zhí)行的,widthMeasureSpec和heightMeasureSpec通常情況下是由父容器傳遞給子視圖的.但是最外層的根視圖,怎么拿到MeasureSpec呢? 在執(zhí)行performMeasure方法之前,我們需要拿到最外層的視圖的MeasureSpec,看代碼
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
//如果是MATCH_PARENT,那么就是EXACTLY
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window can't resize. Force root view to be windowSize.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
//如果是WRAP_CONTENT,就是AT_MOST
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Window can resize. Set max size for root view.
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
//如果是固定的值,也是EXACTLY
// Window wants to be an exact size. Force root view to be that size.
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
最外層的根視圖的MeasureSpec只由自己的LayoutParams決定,做自己的主人,舒服.
既然我們根視圖拿到了MeasureSpec,接下來就要拿自己的MeasureSpec教孩子做人了.
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
//調用根視圖的measure方法,開始測量流程
mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
這里的mView就是DecorView.DecorView是一個FrameLayout,而FrameLayout是一個ViewGroup,而ViewGroup是一個View,這個measure方法就是在View里面的. 因為measure是一個final方法,哈哈,所以子類不能覆寫它.
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
......
//調用onMeasure方法
onMeasure(widthMeasureSpec, heightMeasureSpec);
......
}
measure方法里面有一些檢測是否需要重新onMeasure的代碼,被我略去了.
onMeasure是View里面的方法,ViewGroup是一個抽象類并且沒有重寫onMeasure.因為onMeasure方法的實現,每個都是不一樣的,比如LinearLayout和FrameLayout的onMeasure方法肯定是實現邏輯不一樣的.
因為DecorView是FrameLayout,所以我們看看FrameLayout中的onMeasure.
FrameLayout->onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
//分析1 : 遍歷所有子控件,測量每個子控件的大小
//參數1:View控件
//參數2:寬MeasureSpec
//參數3:父容器在寬度上已經用了多少了,因為FrameLayout的規(guī)則是:前面已經放置的View并不會影響后面放置View的寬高,是直接覆蓋到上一個View上的.所以這里傳0
//參數4:高MeasureSpec
//參數5:父容器在高度上已經用了多少了
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
}
}
......
//分析2 : 測量完所有的子控件的大小之后,才知道自己的大小 這很符合FrameLayout的規(guī)則嘛
setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
resolveSizeAndState(maxHeight, heightMeasureSpec,
childState << MEASURED_HEIGHT_STATE_SHIFT));
......
}
FrameLayout的onMeasure方法中會遍歷所有子控件,然后進行所有子控件的大小測量.最后才來設置自己的大小.注意,onMeasure方法的入參MeasureSpec是從父容器傳過來的,意思就是給你個參考,你自己看著辦吧.
在測量子控件大小的時候會調用ViewGroup的measureChildWithMargins方法,下面是代碼:
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
//獲取子控件的LayoutParams
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//分析1: 計算子控件在寬上的MeasureSpec
//參數1:父容器的MeasureSpec
//參數2:這里官方的入參名稱是padding,從下面這個傳值的形式來看,顯然是子控件在寬上不能利用的空間(ViewGroup的左右兩邊padding+子控件的左右margin+父容器在寬度上已經使用了并且不能再使用的空間)
//參數3:子控件想要的寬度
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
//分析2: 將measure過程傳遞給子控件 如果子控件又是一個ViewGroup,那么繼續(xù)向下傳遞
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在measureChildWithMargins方法里我們首先是看到根據子控件的LayoutParams和父容器的MeasureSpec計算子控件的MeasureSpec,然后將計算出的MeasureSpec通過子控件的measure方法傳遞下去.如果子控件又是一個ViewGroup,那么它又會重復的measure流程,一直向下傳遞這個過程,直接最后的那個是View為止.因為View沒有子控件,它就不能向下傳遞了.
所以我們自定義View(這里指那種直接繼承自View)的時候,在onMeasure方法里面,需要根據自身的LayoutParams+父容器的MeasureSpec來計算SpecSize和SpecMode,最后根據業(yè)務場景來確定自己的大小(調用setMeasuredDimension來確定大小).
注意了,接下來的getChildMeasureSpec方法就比較重要了
//這里來自ViewGroup的getChildMeasureSpec方法,無刪減
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
//根據父容器的MeasureSpec獲取父容器的SpecMode和SpecSize
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
//剩下的size
int size = Math.max(0, specSize - padding);
//最終的size和mode
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
// Parent has imposed an exact size on us
//父容器有一個確定的大小
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
//子控件也是確定的大小,那么最終的大小就是子控件設置的大小,SpecMode為EXACTLY
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size. So be it.
// 子控件想要占滿剩余的空間,那么就給它吧.
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
//子控件想要自己定義大小,但是不能超過剩余空間 size
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent has imposed a maximum size on us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size, but our size is not fixed.
// Constrain child to not be bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size. It can't be
// bigger than us.
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
// Parent asked to see how big we want to be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// Child wants to determine its own size.... find out how
// big it should be
resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
//noinspection ResourceType
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
這段代碼對應著下面這段總結
- 如果View的寬高是固定的值,那么不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY
- 如果View的寬高是
wrap_content
,那么不管父容器的MeasureSpec是EXACTLY還是AT_MOST
,最終View的MeasureSpec都是AT_MOST
,這里暫時不用管UNSPECIFIED(我們用不到).而且View最終的大小不能超過父容器的剩余空間 - 如果View的寬高是
match_parent
,那么要分兩種情況- 如果父容器是EXACTLY,那么View就是EXACTLY
- 如果父容器是
AT_MOST
,那么View也是AT_MOST
.
這段代碼對應著上面剛哥總結的那個表格.同時也是measure流程的核心內容.
因為在measureChildWithMargins方法里我們已經計算出子控件的MeasureSpec,然后通過measure傳遞給子控件了,如果子控件又是一個ViewGroup,那么它又會重復的measure流程,一直向下傳遞這個過程,直接最后的那個是View為止.因為View沒有子控件,它就不能向下傳遞了.到這里其實我們的View的measure流程已經走完了,哈哈,不知不覺.
下面簡單畫一個流程圖,方便理解上面的流程.
2.3 measure小結
從ViewRootImpl的performTraversals方法開始進入View的繪制過程,performTraversals方法里面會有一個performMeasure方法.這個performMeasure方法是專門拿來測量View的大小的.而且會遍歷整個View樹,全部進行測量.
在performMeasure里面會調用measure方法,然后measure會調用onMeasure方法,而在onMeasure方法中則會對所有的子元素進行measure過程.這相當于完成了一次從父元素到子元素的measure傳遞過程,如果子元素是一個ViewGroup,那么繼續(xù)向下傳遞,直到所有的View都已測量完成.測量完成之后,我們可以根據getMeasureHeight和getMeasureWidth方法獲取該View的高度和寬度.
3. View 布局流程
3.1 從performLayout開始布局
我們還是從ViewRootImpl的performTraversals()開始著手
private void performTraversals() {
....
//開始布局流程
performLayout(lp, mWidth, mHeight);
.....
}
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
int desiredWindowHeight) {
.....
//這里的host其實是根視圖(DecorView)
//參數:left,top,right,bottom 這些位置都是相對于父容器而言的
host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
.....
}
在performLayout方法里面調用了DecorView的layout方法,然后我發(fā)現:layout方法其實是View這個父類里面的,然后ViewGroup繼承了View之后重寫了一下,只是調了一下super.layout(l, t, r, b);
,相當于實現還是在View的layout里面.而且ViewGroup的layout方法是final修飾的,意味著子類不能再重寫這個方法了.
//以下是View的layout方法
public void layout(int l, int t, int r, int b) {
......
onLayout(changed, l, t, r, b);
......
}
layout方法其實就是調用onLayout方法,如果這里子控件是一個View的話,那么onLayout其實是空實現.onLayout在ViewGroup是一個抽象方法,如果是一個ViewGroup的話,比如FrameLayout,那么onLayout是需要自己實現的.
//View中的定義
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}
//ViewGroup中的定義,沒錯,這是抽象方法,具體的實現交由實現類去實現
@Override
protected abstract void onLayout(boolean changed, int l, int t, int r, int b);
因為我們的根視圖是DecorView,也就是FrameLayout,那么我們來看一下FrameLayout的onLayout實現:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//布局子控件 我沒看懂這個changed參數是拿來干什么的,好像并沒有用上(這里已經是FrameLayout的onLayout方法的全部代碼了)
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();
//最左側
final int parentLeft = getPaddingLeftWithForeground();
//最右側
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();
//因為已經measure流程走完了,所以這里是能通過getMeasuredWidth方法獲取測量寬度的
final int width = child.getMeasuredWidth();
final int height = child.getMeasuredHeight();
//實際子控件的left
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;
switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
//水平居中
case Gravity.CENTER_HORIZONTAL:
childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
lp.leftMargin - lp.rightMargin;
break;
case Gravity.RIGHT: //子控件在父容器的最右側
if (!forceLeftGravity) {
childLeft = parentRight - width - lp.rightMargin;
break;
}
case Gravity.LEFT: //子控件在父容器的最左側
default:
childLeft = parentLeft + lp.leftMargin;
}
//豎直方向上的gravity
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;
}
//最后給這個子控件一個最終的left,top,right,bottom值
//把這個子控件放在這里
child.layout(childLeft, childTop, childLeft + width, childTop + height);
}
}
}
不同的ViewGroup的實現類的onLayout方法實現是不一樣的,是根據自身情況來決定將子控件放在那里的,比如FrameLayout和LinearLayout的onLayout是不一樣的實現,但是onLayout這個方法最終是將各個子控件有條不紊的放在對應的位置上.
我們看到在onLayout方法的最后,調用了子控件的layout方法,其實就是將layout流程向下進行傳遞了.如果子控件還是ViewGroup的話,那么它又會對它自己所有的子控件進行布局,放置.最后一層一層的往下,直到全部都layout完成.每個View都知道自己的left,top,right,bottom.這個時候是可以通過View的getWidth和getHeight來獲取最終的寬高的.
下面的View的getWidth和getHeight方法的實現,可以看到,就是通過這四個位置來確定的寬高.
public final int getWidth() {
return mRight - mLeft;
}
public final int getHeight() {
return mBottom - mTop;
}
3.2 layout小結
layout主要是為了確定該控件以及其子控件的位置和大小.在performLayout中,主要是確定每個控件的left+top+right+bottom,performLayout之后它們的位置就已經被確定了,就只剩下最后一步繪制了.
4. View 繪制流程
還是從ViewRootImpl的performTraversals方法開始分析
private void performTraversals() {
//開始繪畫流程
performDraw();
}
private void performDraw() {
......
draw(fullRedrawNeeded);
......
}
private void draw(boolean fullRedrawNeeded){
.....
drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty);
.....
}
private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
boolean scalingRequired, Rect dirty) {
......
mView.draw(canvas);
......
}
隨著方法的調用深入,發(fā)現來到了View的draw方法
public void draw(Canvas canvas) {
.....
/*
注意了這是官方給的注釋,谷歌工程師還真是貼心,把draw步驟寫的詳詳細細,給力,點贊
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/
// Step 1, draw the background, if needed
//1. 繪制背景
if (!dirtyOpaque) {
drawBackground(canvas);
}
// skip step 2 & 5 if possible (common case)
final int viewFlags = mViewFlags;
boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
if (!verticalEdges && !horizontalEdges) {
// Step 3, draw the content
//3. 繪制自己的內容
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
//4. 繪制子控件 如果是View的話這個方法是空實現,如果是ViewGroup則繪制子控件
dispatchDraw(canvas);
drawAutofilledHighlight(canvas);
// Overlay is part of the content and draws beneath Foreground
if (mOverlay != null && !mOverlay.isEmpty()) {
mOverlay.getOverlayView().dispatchDraw(canvas);
}
// Step 6, draw decorations (foreground, scrollbars)
//6. 繪制裝飾和前景
onDrawForeground(canvas);
// Step 7, draw the default focus highlight
//7. 繪制默認焦點高亮顯示
drawDefaultFocusHighlight(canvas);
if (debugDraw()) {
debugDrawFocus(canvas);
}
// we're done...
return;
}
.....
}
注意到,谷歌工程師將draw的步驟完完全全的寫出來了的.還真是貼心啊.draw的基本步驟如下
- 繪制背景
- 繪制控件自己本身的內容
- 繪制子控件
- 繪制裝飾(比如滾動條)和前景
這里簡單提一下dispatchDraw方法,在這個方法里面會去調用drawChild方法,在drawChild里面會調用子控件的draw方法,這相當于完成了draw的傳遞過程,通知子控件去繪制它自己. 然后如果子控件是ViewGroup,它又會重復上面這個遞推.
draw的流程比測量和布局要簡單一些,但是需要注意的是,View繪制過程是通過dispatchDraw來傳遞的.
5. 結束語
寫一篇深入(可能只是對我來說)的文章真的好不容易,期間遇到了很多坑,也學到了很多.之前其實這部分是學過的.但是只有當自己去看源碼,一步步分析,輸出成文檔,才真正理解其中的原理,為什么代碼要這樣寫.
可能還是太菜了吧,寫這么一篇水文花了大概4天......