該文章是一個系列文章,是本人在Android開發的漫漫長途上的一點感想和記錄,若是能給各位看官帶來一絲啓發或者幫助,那真是極好的。android
終於到了咱們的豬腳ViewRootImpl出場的時候了。ViewRootImpl類比較複雜,若是要把這個類所有解釋清楚那須要不少章節,而且該類涉及了許多其餘知識,如Android進程間通訊的Binder了,還有其餘許多本文以及前文沒有講到的概念。因此咱們只分析其中的一部分。ide
public ViewRootImpl(Context context, Display display) { ... /**① 從WindowManagerGlobal 中獲取一個IWindowSession的實例。 *它是ViewRootImpl和WindowManagerService(如下簡稱WMS)進行通訊的代理 */ mWindowSession = WindowManagerGlobal.getWindowSession(); //② FallbackEventHandler是一個處理未經任何人消費的輸入事件的場所。 mFallbackEventHandler = new PhoneFallbackEventHandler(context); ... }
注:函數
它是一個Binder對象,真正的實現類是Session,也就是說下文setView方法中關於它的操做實際上是一次IPC過程。關於IPC(進程間通訊)的方式,以及Android操做系統中最主要的IPC方式Binder會在之後的文章中介紹。工具
關於FallbackEventHandler具體我會在下一章介紹。oop
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { if (mView == null) { //保存了控件的根 mView = view; ... mFallbackEventHandler.setView(view); ... /** *① 在添加窗口以前,先經過requestLayout方法在主線程上安排一次「遍歷」。 * 所謂「遍歷」是指ViewRootImpl中的核心方法performTraversal()。 * 這個方法實現對控件樹進行測量、佈局、向WMS申請修改窗口屬性以及重繪的全部工做。 */ requestLayout(); /** * ② 初始化mInputChanel。InputChannel是窗口接收來自InputDispatcher的輸入事件的管道 */ if ((mWindowAttributes.inputFeatures & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) { mInputChannel = new InputChannel(); } ... try { /** *上文剛講過mWindowSession是個Binder類,它的實現類是Session, *將經過IPC遠程調用(即調用另外一個進程中的)Session的addToDisplay方法把窗口添加進WMS中。 *完成這個操做後,mWindow已經被添加到指定對象中並且mInputChannel(若是不爲空)已經準備好接收事件 */ res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } catch (RemoteException e) { } finally { } ... // 錯誤處理。窗口添加失敗的緣由一般是是權限問題、重複添加或者token無效 if (res < WindowManagerGlobal.ADD_OKAY) { } ... // ③ 若是mInputChannel不爲空,則建立mInputEventReceiver用於接收輸入事件。 if (mInputChannel != null) { mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); } ... view.assignParent(this); ... } } }
接着咱們來一個個分析,先來最重要的,也是本章的最主要內容,另外兩個將會在下一章分析。佈局
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } }
scheduleTraversals();函數聲明以下post
void scheduleTraversals() { if (!mTraversalScheduled) { mTraversalScheduled = true; mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); mChoreographer.postCallback( Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null); if (!mUnbufferedInputDispatch) { scheduleConsumeBatchedInput(); } notifyRendererOfFramePending(); pokeDrawLockIfNeeded(); } }
其中mTraversalRunnable的定義是這樣的this
final class TraversalRunnable implements Runnable { @Override public void run() { doTraversal(); } }
doTraversal()函數聲明以下;spa
void doTraversal() { if (mTraversalScheduled) { mTraversalScheduled = false; mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier); if (mProfile) { Debug.startMethodTracing("ViewAncestor"); } performTraversals(); if (mProfile) { Debug.stopMethodTracing(); mProfile = false; } } }
注:如下文章屢次摘抄於張大偉老師的《深刻理解Android卷Ⅲ》,請支持原創,讀者也可去看張大偉老師的這本書籍
終於看到了咱們的豬腳performTraversals();,ViewRootImpl中接收的各類變化,如來自WMS的窗口屬性變化、來自控件樹的尺寸變化以及重繪請求等都引起performTraversals();的調用,並在其中完成處理。View類及其子類的onMeasure()、onLayout()、onDraw()等回調也都是在該方法執行的過程當中直接或間接的引起。該函數可謂是是ViewRootImpl的「心跳」。咱們就來看一下這個方法把。操作系統
先上源碼:(注:源碼很長,具體的分析在下方)
private void performTraversals() { final View host = mView; /** 第1階段 預測量 */ boolean windowSizeMayChange = false; boolean newSurface = false; boolean surfaceChanged = false; WindowManager.LayoutParams lp = mWindowAttributes; ...... //聲明本階段的豬腳,這兩個變量將是mView的SPEC_SIZE份量的候選 int desiredWindowWidth; int desiredWindowHeight; ...... Rect frame = mWinFrame; ...... if (mFirst) { mFullRedrawNeeded = true; mLayoutRequested = true; final Configuration config = mContext.getResources().getConfiguration(); if (shouldUseDisplaySize(lp)) { //爲狀態欄設置desiredWindowWidth/height 其取值是屏幕尺寸 Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { // ① 第1次「遍歷」的測量,採用了應用可使用的最大尺寸做爲SPEC_SIZE的候選 desiredWindowWidth = dipToPx(config.screenWidthDp); desiredWindowHeight = dipToPx(config.screenHeightDp); } ...... } else { // ② 在非第1次遍歷的狀況下,會採用窗口的最新尺寸做爲SPEC_SIZE的候選 desiredWindowWidth = frame.width(); desiredWindowHeight = frame.height(); //若是窗口的最新尺寸與ViewRootImpl中的現有尺寸不一樣,說明WMS單方面改變了窗口的尺寸,將致使一下三個結果 if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) { //須要完整的重繪以適應新的窗口尺寸 mFullRedrawNeeded = true; //須要對控件樹從新佈局 mLayoutRequested = true; //控件樹可能拒絕接受新的窗口尺寸,可能須要窗口在佈局階段嘗試設置新的窗口尺寸,,只是嘗試 windowSizeMayChange = true; } } ...... boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw); if (layoutRequested) { final Resources res = mView.getContext().getResources(); if (mFirst) { ...... } else { ...... /** *檢查WMS是否單方面改變了一些參數,標記下來,而後做爲後文是否進行控件佈局的條件之一 *若是窗口的width或height被指定爲WRAP_CONTENT時。表示該窗口爲懸浮窗口。 */ if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) { //懸浮窗口的尺寸取決於測量結果。所以有可能向WMS申請改變窗口的尺寸 windowSizeMayChange = true; if (shouldUseDisplaySize(lp)) { //同樣的設置狀態欄的desiredWindowWidth/height Point size = new Point(); mDisplay.getRealSize(size); desiredWindowWidth = size.x; desiredWindowHeight = size.y; } else { // ③ 設置懸浮窗口的SPEC_SIZE的候選爲應用可使用的最大尺寸 Configuration config = res.getConfiguration(); desiredWindowWidth = dipToPx(config.screenWidthDp); desiredWindowHeight = dipToPx(config.screenHeightDp); } } } // ④ 進行測量 windowSizeMayChange |= measureHierarchy(host, lp, res, desiredWindowWidth, desiredWindowHeight); } ...... if (layoutRequested) { mLayoutRequested = false; } ...... //⑤ 判斷窗口是否須要改變尺寸 boolean windowShouldResize = layoutRequested && windowSizeMayChange && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT && frame.width() < desiredWindowWidth && frame.width() != mWidth) || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && frame.height() < desiredWindowHeight && frame.height() != mHeight)); ...... /** 第1階段 預測量到這裏結束 */ /** 第2階段 窗口布局階段從這裏開始 */ if (/*進入窗口布局的幾個條件*/) { ...... boolean hadSurface = mSurface.isValid(); ...... try { relayoutResult = relayoutWindow(params, viewVisibility, insetsPending); }catch(...){ ...... }finally{ ...... } /** 第2階段 窗口布局階段到這裏結束。關於窗口布局的部分涉及太多,咱們不具體分析源碼,後文會有總結 */ /** 第3階段 最終測量階段從這裏開始 */ if (!mStopped || mReportNextDraw) { ...... int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); //① 能夠看到與與測量中調用的performMeasure performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); int width = host.getMeasuredWidth(); int height = host.getMeasuredHeight(); boolean measureAgain = false; //② 判斷LayoutParams.horizontalWeight和lp.verticalWeight ,以做爲是否再次測量的依據 if (lp.horizontalWeight > 0.0f) { width += (int) ((mWidth - width) * lp.horizontalWeight); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); measureAgain = true; } if (lp.verticalWeight > 0.0f) { height += (int) ((mHeight - height) * lp.verticalWeight); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); measureAgain = true; } if (measureAgain) { if (DEBUG_LAYOUT) Log.v(mTag, "And hey let's measure once more: width=" + width + " height=" + height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } layoutRequested = true; } } } else { } /** 第3階段 最終測量階段到這裏結束 */ /** 第4階段 控件佈局階段從這裏開始 */ //① 佈局階段的判斷條件 final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw); ...... if (didLayout) { ...... //② 經過performLayout對控件進行佈局 performLayout(lp, mWidth, mHeight); ...... //③ 若是有必要,計算窗口的透明區域並把該區域設置給WMS if ((host.mPrivateFlags & View.PFLAG_REQUEST_TRANSPARENT_REGIONS) != 0) { host.getLocationInWindow(mTmpLocation); mTransparentRegion.set(mTmpLocation[0], mTmpLocation[1], mTmpLocation[0] + host.mRight - host.mLeft, mTmpLocation[1] + host.mBottom - host.mTop); host.gatherTransparentRegion(mTransparentRegion); if (mTranslator != null) { mTranslator.translateRegionInWindowToScreen(mTransparentRegion); } if (!mTransparentRegion.equals(mPreviousTransparentRegion)) { mPreviousTransparentRegion.set(mTransparentRegion); mFullRedrawNeeded = true; try { mWindowSession.setTransparentRegion(mWindow, mTransparentRegion); } catch (RemoteException e) { } } } /** 第4階段 控件佈局階段到這裏結束 */ /** 第5階段 繪製階段從這裏開始 */ ...... boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible; if (!cancelDraw && !newSurface) { if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).startChangingAnimations(); } mPendingTransitions.clear(); } performDraw(); } else { if (isViewVisible) { // Try again scheduleTraversals(); } else if (mPendingTransitions != null && mPendingTransitions.size() > 0) { for (int i = 0; i < mPendingTransitions.size(); ++i) { mPendingTransitions.get(i).endChangingAnimations(); } mPendingTransitions.clear(); } } mIsInTraversal = false; }
因爲該方法是Android源代碼中最龐大的方法之一,因此咱們對其進行分階段分析。在源碼中有標註1,2,3,4,5,對每一階段再細分爲①②...,對照上文註釋
這是進入performTraversals();的第一個階段。它會對控件樹進行第一次測量。在此階段中將會計算出控件樹爲顯示其內容所需的尺寸,即指望的窗口尺寸。在這個階段中View及其子類的onMeasure()方法將會沿着控件樹依次獲得回調。
預測量和測量原理
預測量也是一次完整的測量過程,它與最終測量的區別僅在於參數不一樣而已。實際的測量工做是在View或其子類的onMeasure()方法中完成,而且其測量結果須要受限於來自其父控件的指示。這個指示由onMeasure()方法中的兩個參數進行傳達:widthSpec和heightSpec。它們是被稱爲MeasureSpec的複合整型變量,用於指導控件對自身進行測量。她又兩個份量,結構如圖
由①、②、③可知預測量時的SPEC_SIZE按照以下原則進行取值:
在第1階段第④步時,咱們看到了measureHierarchy方法,該方法用於測量整個控件樹。傳入的參數desiredWindowWidth,desiredWindowHeight在前述代碼中作了精心的挑選。控件樹本能夠按照這兩個參數完成測量,可是measureHierarchy有本身的考量,即如何將窗口布局的儘量優雅。measureHierarchy如何作到這一步呢,經過跟控件樹的協商。可是協商只發生在LayoutParams.width被指定爲WRAP_CONTENT時,若是LayoutParams.width被指定爲MATCH_PARENT或者固定數值時。該協商過程不會發生。
咱們來看一下measureHierarchy的源碼。
private boolean measureHierarchy(final View host, final WindowManager.LayoutParams lp, final Resources res, final int desiredWindowWidth, final int desiredWindowHeight) { int childWidthMeasureSpec; int childHeightMeasureSpec; boolean windowSizeMayChange = false;//表示是否可能致使窗口的尺寸變化 boolean goodMeasure = false;//表示側臉是否能知足控件樹充分顯示內容的要求 if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT) { /** ① 第一次協商 measureHierarchy使用它指望的寬度限制進行測量, */ final DisplayMetrics packageMetrics = res.getDisplayMetrics(); res.getValue(com.android.internal.R.dimen.config_prefDialogWidth, mTmpValue, true); int baseSize = 0; //寬度限制保存在baseSize中 if (mTmpValue.type == TypedValue.TYPE_DIMENSION) { baseSize = (int)mTmpValue.getDimension(packageMetrics); } //若是寬度限制不爲0而且傳入的desiredWindowWidth 大於measureHierarchy指望的限制寬度, if (baseSize != 0 && desiredWindowWidth > baseSize) { childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); //② 第一次測量 使用measureHierarchy指望的限制寬度 並獲得狀態 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //判斷狀態 if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = true;//控件樹對測量結果滿意 } else { //③ 控件樹對測量結果不滿意,進行第二次協商,此次把限制寬度放大爲指望寬度baseSize和最大寬度desiredWindowWidth和的一半 baseSize = (baseSize+desiredWindowWidth)/2; childWidthMeasureSpec = getRootMeasureSpec(baseSize, lp.width); //④ 第2次測量 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = true; } } } } //若是兩次協商測量均不能讓控件樹滿意,那麼measureHierarchy再也不對寬度進行限制,使用最大寬度進行測量 if (!goodMeasure) { childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height); performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); //若是測量獲得的寬度或者高度與ViewRootImpl中的窗口不一致,,那麼以後可能要改變窗口的尺寸了 if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = true; } } return windowSizeMayChange; }
咱們再來看performMeasure方法,performMeasure方法的實現很是簡單,它直接調用了mView.measure的方法
private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) { if (mView == null) { return; } Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure"); try { mView.measure(childWidthMeasureSpec, childHeightMeasureSpec); } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }
終於到了View的measure方法,在該方法內部會調用咱們熟悉的onMeasure方法,咱們來看View.measure方法的實現
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { .......//初始化操做 if (forceLayout || needsLayout) { //① 準備工做 mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; ....... //② 對本控價進行測量 onMeasure(widthMeasureSpec, heightMeasureSpec); ....... //③ 檢查onMeasure的實現是否調用了setMeasuredDimension() if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } //④ 將PFLAG_LAYOUT_REQUIRED加入mPrivateFlags ,這一操做會對以後的佈局操做放行 mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
前文屢次設置了windowSizeMayChange 爲true,可是windowSizeMayChange 爲true滿是窗口是否須要改變尺寸的條件之一,咱們來看第1階段⑤對應代碼。
boolean windowShouldResize = layoutRequested && windowSizeMayChange && ((mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) || (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT && frame.width() < desiredWindowWidth && frame.width() != mWidth) || (lp.height == ViewGroup.LayoutParams.WRAP_CONTENT && frame.height() < desiredWindowHeight && frame.height() != mHeight));
能夠看到windowShouldResize 的判斷較爲複雜,咱們來總結一下
必要條件:
layoutRequested爲true。表示ViewRootImpl的requestLayout方法被調用過。
在View中也有requestLayout方法。當控件內容發生變化從而須要調整其尺寸時,會調用自身的requestLayout(),而且此方法會沿着控件樹向根部回溯,最終調用到ViewRootImpl的requestLayout,從而引起一次performTraversals()調用。
之因此這是一個必要條件,是由於performTraversals()還有可能由於重繪時調用,當控件僅須要重繪而不須要從新佈局時(例如背景色或者前景色發生變化時)。會經過invalidate()方法回溯到ViewRootImpl,此時不會經過requestLayout觸發performTraversals()調用,而是經過scheduleTraversals()方法進行觸發。這種狀況下不須要進行佈局窗口階段
windowSizeMayChange爲true,該變量前文中已有詳細描述。
在上述條件知足的條件下,如下條件知足其一即觸發布局窗口階段
根據預測量的結果,經過IWindowSession.relayout()方法向WMS請求調整窗口的尺寸等屬性,這將引起WMS對窗口從新佈局,並將佈局結果返回給ViewRootImpl.
總結:佈局窗口得以進行的緣由是控件系統有修改窗口屬性的需求,如第一次「遍歷」須要肯定窗口的尺寸以及一塊Surface,預測量結果與窗口當前尺寸不一致須要進行窗口尺寸更改,mView可見性發生變化須要將窗口隱藏或顯示等。
預測量的結果是控件樹所指望的窗口尺寸。然而因爲在WMS中影響佈局的因素不少,WMS不必定會將窗口的準確的佈局爲控件樹所要求的尺寸,而迫於WMS做爲系統服務的強勢地位,控件樹不得不接受WMS的佈局結果。在這個階段中View及其子類的onMeasure()方法將會沿着控件樹依次被回調。最終測量階段直接調用performMeasure而不是measureHierarchy,是由於measureHierarchy有個協商過程,而到了最終測量階段控件樹已經沒有了協商的餘地,不管控件樹樂意與否,他只能被迫接受WMS的佈局結果
將上一步完成的最終測量的結果做爲依據進行佈局。測量肯定的是控件的尺寸,而佈局肯定的是控件的位置。在這個階段中View及其子類的onLayout()方法將會被回調。
整體來講4. 佈局控件樹階段(Layout)作了兩件事。
調用了performLayout函數,雖然咱們還沒看到該函數,但猜想想必和performMeasure差很少。咱們來看一下
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { ...... try { //同樣是調用View.layout函數 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); ...... } finally { } ...... }
咱們再來看View.layout。佈局階段把測量結果轉化爲控件的實際位置與尺寸。而控件的實際位置與尺寸由Veiw的mLeft、mTop、mRight、mBottom 這4個成員變量存儲的座標值。即控件樹的佈局過程就是根據測量結果爲每個控件設置這4個成員變量的過程。其中mLeft、mTop、mRight、mBottom 是相對於父控件的座標值。
public void layout(int l, int t, int r, int b) { //若是設置了PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT標誌位,那麼在佈局以前先進行測量,調用onMeasure函數 if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) { onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec); mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT; } //保存原始座標 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //設置新座標 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //應該還記得上文View.measure方法中的最後設置了PFLAG_LAYOUT_REQUIRED吧 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { //調用onLayout方法。若是該Vie是個ViewGroup。onLayout中須要依次調用子控件的layout方法 onLayout(changed, l, t, r, b); ...... //清除PFLAG_LAYOUT_REQUIRED標記 mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED; //通知每個對此控件佈局變化有興趣的Listener 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); } } } ...... } }
咱們來對比測量和佈局階段以便更好的理解
佈局階段的另外一個工做是計算並設置窗口的透明區域。這一功能主要是爲SurfaceView服務。關於SurfaceView的相關知識咱們後文介紹
這是performTraversals();的最後階段。肯定控件的尺寸和位置後。便進行對控件樹的繪製。在這個階段中View及其子類的onDraw()方法將會被回調。
咱們在開發Android自定義控件時,每每都須要重寫View.onDraw()方法以繪製內容到一個給定的Canvas中。 咱們來看一下Canvas。Canvas是一個繪圖工具類,其API提供了一系列繪圖指定供開發者使用。這些指令能夠分爲兩個部分:
本篇文章詳細分析了ViewRootImpl的五大過程,ViewRootImpl比較複雜,尤爲是它的「心跳」performTraversals();。但願讀者能多看幾遍上面的分析。相信你必定會有收穫的
在下一篇文章中咱們將進行實戰項目,也是對咱們前幾篇文章的實際應用。老話說的好,紙上得來終覺淺,絕知此事要躬行。下一篇甚至幾篇咱們就來自定義View
此致,敬禮