Android自定義View繪制流程詳解
Android 中 Activity
Android 中 Activity 是作為應(yīng)用程序的載體存在,代表著一個完整的用戶界面,提供了一個窗口來繪制各種視圖,當(dāng) Activity 啟動時,我們會通過 setContentView 方法來設(shè)置一個內(nèi)容視圖,這個內(nèi)容視圖就是用戶看到的界面。那么 View 和 activity 是如何關(guān)聯(lián)在一起的呢 ?
Android的UI層級繪制體系
上圖是View與Activity之間的關(guān)系,先介紹一下上面這張圖
- PhoneWindow:每個Activity都會創(chuàng)建一個Window用來承載View的顯示,Window是一個抽象類,PhoneWindow是Window的唯一實現(xiàn)類,該類中包含一個DecorView。
- DecorView:最頂層的View,該View繼承自 FrameLayout,它的內(nèi)部包含兩部分,一部分是ActionBar ,另一部分ContentView,
- ContentView:我們 setContentView() 中傳入的布局,就在該View中加載顯示
- ViewRootImpl:視圖層次結(jié)構(gòu)的頂部。一個 Window 對應(yīng)著一個 ViewRootImpl 和 一個DecorView,通過該實例對DecorView進(jìn)行控制,最終通過執(zhí)行ViewRootImpl的performTraversals()開啟整個View樹的繪制,
View的加載流程
- 當(dāng)調(diào)用 Activity 的setContentView 方法后會調(diào)用PhoneWindow 類的setContentView方法
public void setContentView(@LayoutRes int layoutResID) { getWindow().setContentView(layoutResID); initWindowDecorActionBar(); }
- PhoneWindow類的setContentView方法中最終會生成一個DecorView對象
@Override public void setContentView(int layoutResID) { if (mContentParent == null) { //在這里生成一個DecorView installDecor(); } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) { mContentParent.removeAllViews(); } ... } private void installDecor() { mForceDecorInstall = false; //mDecor 為DecorView if (mDecor == null) { mDecor = generateDecor(-1); mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); mDecor.setIsRootNamespace(true); if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) { mDecor.postOnAnimation(mInvalidatePanelMenuRunnable); } } else { mDecor.setWindow(this); } ... } protected DecorView generateDecor(int featureId) { ... // 在這里直接 new 了一個DecorView return new DecorView(context, featureId, this, getAttributes()); }
- DecorView容器中包含根布局,根布局中包含一個id為content的FrameLayout布局,Activity加載布局的xml最后通過LayoutInflater將xml文件中的內(nèi)容解析成View層級體系,最后填加到id為content的FrameLayout布局中。
protected ViewGroup generateLayout(DecorView decor) { //做一些窗體樣式的判斷 ... //給窗體進(jìn)行裝飾 int layoutResource; int features = getLocalFeatures(); // System.out.println("Features: 0x" + Integer.toHexString(features)); //加載系統(tǒng)布局 判斷到底是加載那個布局 if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) { layoutResource = R.layout.screen_swipe_dismiss; setCloseOnSwipeEnabled(true); } ... mDecor.startChanging(); //將加載到的基礎(chǔ)布局添加到mDecor中 mDecor.onResourcesLoaded(mLayoutInflater, layoutResource); //通過系統(tǒng)的content的資源ID去進(jìn)行實例化這個控件 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); if (contentParent == null) { throw new RuntimeException("Window couldn't find content container view"); } }
到此,Actvity的繪制完成
View的視圖繪制流程剖析
- DecorView被加載到Window中
在ActivityThread的 handleResumeActivity() 方法中通過WindowManager將DecorView加載到Window中,通過ActivityThread中一下代碼可以得到應(yīng)征
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) { ... //在此處執(zhí)行Activity的onResume方法 r = performResumeActivity(token, clearHide, reason); if (r != null) { final Activity a = r.activity; if (localLOGV) Slog.v( TAG, "Resume " + r + " started activity: " + a.mStartedActivity + ", hideForNow: " + r.hideForNow + ", finished: " + a.mFinished); final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } if (r.window == null && !a.mFinished && willBeVisible) { //獲取window對象 r.window = r.activity.getWindow(); //獲取DecorView View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); //獲取WindowManager,在這里getWindowManager()實質(zhì)上獲取的是ViewManager的子類對象WindowManager ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (r.mPreserveWindow) { a.mWindowAdded = true; r.mPreserveWindow = false; //獲取ViewRootImpl對象 ViewRootImpl impl = decor.getViewRootImpl(); if (impl != null) { impl.notifyChildRebuilt(); } } if (a.mVisibleFromClient && !a.mWindowAdded) { a.mWindowAdded = true; //在這里WindowManager將DecorView添加到PhoneWindow中 wm.addView(decor, l); } }
總結(jié):在ActivityThread的handleResumeActivity方法中WindowManager將DecorView添加到PhoneWindow中,addView()方法執(zhí)行時將視圖添加的動作交給了ViewRootImpl處理,最后在ViewRootImpl的performTraversals中開始View樹的繪制
ViewRootImpl的performTraversals()方法完成具體的視圖繪制流程
private void performTraversals() { if (!mStopped || mReportNextDraw) { ... int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width); int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height); ... // Ask host how big it wants to be //View繪制:開始測量 View的測量時遞歸逐層測量,由父布局與子布局共同確認(rèn)子View的測量模式,在子布局測量完畢時確認(rèn)副布局的寬高, //在此方法執(zhí)行完畢后才可獲取到View的寬高,否側(cè)獲取的寬高都為0 performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } if (didLayout) { //開始擺放,該方法是ViewGroup中的方法,例如 LinerLayout... performLayout(lp, mWidth, mHeight); } if (!cancelDraw && !newSurface) { //開始繪制,執(zhí)行View的onDraw()方法 performDraw(); } }
下面開始對performMeasure(),performLayout(),performDraw()進(jìn)行解析
- 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); } }
通過以上這段代碼,我們可以看到兩個重要的參數(shù) childWidthMeasureSpec,childHeightMeasureSpec,這兩個Int類型的參數(shù)包含了View的測量模式和寬高信息,因此在onMeasure()方法中我們可以通過該參數(shù)獲取到測量模式,和寬高信息,我們在onMeasue中設(shè)置寬高信息也是通過MeasureSpec設(shè)置,
*/ public static class MeasureSpec { //int類型占4個字節(jié),其中高2位表示尺寸測量模式,低30位表示具體的寬高信息 private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; /** @hide */ @IntDef({UNSPECIFIED, EXACTLY, AT_MOST}) @Retention(RetentionPolicy.SOURCE) public @interface MeasureSpecMode {} //如下所示是MeasureSpec中的三種模式:UNSPECIFIED、EXACTLY、AT_MOST //UNSPECIFIED:未指定模式,父容器不限制View的大小,一般用于系統(tǒng)內(nèi)部的測量 public static final int UNSPECIFIED = 0 << MODE_SHIFT; //AT_MOST:最大模式,對應(yīng)于在xml文件中指定控件大小為wrap_content屬性,子View的最終大小是父View指定的大小值,并且子View的大小不能大于這個值 public static final int EXACTLY = 1 << MODE_SHIFT; //EXACTLY :精確模式,對應(yīng)于在xml文件中指定控件為match_parent屬性或者是具體的數(shù)值,父容器測量出View所需的具體大小 public static final int AT_MOST = 2 << MODE_SHIFT; //獲取測量模式 @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } //獲取寬高信息 public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } ... }
performMeasure()會繼續(xù)調(diào)用mView.measure()方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { boolean optical = isLayoutModeOptical(this); if (optical != isLayoutModeOptical(mParent)) { Insets insets = getOpticalInsets(); int oWidth = insets.left + insets.right; int oHeight = insets.top + insets.bottom; //根據(jù)原有寬高計算獲取不同模式下的具體寬高值 widthMeasureSpec = MeasureSpec.adjust(widthMeasureSpec, optical ? -oWidth : oWidth); heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight); } ... if (forceLayout || needsLayout) { // first clears the measured dimension flag mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET; resolveRtlPropertiesIfNeeded(); int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key); if (cacheIndex < 0 || sIgnoreMeasureCache) { // measure ourselves, this should set the measured dimension flag back //在該方法中子控件完成具體的測量 onMeasure(widthMeasureSpec, heightMeasureSpec); ... } ... }
從上述代碼片段中可以看到執(zhí)行到了onMeasure()方法,如果該控件為View的話,測量到此結(jié)束,如果是ViewGroup的話,會繼續(xù)循環(huán)獲取所有子View,調(diào)用子View的measure方法,下面以LinearLayout為例,繼續(xù)看
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mOrientation == VERTICAL) { measureVertical(widthMeasureSpec, heightMeasureSpec); } else { measureHorizontal(widthMeasureSpec, heightMeasureSpec); } }
LinearLayout通過不同的擺放布局執(zhí)行不同的測量方法,以measureVertical為例,向下看
void measureVertical(int widthMeasureSpec, int heightMeasureSpec) { //獲取子View的個數(shù) final int count = getVirtualChildCount(); ... //循環(huán)獲取所有子View for (int i = 0; i < count; ++i) { //獲取子View final View child = getVirtualChildAt(i); //調(diào)用子View的measure方法 child.measure(childWidthMeasureSpec, childHeightMeasureSpec); } .... }
至此,View的測量流程結(jié)束
View的layout流程分析
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) { final View host = mView; // 在此處調(diào)用mView的layout()擺放開始 host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight()); }
/* *@param l view 左邊緣相對于父布局左邊緣距離 *@param t view 上邊緣相對于父布局上邊緣位置 *@param r view 右邊緣相對于父布局左邊緣距離 *@param b view 下邊緣相對于父布局上邊緣距離 */ public void layout(int l, int t, int r, int b) { ... //記錄 view 原始位置 int oldL = mLeft; int oldT = mTop; int oldB = mBottom; int oldR = mRight; //調(diào)用 setFrame 方法 設(shè)置新的 mLeft、mTop、mBottom、mRight 值, //設(shè)置 View 本身四個頂點位置 //并返回 changed 用于判斷 view 布局是否改變 boolean changed = isLayoutModeOptical(mParent) ? setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b); //第二步,如果 view 位置改變那么調(diào)用 onLayout 方法設(shè)置子 view 位置 if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) { //開始調(diào)用 onLayout 在此處根據(jù)子View的寬高及相關(guān)規(guī)則進(jìn)行擺放 onLayout(changed, l, t, r, b); ... } } } }
View的Draw流程分析
private void performDraw() { ... //調(diào)用draw方法 draw(fullRedrawNeeded); ... } private void draw(boolean fullRedrawNeeded) { ... //View的繪制流程調(diào)用的 drawSoftware() 該方法 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; ... //初始化畫布 canvas = mSurface.lockCanvas(dirty); ... //開始調(diào)用ViewGroup 和 View的draw方法 mView.draw(canvas); ... } public void draw(Canvas canvas) { drawBackground(canvas); //ViewGroup 默認(rèn)是不會調(diào)用OnDraw方法的 if (!dirtyOpaque) onDraw(canvas); //這個方法主要是ViewGroup循環(huán)調(diào)用 drawChild()進(jìn)行對子View的繪制 dispatchDraw(canvas); } protected void onDraw(Canvas canvas) { }
View的onDraw方法只是一個模版,具體實現(xiàn)方式,交由我們這些開發(fā)者去進(jìn)行實現(xiàn)
至此,View的繪制流程完畢
- requestLayout重新繪制視圖
子View調(diào)用requestLayout方法,會標(biāo)記當(dāng)前View及父容器,同時逐層向上提交,直到ViewRootImpl處理該事件,ViewRootImpl會調(diào)用三大流程,從measure開始,對于每一個含有標(biāo)記位的view及其子View都會進(jìn)行測量、布局、繪制。
- invalidate在UI線程中重新繪制視圖
當(dāng)子View調(diào)用了invalidate方法后,會為該View添加一個標(biāo)記位,同時不斷向父容器請求刷新,父容器通過計算得出自身需要重繪的區(qū)域,直到傳遞到ViewRootImpl中,最終觸發(fā)performTraversals方法,進(jìn)行開始View樹重繪流程(只繪制需要重繪的視圖)。
- postInvalidate在非UI線程中重新繪制視圖
這個方法與invalidate方法的作用是一樣的,都是使View樹重繪,但兩者的使用條件不同,postInvalidate是在非UI線程中調(diào)用,invalidate則是在UI線程中調(diào)用。
以上就是Android自定義View繪制流程詳解的詳細(xì)內(nèi)容,更多關(guān)于Android View繪制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中的AtomicLong原理、使用與實戰(zhàn)指南
本文詳細(xì)介紹了AtomicLong在Android多線程開發(fā)中的應(yīng)用,包括其核心原理、基本使用、適用場景、生產(chǎn)環(huán)境實戰(zhàn)案例以及性能優(yōu)化建議,通過大量Kotlin代碼示例,幫助開發(fā)者更好地理解和使用AtomicLong,感興趣的朋友一起看看吧2025-03-03Android使用OkHttp進(jìn)行網(wǎng)絡(luò)同步異步操作
這篇文章主要為大家詳細(xì)介紹了Android使用OkHttp進(jìn)行網(wǎng)絡(luò)同步異步操作,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-07-07Android使用setContentView實現(xiàn)頁面的轉(zhuǎn)換效果
這篇文章主要介紹了Android如何使用setContentView實現(xiàn)頁面的轉(zhuǎn)換效果,幫助大家更好的利用Android進(jìn)行開發(fā),感興趣的朋友可以了解下2021-01-01Eclipse工程轉(zhuǎn)為兼容Android Studio模式的方法步驟圖文詳解
這篇文章主要介紹了Eclipse工程轉(zhuǎn)為兼容Android Studio模式的方法步驟,本文圖文并茂給大家介紹的非常詳細(xì),需要的朋友可以參考下2017-12-12Android recycleView的應(yīng)用和點擊事件實例詳解
這篇文章主要介紹了Android recycleView的應(yīng)用和點擊事件實例詳解的相關(guān)資料,需要的朋友可以參考下2016-12-12Android ProgressBar實現(xiàn)進(jìn)度條效果
這篇文章主要為大家詳細(xì)介紹了Android ProgressBar實現(xiàn)進(jìn)度條效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-04-04