Ⅰ android UI | View 的繪制流程詳解
上一篇文章講解了,從setContentView方法到了解View是如何繪制的: 傳送門
在這篇博客講述了, 在ViewRootImpl類中performTraversals方法中具體的繪制過程,其中裡面就有 performMeasure()、performLayout()、performDraw() 三個方法的調用, 那麼要了解View 的測量、布局、繪制,就分別跟這三個方法有關系。
先來看看performMeasure()方法的調用過程
先看performMeasure方法,這個方法有兩個參數,都是通過getRootMeasureSpec()方法計算得到
這里有一個關鍵類MeasureSpec,在這里需要了解下這個類的原理。
這里要感謝 這位博主 ,他講述的很清晰,我自己動手測算了,很容易理解。大概就是用一個數字通過高位記錄Mode,地位記錄size的方式,記錄兩個數據,都是通過一個掩碼做位移運算得來。就是說這個變數(measureSpec)的值可以通過掩碼分別得到測量mode 和 測量size。
繼續查看performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);方法:
這里mView是DecorView對象,那麼他調用的實際上是View的measure方法,查詢DecorView和FrameLayout都沒有measure方法,所以他調用的是View的measure方法
DecorView.onMeasure()方法如下:
FrameLayout.onMeasure方法如下:(這個方法裡面都很重要)
我們先看 , measureChildWithMargins 方法,
getChildMeasureSpec 方法內容如下:
那麼假如我們寫的布局根節點是LinearLayout,那麼就會在執行到View.measure方法裡面的onMeasure方法時,就會調用到LinearLayout.onMeasure方法,具體內容如下:
通過源碼可以看到,還是會循環調用子View , 就這樣循環遞歸的測量完最裡面的一個view,這個過程中onMeasure方法可能會被多次執行。
還是從ViewRootImpl.performTraversals開始
這里跟measure 調用流程其實一樣,DecorView和FrameLayout沒有重寫layout方法,所以調用的是View.layout方法,
由於當前對象是decorView,所以調用的是DecorView.onLayout方法:
FrameLayout.onLayout 方法如下:
繼續查看 View.draw方法
在這個方法裡面,所有重要的方法都在裡面,onDraw、dispatchDraw 等等都在裡面,我看了下這個方法裡面都挺重要就沒刪減,也都能看得懂。
到此View的整個繪制流程就搞清楚了。
關於子view測量
1、不管父View是何模式,若子View有確切數值,則子View大小就是其本身大小,且mode是EXACTLY
2、若子View是match_parent,則模式與父View相同,且大小同父View(若父View是UNSPECIFIED,則子View大小為0)
3、若子View是wrap_content,則模式是AT_MOST,大小同父View,表示不可超過父View大小(若父View是UNSPECIFIED,則子View大小為0)
關於繪制流程
我們自定義的view,基本上只需要重寫 onMeasure、onLayout、onDraw即可
Ⅱ Carson帶你學Android:你真的了解view.post()嗎
為什麼view.post()能保證獲取到view的寬高?
View.post()的原理: 以Handler為基礎,View.post() 將傳入任務添加到 View繪制任務所在的消息隊列尾部,從而保證View.post() 任務的執行時機是在View 繪制任務完成之後的。 其中,幾個關鍵點:
所以:
具體源碼分析請看: Android:為什麼view.post()能保證獲取到view的寬高?
為什麼onCreate()使用view.post()無法立刻執行任務(如獲取寬高),需要在onResume()後才可獲取?
在onCreate()時,AttachInfo還沒被賦值(為null)(是在view.dispatchAttachedToWindow()才被賦值),所亮物猜以會走下述源碼的過程2;通過上面分析,此過程的作用僅是:保存了通過post()添加的任務,並沒執行。
若只是創建一個 View & 調用它的post(),那麼post的任務會不會被執行?
不會。主要原因是:
每個View中post() 需執行的任務,必須得添加到窗口視圖-執行繪制流程 - 任務才會被post到消息隊列里去等待執行,即依賴於dispatchAttachedToWindow ();
若View未添加到窗口視圖,那麼就不會走繪制流程,post() 添加的任務最終不會被post到消息隊列里,即得不到執行。(但會保存到HandlerAction數組里)
上述例子,因為它沒有被添加到窗口視圖,所以不會走繪制流程,所以該任務最終不會被post到消息隊列里 & 執行
此時只需要添加將View添加到窗口,那麼post()的任務即可螞租被執行
view.pos()傳入的任務被執行的有效期是多久?
在整個 Activity 的生命周期內都可以正常使用 View.post() 任務
任務被執行是構造AttachInfo,所敬型以任務釋放即時釋放AttachInfo (置為null)。而AttachInfo 的釋放操作(置為null)是在 Activity 生命周期 onDestory 方法之後
下面,我們將分析,什麼時候調用上述入口,即DecorView.dispatchDetachedFromWindow();
此時需從 將DecorView從WindowManager中移除 開始講起:移除 Window 窗口任務是通過 ActivityThread.handleDestoryActivity()完成。
View.post() 任務被執行的有效期是在 Activity 生命周期 onDestory()後。本質是追蹤AttachInfo的釋放過程(置為null)
AttachInfo的釋放過程是在 將DecorView從WindowManager中移除時:回調DecorView.dispatchDetachedFromWindow(),其具體行為是:
而上述過程是在ActivityThread.handleDestoryActivity()中回調 Activity.onDestory()之後。
至此,關於view.post()的四大常見疑問 (坑)內容講解完畢。
不定期分享關於 安卓開發 的干貨,追求 短、平、快 ,但 卻不缺深度 。
Ⅲ Android 重學系列 View的繪制流程(六) 硬體渲染(上)
本文開始聊聊Android中的硬體渲染。如果跟著我的文章順序,從SF進程到App進程的繪制流程一直閱讀,我們到這里已經有了一定的基礎,可以試著進行橫向比對如Chrome瀏覽器渲染流程,看看軟體渲染,硬體渲染,SF合成都做了什麼程度的優化。
先讓我們回顧一下負責硬體渲染的主體對象ThreadedRenderer在整個繪制流程中做了哪幾個步驟。
在硬體渲染的過程中,有一個很核心的對象RenderNode,作為每一個View繪制的節點對象。
當每一次進行准備進行繪制的時候,都會雷打不動執行如下三個步驟:
如果遇到什麼問題歡迎來到 https://www.jianshu.com/p/c84bfa909810 下進行討論
實際上整個硬體渲染的設計還是比較龐大。因此本文先聊聊ThreadedRender整個體系中主要對象的構造以及相關的原理。
首先來認識下面幾個重要的對象有一個大體的印象。
在java層中面向Framework中,只有這么多,下面是一一映射的簡圖。
能看到實際上RenderNode也會跟著View 樹的構建同時一起構建整個顯示層級。也是因此ThreadedRender也能以RenderNode為線索構建出一套和軟體渲染一樣的渲染流程。
僅僅這樣?如果只是這么簡單,知道我習慣的都知道,我喜歡把相關總結寫在最後。如果把總攬寫在正文開頭是因為設計比較繁多。因為我們如果以流水線的形式進行剖析容易造成迷失細節的困境。
讓我繼續介紹一下,在硬體渲染中native層的核心對象。
如下是一個思維導圖:
有這么一個大體印象後,就不容易迷失在源碼中。我們先來把這些對象的實例化以及上面列舉的ThreadedRenderer在ViewRootImpl中執行行為的順序和大家來聊聊其原理,先來看看ThreadedRenderer的實例化。
當發現mSurfaceHolder為空的時候會調用如下函數:
而這個方法則調用如下的方法對ThreadedRenderer進行創建:
文件:/ frameworks / base / core / java / android / view / ThreadedRenderer.java
能不能創建的了ThreadedRenderer則決定於全局配置。如果ro.kernel.qemu的配置為0,說明支持OpenGL 則可以直接返回true。如果qemu.gles為-1說明不支持OpenGL es返回false,只能使用軟體渲染。如果設置了qemu.gles並大於0,才能打開硬體渲染。
我們能看到ThreadedRenderer在初始化,做了三件事情:
關鍵是看1-3點中ThreadRenderer都做了什麼。
文件:/ frameworks / base / core / jni / android_view_ThreadedRenderer.cpp
能看到這里是直接實例化一個RootRenderNode對象,並把指針的地址直接返回。
能看到RootRenderNode繼承了RenderNode對象,並且保存一個JavaVM也就是我們所說的Java虛擬機對象,一個java進程全局只有一個。同時通過getForThread方法,獲取ThreadLocal中的Looper對象。這里實際上拿的就是UI線程的Looper。
在這個構造函數有一個mDisplayList十分重要,記住之後會頻繁出現。接著來看看RenderNode的頭文件:
文件:/ frameworks / base / libs / hwui / RenderNode.h
實際上我把幾個重要的對象留下來:
文件:/ frameworks / base / core / java / android / view / RenderNode.java
能看到很簡單,就是包裹一個native層的RenderNode返回一個Java層對應的對象開放Java層的操作API。
能看到這個過程生成了兩個對象:
這個對象實際上讓RenderProxy持有一個創建動畫上下文的工廠。RenderProxy可以通過ContextFactoryImpl為每一個RenderNode創建一個動畫執行對象的上下文AnimationContextBridge。
文件:/ frameworks / base / libs / hwui / renderthread / RenderProxy.cpp
在這里有幾個十分重要的對象被實例化,當然這幾個對象在聊TextureView有聊過( SurfaceView和TextureView 源碼淺析 ):
我們依次看看他們初始化都做了什麼。
文件:/ frameworks / base / libs / hwui / renderthread / RenderThread.cpp
能看到其實就是簡單的調用RenderThread的構造函數進行實例化,並且返回對象的指針。
RenderThread是一個線程對象。先來看看其頭文件繼承的對象:
文件:/ frameworks / base / libs / hwui / renderthread / RenderThread.h
其中RenderThread的中進行排隊處理的任務隊列實際上是來自ThreadBase的WorkQueue對象。
文件:/ frameworks / base / libs / hwui / thread / ThreadBase.h
ThreadBase則是繼承於Thread對象。當調用start方法時候其實就是調用Thread的run方法啟動線程。
另一個更加關鍵的對象,就是實例化一個Looper對象到WorkQueue中。而直接實例化Looper實際上就是新建一個Looper。但是這個Looper並沒有獲取當先線程的Looper,這個Looper做什麼的呢?下文就會揭曉。
WorkQueue把一個Looper的方法指針設置到其中,其作用可能是完成了某一件任務後喚醒Looper繼續工作。
而start方法會啟動Thread的run方法。而run方法最終會走到threadLoop方法中,至於是怎麼走進來的,之後有機會會解剖虛擬機的源碼線程篇章進行講解。
在threadloop中關鍵的步驟有如下四個:
在這個過程中創建了幾個核心對象:
另一個核心的方法就是,這個方法為WorkQueue的Looper注冊了監聽:
能看到在這個Looper中注冊了對DisplayEventReceiver的監聽,也就是Vsync信號的監聽,回調方法為displayEventReceiverCallback。
我們暫時先對RenderThread的方法探索到這里,我們稍後繼續看看回調後的邏輯。
文件:/ frameworks / base / libs / hwui / thread / ThreadBase.h
能看到這里的邏輯很簡單實際上就是調用Looper的pollOnce方法,阻塞Looper中的循環,直到Vsync的信號到來才會繼續往下執行。詳細的可以閱讀我寫的 Handler與相關系統調用的剖析 系列文章。
文件:/ frameworks / base / libs / hwui / thread / ThreadBase.h
實際上調用的是WorkQueue的process方法。
文件:/ frameworks / base / libs / hwui / thread / WorkQueue.h
能看到這個過程中很簡單,幾乎和Message的loop的邏輯一致。如果Looper的阻塞打開了,則首先找到預計執行時間比當前時刻都大的WorkItem。並且從mWorkQueue移除,最後添加到toProcess中,並且執行每一個WorkItem的work方法。而每一個WorkItem其實就是通過從某一個壓入方法添加到mWorkQueue中。
到這里,我們就明白了RenderThread中是如何消費渲染任務的。那麼這些渲染任務又是哪裡誕生呢?
上文聊到了在RenderThread中的Looper會監聽Vsync信號,當信號回調後將會執行下面的回調。
能看到這個方法的核心實際上就是調用drainDisplayEventQueue方法,對ui渲染任務隊列進行處理。
能到在這里mVsyncRequested設置為false,且mFrameCallbackTaskPending將會設置為true,並且調用queue的postAt的方法執行ui渲染方法。
還記得queue實際是是指WorkQueue,而WorkQueue的postAt方法實際實現如下:
/ frameworks / base / libs / hwui / thread / WorkQueue.h
情景帶入,當一個Vsync信號達到Looper的監聽者,此時就會通過WorkQueue的drainDisplayEventQueue 壓入一個任務到隊列中。
每一個默認的任務都是執行dispatchFrameCallback方法。這里的判斷mWorkQueue中是否存在比當前時間更遲的時刻,並返回這個WorkItem。如果這個對象在頭部needsWakeup為true,說明可以進行喚醒了。而mWakeFunc這個方法指針就是上面傳下來:
把阻塞的Looper喚醒。當喚醒後就繼續執行WorkQueue的process方法。也就是執行dispatchFrameCallbacks方法。
在這里執行了兩個事情:
先添加到集合中,在上面提到過的threadLoop中,會執行如下邏輯:
如果大小不為0,則的把中的IFrameCallback全部遷移到mFrameCallbacks中。
而這個方法什麼時候調用呢?稍後就會介紹。其實這部分的邏輯在TextureView的解析中提到過。
接下來將會初始化一個重要對象:
這個對象名字叫做畫布的上下文,具體是什麼上下文呢?我們現在就來看看其實例化方法。
文件:/ frameworks / base / libs / hwui / renderthread / CanvasContext.cpp
文件:/ device / generic / goldfish / init.ranchu.rc
在init.rc中默認是opengl,那麼我們就來看看下面的邏輯:
首先實例化一個OpenGLPipeline管道,接著OpenGLPipeline作為參數實例化CanvasContext。
文件:/ frameworks / base / libs / hwui / renderthread / OpenGLPipeline.cpp
能看到在OpenGLPipeline中,實際上就是存儲了RenderThread對象,以及RenderThread中的mEglManager。透過OpenGLPipeline來控制mEglManager進而進一步操作OpenGL。
做了如下操作:
文件:/ frameworks / base / libs / hwui / renderstate / RenderState.cpp
文件:/ frameworks / base / libs / hwui / renderthread / DrawFrameTask.cpp
實際上就是保存這三對象RenderThread;CanvasContext;RenderNode。
文件:/ frameworks / base / core / jni / android_view_ThreadedRenderer.cpp
能看到實際上就是調用RenderProxy的setName方法給當前硬體渲染對象設置名字。
文件:/ frameworks / base / libs / hwui / renderthread / RenderProxy.cpp
能看到在setName方法中,實際上就是調用RenderThread的WorkQueue,把一個任務隊列設置進去,並且調用runSync執行。
能看到這個方法實際上也是調用post執行排隊執行任務,不同的是,這里使用了線程的Future方式,阻塞了執行,等待CanvasContext的setName工作完畢。
Ⅳ Android UI繪制之View繪制的工作原理
這是AndroidUI繪制流程分析的第二篇文章,主要分析界面中View是如何繪制到界面上的具體過程。
ViewRoot 對應於 ViewRootImpl 類,它是連接 WindowManager 和 DecorView 的紐帶,View的三大流程均是通過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被創建完畢後,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 對象,並將 ViewRootImpl 對象和 DecorView 建立關聯。
measure 過程決定了 View 的寬/高, Measure 完成以後,可以通過 getMeasuredWidth 和 getMeasuredHeight 方法來獲取 View 測量後的寬/高,在幾乎所有的情況下,它等同於View的最終的寬/高,但是特殊情況除外。 Layout 過程決定了 View 的四個頂點的坐標和實際的寬/高,完成以後,可以通過 getTop、getBottom、getLeft 和 getRight 來拿到View的四個頂點的位置,可以通過 getWidth 和 getHeight 方法拿到View的最終寬/高。 Draw 過程決定了 View 的顯示,只有 draw 方法完成後 View 的內容才能呈現在屏幕上。
DecorView 作為頂級 View ,一般情況下,它內部會包含一個豎直方向的 LinearLayout ,在這個 LinearLayout 裡面有上下兩個部分,上面是標題欄,下面是內容欄。在Activity中,我們通過 setContentView 所設置的布局文件其實就是被加到內容欄中的,而內容欄id為 content 。可以通過下面方法得到 content:ViewGroup content = findViewById(R.android.id.content) 。通過 content.getChildAt(0) 可以得到設置的 view 。 DecorView 其實是一個 FrameLayout , View 層的事件都先經過 DecorView ,然後才傳遞給我們的 View 。
MeasureSpec 代表一個32位的int值,高2位代表 SpecMode ,低30位代表 SpecSize , SpecMode 是指測量模式,而 SpecSize 是指在某種測量模式下的規格大小。
SpecMode 有三類,如下所示:
UNSPECIFIED
EXACTLY
AT_MOST
LayoutParams需要和父容器一起才能決定View的MeasureSpec,從而進一步決定View的寬/高。
對於頂級View,即DecorView和普通View來說,MeasureSpec的轉換過程略有不同。對於DecorView,其MeasureSpec由窗口的尺寸和其自身的LayoutParams共同確定;
對於普通View,其MeasureSpec由父容器的MeasureSpec和自身的Layoutparams共同決定;
MeasureSpec一旦確定,onMeasure就可以確定View的測量寬/高。
小結一下
當子 View 的寬高採用 wrap_content 時,不管父容器的模式是精確模式還是最大模式,子 View 的模式總是最大模式+父容器的剩餘空間。
View 的工作流程主要是指 measure 、 layout 、 draw 三大流程,即測量、布局、繪制。其中 measure 確定 View 的測量寬/高, layout 確定 view 的最終寬/高和四個頂點的位置,而 draw 則將 View 繪制在屏幕上。
measure 過程要分情況,如果只是一個原始的 view ,則通過 measure 方法就完成了其測量過程,如果是一個 ViewGroup ,除了完成自己的測量過程外,還會遍歷調用所有子元素的 measure 方法,各個子元素再遞歸去執行這個流程。
如果是一個原始的 View,那麼通過 measure 方法就完成了測量過程,在 measure 方法中會去調用 View 的 onMeasure 方法,View 類裡面定義了 onMeasure 方法的默認實現:
先看一下 getSuggestedMinimumWidth 和 getSuggestedMinimumHeight 方法的源碼:
可以看到, getMinimumWidth 方法獲取的是 Drawable 的原始寬度。如果存在原始寬度(即滿足 intrinsicWidth > 0),那麼直接返回原始寬度即可;如果不存在原始寬度(即不滿足 intrinsicWidth > 0),那麼就返回 0。
接著看最重要的 getDefaultSize 方法:
如果 specMode 為 MeasureSpec.UNSPECIFIED 即未指定模式,那麼返回由方法參數傳遞過來的尺寸作為 View 的測量寬度和高度;
如果 specMode 不是 MeasureSpec.UNSPECIFIED 即是最大模式或者精確模式,那麼返回從 measureSpec 中取出的 specSize 作為 View 測量後的寬度和高度。
看一下剛才的表格:
當 specMode 為 EXACTLY 或者 AT_MOST 時,View 的布局參數為 wrap_content 或者 match_parent 時,給 View 的 specSize 都是 parentSize 。這會比建議的最小寬高要大。這是不符合我們的預期的。因為我們給 View 設置 wrap_content 是希望View的大小剛好可以包裹它的內容。
因此:
如果是一個 ViewGroup,除了完成自己的 measure 過程以外,還會遍歷去調用所有子元素的 measure 方法,各個子元素再遞歸去執行 measure 過程。
ViewGroup 並沒有重寫 View 的 onMeasure 方法,但是它提供了 measureChildren、measureChild、measureChildWithMargins 這幾個方法專門用於測量子元素。
如果是 View 的話,那麼在它的 layout 方法中就確定了自身的位置(具體來說是通過 setFrame 方法來設定 View 的四個頂點的位置,即初始化 mLeft , mRight , mTop , mBottom 這四個值), layout 過程就結束了。
如果是 ViewGroup 的話,那麼在它的 layout 方法中只是確定了 ViewGroup 自身的位置,要確定子元素的位置,就需要重寫 onLayout 方法;在 onLayout 方法中,會調用子元素的 layout 方法,子元素在它的 layout 方法中確定自己的位置,這樣一層一層地傳遞下去完成整個 View 樹的 layout 過程。
layout 方法的作用是確定 View 本身的位置,即設定 View 的四個頂點的位置,這樣就確定了 View 在父容器中的位置;
onLayout 方法的作用是父容器確定子元素的位置,這個方法在 View 中是空實現,因為 View 沒有子元素了,在 ViewGroup 中則進行抽象化,它的子類必須實現這個方法。
1.繪制背景( background.draw(canvas); );
2.繪制自己( onDraw );
3.繪制 children( dispatchDraw(canvas) );
4.繪制裝飾( onDrawScrollBars )。
dispatchDraw 方法的調用是在 onDraw 方法之後,也就是說,總是先繪制自己再繪制子 View 。
對於 View 類來說, dispatchDraw 方法是空實現的,對於 ViewGroup 類來說, dispatchDraw 方法是有具體實現的。
通過 dispatchDraw 來傳遞的。 dispatchDraw 會遍歷調用子元素的 draw 方法,如此 draw 事件就一層一層傳遞了下去。dispatchDraw 在 View 類中是空實現的,在 ViewGroup 類中是真正實現的。
如果一個 View 不需要繪制任何內容,那麼就設置這個標記為 true,系統會進行進一步的優化。
當創建的自定義控制項繼承於 ViewGroup 並且不具備繪制功能時,就可以開啟這個標記,便於系統進行後續的優化;當明確知道一個 ViewGroup 需要通過 onDraw 繪制內容時,需要關閉這個標記。
參考:《Android開發藝術探索》
Ⅳ Android ViewRootImpl
本文主要分析兩個問題:
1、為什麼View 的繪制流程是從 ViewRootImpl 的performTraversals()方法開始的?
2、View 的invalidate方法是怎麼觸發到ViewRootImpl 的performTraversals()方法的。
在閱讀本文前,最好先了解window的添加過程,Android消息處理機制 和 View 的繪制流程。推薦先閱讀以下文章:
Android Window和WindowManager
Android-消息機制
Android View 的繪制流程
android 源碼注釋的意思是:ViewRootImpl是視圖層次結構的頂部,實現 View 和 WindowManager 之間所需的協議。是 WindowManager Global 的內部實現中重要的組成部分。
View 的繪制流程是從 ViewRootImpl 的performTraversals()方法開始的,那到底是哪裡調用了performTraversals()方法呢,下面我們分析一下:
1.私有屬性的performTraversals()方法肯定是在內部調用起來的,經過搜索找到是doTraversal()方法調用了。
2.接著找到了,調用了doTraversal() 的TraversalRunnable 類
3.內部只有一個地方實例化了TraversalRunnable 的實例mTraversalRunnable ,查到到兩個方法內都調用了mTraversalRunnable ,明顯 scheleTraversals 是主動觸發這個 Runnable 。這就表明調用了scheleTraversals ()函數的地方都主動觸發了view的刷新。
4.接著我們看一下 mChoreographer.postCallback 做了什麼
可以看到,最後後走進了scheleVsyncLocked()方法內。
5.mDisplayEventReceiver 的類 是 FrameDisplayEventReceiver,繼承自
DisplayEventReceiver 。
最後走到這里就沒了,那麼這個方法是做了什麼呢,這個方法的注釋是這個意思:安排在下一個顯示幀開始時傳送單個垂腔察直同步脈沖。意思就是,調用了這個方法可以收到系統傳送過來的垂直同步脈沖信號。Android系統每隔16ms就會發送一個VSYNC信號(VSYNC:vertical synchronization 垂直同步,幀同步),觸發對UI進行渲染。這個垂直同步信對於應用來說了,只有了訂閱了監聽,才能收到。而且是訂閱一次,收到一次。
6.既然是在這個類裡面訂閱垂直同步信號的,那回調也應該在這里。於是找到了以下方法。
native code 調用到 onVsync,這個方法的注釋解釋如下:當接收到垂直同步脈沖時調用。接收者應該渲染一個幀,然後調用 {@link scheleVsync} 來安排下一個垂直同步脈沖。
這個方法的具體實現在前面分析到的FrameDisplayEventReceiver 類裡面。
這里可以看到,其實mHandler就是當前主線程的handler,當接收到onVsync信號的時候,將自己封裝到Message中,等到Looper處理,最後Looper處理消息基圓寬的時候就會調用run方法,這里是Handler的機制,不做解釋。
7.最後,如下圖調用所示,最終從mCallbackQueues取回之前添加的任務再執行run方法,也就是TraservalRunnable的run方法。
總結上面的分析,調用流程如下圖所示如下:
上面我們分析到只要調用了ViewRootImpl 的scheleTraversals ()方法,最終就能調用了ViewRootImpl 的performTraversals()來開始繪制。搏亮那肯定是我們常調用的view刷新的介面,經過一系列的方法調用,最終調用了ViewRootImpl 的scheleTraversals ()方法。下面我們分析一下常用的View 的 invalidate()介面是怎麼調用到了ViewRootImpl 的scheleTraversals ()方法。
可以看出,invalidate有多個重載方法,但最終都會調用invalidateInternal方法,在這個方法內部,進行了一系列的判斷,判斷View是否需要重繪,接著為該View設置標記位,然後把需要重繪的區域傳遞給父容器,即調用父容器的invalidateChild方法。
接著我們看ViewGroup#invalidateChild:
由於不斷向上調用父容器的方法,到最後會調用到ViewRootImpl的invalidateChildInParent方法,我們來看看它的源碼,ViewRootImpl#invalidateChildInParent:
最後調用了scheleTraversals方法,觸發View的工作流程。至此,我們已經完整地分析了一次調用View 的 invalidate()方法到觸發ViewRootImpl 的scheleTraversals()方法。
Ⅵ Android 自定義View之Draw過程(上)
Draw 過程系列文章
Android 展示之三部曲:
前邊我們已經分析了:
這倆最主要的任務是: 確定View/ViewGroup可繪制的矩形區域。
接下來將會分析,如何在這給定的區域內繪制想要的圖形。
通過本篇文章,你將了解到:
Android 提供了關於View最基礎的兩個類:
然而ViewGroup 並沒有約定其內部的子View是如何布局的,是疊加在一起呢?還是橫向擺放、縱向擺放等。同樣的View 也沒有約定其展示的內容是啥樣,是矩形、圓形、三角形、一張圖片、一段文字抑或是不規則的形狀?這些都要我們自己去實現嗎?
不盡然,值得高興的是Android已經考慮到上述需求了,為了開發方便已經預制了一些常用的ViewGroup、View。
如:
繼承自ViewGroup的子類
繼承自View的子類
雖然以上衍生的View/ViewGroup子類已經大大為我們提供了便利,但也僅僅是通用場景下的通用控制項,我們想實現一些較為復雜的效果,比如波浪形狀進度條、會發光的球體等,這些系統控制項就無能為力了,也沒必要去預制千奇百怪的控制項。想要達到此效果,我們需要自定義View/ViewGroup。
通常來說自定義View/ViewGroup有以下幾種:
3 一般不怎麼用,除非布局比較特殊。1、2、4 是我們常用的手段,對於我們常說的"自定義View" 一般指的是 4。
接下來我們來看看 4是怎麼實現的。
在xml里引用MyView
效果如下:
黑色部分為其父布局背景。
紅色矩形+黃色圓形即是MyView繪制的內容。
以上是最簡單的自定義View的實現,我們提取重點歸納如下:
由上述Demo可知,我們只需要在重寫的onDraw(xx)方法里繪制想要的圖形即可。
來看看View 默認的onDraw(xx)方法:
發現是個空實現,因此繼承自View的類必須重寫onDraw(xx)方法才能實現繪制。該方法傳入參數為:Canvas類型。
Canvas翻譯過來一般叫做畫布,在重寫的onDraw(xx)里拿到Canvas對象後,有了畫布我們還需要一支筆,這只筆即為Paint,翻譯過來一般稱作畫筆。兩者結合,就可以愉快的作畫(繪制)了。
你可能發現了,在Demo里調用
並沒有傳入Paint啊,是不是Paint不是必須的?實際上調用該方法後,底層會自動生成Paint對象。
可以看到,底層初始化了Paint,並且給其設置的顏色為在Java層設置的顏色。
onDraw(xx)比較簡單,開局一個Canvas,效果全靠畫。
試想,這個Canvas怎麼來的呢,換句話說是誰調用了onDraw(xx)。發揮一下聯想功能,在Measure、Layout 過程有提到過兩者套路很像:
那麼Draw過程是否也是如此套路呢?看見了onDraw(xx),那麼draw(xx)還遠嗎?
沒錯,還真有draw(xx)方法:
可以看出,draw(xx)主要分為兩個部分:
不管是A分支還是B分支,都進行了好幾步的繪制。
通常來說,單一一個View的層次分為:
後面繪制的可能會遮擋前邊繪制的。
對於一個ViewGroup來說,層次分為:
來看看A分支標注的4個點:
(1)
onDraw(canvas)
前面分析過,對於單一的View,onDraw(xx)是空實現,需要由我們自定義繪制。
而對於ViewGroup,也並沒有具體實現,如果在自定義ViewGroup里重寫onDraw(xx),它會執行嗎?默認是不會執行的,相關分析請移步:
Android ViewGroup onDraw為什麼沒調用
(2)
dispatchDraw(canvas),來看看在View.java里的實現:
發現是個空實現,再看看ViewGroup.java里的實現:
也即是說,對於單一View,因為沒有子布局,因此沒必要再分發Draw,而對於ViewGroup來說,需要觸發其子布局發起Draw過程(此過程後續分析),可以類比事件分發過程View、ViewGroup的處理。感興趣的請移步:
Android 輸入事件一擼到底之View接盤俠(3)
(3)
OverLay,顧名思義就是"蓋在某個東西上面",此處是在繪制內容之後,繪制前景之前。怎麼用呢?
以上是給一個ViewGroup設置overLay,效果如下:
你可能發現了,這和設置overLay差不多的嘛,實際還是有差別的。在onDrawForeground(xx)里會重新調整Drawable的尺寸,該尺寸與View大小一致,之前給Drawable設置的尺寸會失效。運行效果如下:
可以看出,ViewGroup都被前景蓋住了。
再來看看B分支的重點:邊緣漸變效果
先來看看TextView 邊緣漸變效果:
加上這倆參數。
實際上系統自帶的一些控制項也使用了該效果,如NumberPicker、YearPickerView
以上是NumberPicker 的效果,可以看出是垂直方向漸變的。
對於View.java 里的onDraw(xx)、draw(xx),ViewGroup.java里並沒有重寫。
而對於dispatchDraw(xx),在View.java里是空實現。在ViewGroup.java里發起對子布局的繪制。
來看看標記的2點:
(1)
設置padding的目的是為了讓子布局留出一定的空隙出來,因此當設置了padding後,子布局的canvas需要根據padding進行裁減。判斷標記為:
FLAG_CLIP_TO_PADDING 默認設置為true
FLAG_PADDING_NOT_NULL 只要有padding不為0,該標記就會打上。
也就是說:只要設置了padding 不為0,子布局顯示區域需要裁減。
能不能不讓子布局裁減顯示區域呢?
答案是可以的。
考慮到一種場景:使用RecyclerView的時候,我們需要設置paddingTop = 20px,效果是:RecyclerView Item展示時離頂部有20px,但是滾動的時候永遠滾不到頂部,看起來不是那麼友好。這就是上述的裁減起作用了,需要將此動作禁止。通過設置:
當然也可以在xml里設置:
(2)
drawChild(xx)
從方法名上看是調用子布局進行繪制。
child.draw(x1,x2,x3)里分兩種情況:
這兩者具體作用與區別會在下篇文章分析,不管是硬體加速繪制還是軟體加速繪制,最終都會調用View.draw(xx)方法,該方法上面已經分析過。
注意,draw(x1,x2,x3)與draw(xx)並不一樣,不要搞混了。
用圖表示:
View/ViewGroup Draw過程的聯系:
一般來說,我們通常會自定義View,並且重寫其onDraw(xx)方法,有沒有繪制內容的ViewGroup需求呢?
是有的,舉個例子,大家可以去看看RecyclerView ItemDecoration 的繪制,其中運用到了ViewGroup draw(xx)、ViewGroup onDraw(xx) 、View onDraw(xx)繪制的先後順序來實現分割線,分組頭部懸停等功能的。
本篇文章基於 Android 10.0
Ⅶ Android 自定義View之Layout過程
系列文章:
在上篇文章: Android 自定義View之Measure過程 ,我們分析了Measure過程,本次將會掀開承上啟下的Layout過程神秘面紗,
通過本篇文章,你將了解到:
在上篇文章的比喻里,我們說過:
該ViewGroup 重寫了onMeasure(xx)和onLayout(xx)方法:
同時,當layout 執行結束,清除PFLAG_FORCE_LAYOUT標記,該標記會影響Measure過程是否需要執行onMeasure。
該View 重寫了onMeasure(xx)和onLayout(xx)方法:
MyViewGroup里添加了MyView、Button兩個控制項,最終運行的效果如下:
可以看出,MyViewGroup 里子布局的是橫向擺放的。我們重點關注Layout過程。實際上,MyViewGroup里我們只重寫了onLayout(xx)方法,MyView也是重寫了onLayout(xx)方法。
接下來,分析View Layout過程。
與Measure過程類似,連接ViewGroup onLayout(xx)和View onLayout(xx)之間的橋梁是View layout(xx)。
可以看出,最終都調用了setFrame(xx)方法。
對於Measure過程在onMeasure(xx)里記錄了尺寸的值,而對於Layout過程則在layout(xx)里記錄了坐標值,具體來說是在setFrame(xx)里,該方法兩個重點地方:
View.onLayout(xx)是空實現
從layout(xx)和onLayout(xx)聲明可知,這兩個方法都是可以被重寫的,接下來看看ViewGroup是否重寫了它們。
ViewGroup.layout(xx)雖然重寫了layout(xx),但是僅僅做了簡單判斷,最後還是調用了View.layout(xx)。
這重寫後將onLayout變為抽象方法,也就是說繼承自ViewGroup的類必須重寫onLayout(xx)方法。
我們以FrameLayout為例,分析其onLayout(xx)做了什麼。
FrameLayout.onLayout(xx)為子布局Layout的時候,起始坐標都是以FrameLayout為基準,並沒有記錄上一個子布局佔了哪塊位置,因此子布局的擺放位置可能會重疊,這也是FrameLayout布局特性的由來。而我們之前的Demo在水平方向上記錄了上一個子布局的擺放位置,下一個擺放時只能在它之後,因此就形成了水平擺放的功能。
由此類推,我們常說的某個子布局在父布局裡的哪個位置,決定這個位置的即是ViewGroup.onLayout(xx)。
上邊我們分析了View.layout(xx)、View.onLayout(xx)、ViewGroup.layout(xx)、ViewGroup.onLayout(xx),這四者什麼關系呢?
View.layout(xx)
View.onLayout(xx)
ViewGroup.layout(xx)
ViewGroup.onLayout(xx)
View/ViewGroup 子類需要重寫哪些方法:
用圖表示:
通過上述的描述,我們發現Measure過程和Layout過程里定義的方法比較類似:
它倆的套路比較類似:measure(xx)、layout(xx)一般不需要我們重寫,measure(xx)里調用onMeasure(xx),layout(xx)為調用者設置坐標值。
若是ViewGroup:onMeasure(xx)里遍歷子布局,並測量每個子布局,最後將結果匯總,設置自己測量的尺寸;onLayout(xx)里遍歷子布局,並設置每個子布局的坐標。
若是View:onMeasure(xx)則測量自身,並存儲測量尺寸;onLayout(xx)不需要做什麼。
Measure過程雖然比Layout過程復雜,但仔細分析後就會發現其本質就是為了設置兩個成員變數:
而Layout過程雖然比較簡單,其本質是為了設置坐標值
將Measure設置的變數和Layout設置的變數聯系起來:
此外,Measure過程通過設置PFLAG_LAYOUT_REQUIRED 標記來告訴需要進行onLayout,而Layout過程通過清除 PFLAG_FORCE_LAYOUT來告訴Measure過程不需要執行onMeasure了。
這就是Layout的承上作用
我們知道View的繪制需要依靠Canvas繪制,而Canvas是有作用區域限制的。例如我們使用:
Cavas繪制的起點是哪呢?
對於硬體繪制加速來說:正是通過Layout過程中設置的RenderNode坐標。
而對於軟體繪制來說:
關於硬體繪制加速/軟體繪制 後續文章會分析。
這就是Layout的啟下作用
以上即是Measure、Layout、Draw三者的內在聯系。
當然Layout的"承上"還需要考慮margin、gravity等參數的影響。具體用法參見最開始的Demo。
getMeasuredWidth()/getMeasuredHeight 與 getWidth/getHeight區別
我們以獲取width為例,分別來看看其方法:
getMeasuredWidth():獲取測量的寬,屬於"臨時值"
getWidth():獲取View真實的寬
在Layout過程之前,getWidth() 默認為0
何時可以獲取真實的寬、高
下篇將分析Draw()過程,我們將分析"一切都是draw出來的"道理
本篇基於 Android 10.0