當前位置:首頁 » 安卓系統 » androidview繪制

androidview繪制

發布時間: 2023-05-18 20:59:58

① Android:窗口、自定義view、bitmap

1、ViewRoot 對應於 ViewRootImpl 類,它是連接 WindowManager 和 DecorView 的紐帶,View 的三大流程均是通過 ViewRoot 來完成的。在 ActivityThread 中,當 Activity 對象被創建完畢後,會將 DecorView 添加到 Window 中,同時會創建 ViewRootImpl 對象,並將 ViewRootImpl 對棚彎象和 DecorView 建立關聯

2、 自定義View-繪制流程概述

4、 Android Handler
6、 Android Bitmap

2、MeasureSpec:

3、一般來說,使用多進程會造成以下幾個方面的問題:

5、Window 概念與分類:
Window 是一個抽象類,它的具體實現是 PhoneWindow。WindowManager 是外界訪問 Window 的入口,Window 的具體實現位於 WindowManagerService 中,WindowManager 和 WindowManagerService 的交互是一個 IPC 過程。Android 中所有的視圖都是通過 Window 來呈現,因此 Window 實際是 View 的直接管理者。

6、window的三大操作:addView、upView、removeView
7、 Bitmap 中有兩個內部枚舉類:

保存圖片資源:

圖片壓縮

基本使用:

8、Context 本身是一個抽象類,是對一系列系統服務介面的封裝,包括:內部伏叢資源、包、類載入、I/O操作、許可權、主線程、IPC 和組件啟動等操作的管理。ContextImpl, Activity, Service, Application 這些都是 Context 的直接或間接子類

9、SharedPreferences 採用key-value(鍵值對)形式, 主要用於輕量級的數據存儲, 尤其適合保存應用的配置參數, 但不建議使用 SharedPreferences 來存儲大規模的數據, 可能會降低性能

10、SharedPreferences源碼有用synchronize進行加鎖同步

11、Handler 有兩個主要用途:
(1)安排 Message 和 runnables 在將來的某個時刻執行;
(2)將要在不同鏈廳悶於自己的線程上執行的操作排入隊列。(在多個線程並發更新UI的同時保證線程安全。)
只有主線程能對UI進行操作,所以在對UI進行跟改之前,ViewRootImpl 對UI操作做了驗證,這個驗證工作是由 ViewRootImpl的 checkThread 方法完成:

12、ThreadLocal 是一個線程內部的數據存儲類,通過它可以在指定的線程中存儲數據,其他線程則無法獲取。Looper、ActivityThread 以及 AMS 中都用到了 ThreadLocal。當不同線程訪問同一個ThreadLocal 的 get方法,ThreadLocal 內部會從各自的線程中取出一個數組,然後再從數組中根據當前 ThreadLcoal 的索引去查找對應的value值:

13、Android 提供了幾種途徑來從其他線程訪問 UI 線程:

Android單線程模式必須遵守的規則:

14、HandlerThread 集成了 Thread,卻和普通的 Thread 有顯著的不同。普通的 Thread 主要用於在 run 方法中執行一個耗時任務,而 HandlerThread 在內部創建了消息隊列,外界需要通過 Handler 的消息方式通知 HanderThread 執行一個具體的任務。

15、IntentService 可用於執行後台耗時的任務,當任務執行後會自動停止,由於其是 Service 的原因,它的優先順序比單純的線程要高,所以 IntentService 適合執行一些高優先順序的後台任務。在實現上,IntentService 封裝了 HandlerThread 和 Handler。IntentService 第一次啟動時,會在 onCreatea 方法中創建一個 HandlerThread,然後使用的 Looper 來構造一個 Handler 對象 mServiceHandler,這樣通過 mServiceHandler 發送的消息最終都會在 HandlerThread 中執行。每次啟動 IntentService,它的 onStartCommand 方法就會調用一次,onStartCommand 中處理每個後台任務的 Intent,onStartCommand 調用了 onStart 方法。可以看出,IntentService 僅僅是通過 mServiceHandler 發送了一個消息,這個消息會在 HandlerThread 中被處理。mServiceHandler 收到消息後,會將 Intent 對象傳遞給 onHandlerIntent 方法中處理,執行結束後,通過 stopSelf(int startId) 來嘗試停止服務。(stopSelf() 會立即停止服務,而 stopSelf(int startId) 則會等待所有的消息都處理完畢後才終止服務)。

16、RecyclerView 優化

② Android自定義View ---- 畫一條線

網上有很多關沖指於三個構造函數使用時機的說法,但是說法正確的卻沒有幾家,這里正式的散猜配給大家科普一下:

詳細可以看這個博客: https://blog.csdn.net/wzy_1988/article/details/49619773

onMeasure方法是用來設置寬高的,當然也可以用來獲取寬高,獲取方法如下:

新手記得列印一下measureWidth,measureHeight。你會發現有時候measureWidth,measureHeight的值為0,所以當我們遇到0的時候不要使用。至於為什麼會有0,我們下次再寫博客講解。兆大

3.繪制一條直線

這里需要傳五個參數,分別是

效果圖如下

③ 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

④ 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 自定義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 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 - View 繪制流程

我們知道,在 Android 中,View 繪制主要包含 3 大流程:

Android 中,主要有兩種視圖: View 和 ViewGroup ,其中:

雖然 ViewGroup 繼承於 View ,但是在 View 繪制三大流程中,某些流程需要區分 View 和 ViewGroup ,它們之間的操作並不完全相同,比如:

對 View 進行測量,主要包含兩個步驟:

對於第一個步驟,即求取 View 的 MeasureSpec ,首先我們來看下 MeasureSpec 的源碼定義:

MeasureSpec 是 View 的一個公有靜態內部類,它是一個 32 位的 int 值,高 2 位表示 SpecMode(測量模式),低 30 位表示 SpecSize(測量尺寸/測量大小)。
MeasureSpec 將兩個數據打包到一個 int 值上,可以減少對象內存分配,並且其提供了相應的工具方法可以很方便地讓我們從一個 int 值中抽取出 View 的 SpecMode 和 SpecSize。

一個 MeasureSpec 表達的是:該 View 在該種測量模式(SpecMode)下對應的測量尺寸(SpecSize)。其中,SpecMode 有三種類型:

對 View 進行測量,最關鍵的一步就是計算得到 View 的 MeasureSpec ,子View 在創建時,可以指定不同的 LayoutParams (布局參數), LayoutParams 的源碼主要內容如下所示:

其中:

LayoutParams 會受到父容器的 MeasureSpec 的影響,測量過程會依據兩者之間的相互約束最終生成子View 的 MeasureSpec ,完成 View 的測量規格。

簡而言之,View 的 MeasureSpec 受自身的 LayoutParams 和父容器的 MeasureSpec 共同決定( DecorView 的 MeasureSpec 是由自身的 LayoutParams 和屏幕尺寸共同決定,參考後文)。也因此,如果要求取子View 的 MeasureSpec ,那麼首先就需要知道父容器的 MeasureSpec ,層層逆推而上,即最終就是需要知道頂層View(即 DecorView )的 MeasureSpec ,這樣才能一層層傳遞下來,這整個過程需要結合 Activity 的啟動過程進行分析。

我們知道,在 Android 中, Activity 是作為視圖組件存在,主要就是在手機上顯示視圖界面,可以供用戶操作, Activity 就是 Andorid 中與用戶直接交互最多的系統組件。

Activity 的基本視圖層次結構如下所示:

Activity 中,實際承載視圖的組件是 Window (更具體來說為 PhoneWindow ),頂層View 是 DecorView ,它是一個 FrameLayout , DecorView 內部是一個 LinearLayout ,該 LinearLayout 由兩部分組成(不同 Android 版本或主題稍有差異): TitleView 和 ContentView ,其中, TitleView 就是標題欄,也就是我們常說的 TitleBar 或 ActionBar , ContentView 就是內容欄,它也是一個 FrameLayout ,主要用於承載我們的自定義根布局,即當我們調用 setContentView(...) 時,其實就是把我們自定義的布局設置到該 ContentView 中。

當 Activity 啟動完成後,最終就會渲染出上述層次結構的視圖。

因此,如果我們要求取得到子View 的 MeasureSpec ,那麼第一步就是求取得到頂層View(即 DecorView )的 MeasureSpec 。大致過程如下所示:

經過上述步驟求取得到 View 的 MeasureSpec 後,接下來就可以真正對 View 進行測量,求取 View 的最終測量寬/高:

Android 內部對視圖進行測量的過程是由 View#measure(int, int) 方法負責的,但是對於 View 和 ViewGroup ,其具體測量過程有所差異。

因此,對於測量過程,我們分別對 View 和 ViewGroup 進行分析:

綜上,無論是對 View 的測量還是 ViewGroup 的測量,都是由 View#measure(int widthMeasureSpec, int heightMeasureSpec) 方法負責,然後真正執行 View 測量的是 View 的 onMeasure(int widthMeasureSpec, int heightMeasureSpec) 方法。

具體來說, View 直接在 onMeasure(...) 中測量並設置自己的最終測量寬/高。在默認測量情況下, View 的測量寬/高由其父容器的 MeasureSpec 和自身的 LayoutParams 共同決定,當 View 自身的測量模式為 LayoutParams.UNSPECIFIED 時,其測量寬/高為 android:minWidth / android:minHeight 和其背景寬/高之間的較大值,其餘情況皆為自身 MeasureSpec 指定的測量尺寸。

而對於 ViewGroup 來說,由於布局特性的豐富性,只能自己手動覆寫 onMeasure(...) 方法,實現自定義測量過程,但是總的思想都是先測量 子View 大小,最終才能確定自己的測量大小。

當確定了 View 的測量大小後,接下來就可以來確定 View 的布局位置了,也即將 View 放置到屏幕具體哪個位置。

View 的布局過程由 View#layout(...) 負責,其源碼如下:

View#layout(...) 主要就做了兩件事:

ViewGroup 的布局流程由 ViewGroup#layout(...) 負責,其源碼如下:

可以看到, ViewGroup#layout(...) 最終也是通過 View#layout(...) 完成自身的布局過程,一個注意的點是, ViewGroup#layout(...) 是一個 final 方法,因此子類無法覆寫該方法,主要是 ViewGroup#layout(...) 方法內部對子視圖動畫效果進行了相關設置。

由於 ViewGroup#layout(...) 內部最終調用的還是 View#layout(...) ,因此, ViewGroup#onLayout(...) 就會得到回調,用於處理 子View 的布局放置,其源碼如下:

由於不同的 ViewGroup ,其布局特性不同,因此 ViewGroup#onLayout(...) 是一個抽象方法,交由 ViewGroup 子類依據自己的布局特性,擺放其 子View 的位置。

當 View 的測量大小,布局位置都確定後,就可以最終將該 View 繪制到屏幕上了。

View 的繪制過程由 View#draw(...) 方法負責,其源碼如下:

其實注釋已經寫的很清楚了, View#draw(...) 主要做了以下 6 件事:

我們知道,在 Activity 啟動過程中,會調用到 ActivityThread.handleResumeActivity(...) ,該方法就是 View 視圖繪制的起始之處:

可以看到, ActivityThread.handleResumeActivity(...) 主要就是獲取到當前 Activity 綁定的 ViewManager ,最後調用 ViewManager.addView(...) 方法將 DecorView 設置到 PhoneWindow 上,也即設置到當前 Activity 上。 ViewManager 是一個介面, WindowManager 繼承 ViewManager ,而 WindowManagerImpl 實現了介面 WindowManager ,此處的 ViewManager.addView(...) 實際上調用的是 WindowManagerImpl.addView(...) ,源碼如下所示:

WindowManagerImpl.addView(...) 內部轉發到 WindowManagerGlobal.addView(...) :

在 WindowManagerGlobal.addView(...) 內部,會創建一個 ViewRootImpl 實例,然後調用 ViewRootImpl.setView(...) 將 ViewRootImpl 與 DecorView 關聯到一起:

ViewRootImpl.setView(...) 內部首先關聯了傳遞過來的 DecorView (通過屬性 mView 指向 DecorView 即可建立關聯),然後最終調用 requestLayout() ,而 requestLayout() 內部又會調用方法 scheleTraversals() :

ViewRootImpl.scheleTraversals() 內部主要做了兩件事:

Choreographer.postCallback(...) 會申請一次 VSYNC 中斷信號,當 VSYNC 信號到達時,便會回調 Choreographer.doFrame(...) 方法,內部會觸發已經添加的回調任務, Choreographer 的回調任務有以下四種類型:

因此, ViewRootImpl.scheleTraversals(...) 內部通過 mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null) 發送的非同步視圖渲染消息就會得到回調,即回調 mTra

⑧ 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即可

⑨ Android自定義View(3) 《Canvas繪制簡單的圖形》

在Android中,我們經常會需要去繪制一些自己需要的控制項,所以繼承自View的自定義View就產生了。這篇文章主要介紹在View中的重要類,Canvas類。

Canvas是自定義View中幾個最重要的類之一,可以理解為畫布,但是我理解下來,這只是單一的圖層,每次Canvas繪制時其實都是在canvas類所定義出的一個圖層上進行繪制圖形,一旦canvas變化,則圖層也發生變化,圖層變化之前繪制的結果不變,但之後的繪制結果會以新的圖層為基準進行繪制

在這里繪制線條調用Canvas.drawLine(float startX, float startY, float stopX, float stopY,Paint paint)方法,參數分別如下

繪制矩形調用canvas.drawRect(RectF rect, Paint paint),矩形Rect的構造參數的四個參數分別為左邊界,上邊界,右邊界,下邊界的值,單位為px,RectF與Rect不同之處在於構造參數的類型一個全為float,一個全為int,運行結果如下

繪制橢圓主要調用drawOval(RectF oval, Paint paint),以一個矩形的邊界來繪制一個橢圓,運行結果如下

繪制圓主要使用Canvas.drawCircle(float cx, float cy, float radius, Paint paint),

繪制Path的方法比較多,這里只簡單示例繪制線條路徑構建一個三角形的用法,後續會再詳細介紹繪制別的類型路徑的用法,該例運行結果如下

這里我們繪制了一段文字和一根線段這里是為了演示出Canvas中的文字的繪制方法只能實現水平方向上的居中顯示,垂直方向上的垂直是需要我們再進行實現的,繪制文字有很多個,我們這里只簡單調用了設置起始點的繪制方法,關於文字的繪制其實內容還是比較多的,這里我們就只簡單的介紹,後續有時間會專門出一個文字繪制的文章,該示例運行結果如下

今天總結了Canvas中一些簡單圖形的繪制,下篇我們介紹Canvas中的一些其他內容。

⑩ Android自定義View(8) 《繪制文本》

在Android中,我們經常會需要去繪制一些自己需要的控制項,所以繼承自View的自定義View就和升畝產生了。這篇文章主要介紹如何在我們的自定義View中繪制Text

先舉例是為了更清楚的看到繪制text時的幾個重要的基準線,上圖運行結果中總共有5條線,它們從上至下分別是

這里參數很笑譽容易看懂,但是這個x,y分別意味著啥呢,它就是繪制文本的基準

除了baseline是我們定義的,其他的幾個參考線都是通過我們設置的文字大小來變化的,所以我們在繪制時一定要記住我們繪制文字的基喚森准線是baseline而不是我們的文字框的最底端,記住這一點在我們繪制文字居中是非常有用的。

今天的繪制文字就總結到這里吧,下篇再學別的有意思的~

熱點內容
搭建自己的伺服器直播間 發布:2025-02-13 11:27:34 瀏覽:986
酷狗音樂試聽緩存刪了會怎樣 發布:2025-02-13 11:02:12 瀏覽:267
python游戲服務端 發布:2025-02-13 11:00:19 瀏覽:927
雲原生伺服器 發布:2025-02-13 10:55:34 瀏覽:827
linuxip命令查看ip 發布:2025-02-13 10:49:45 瀏覽:421
java基礎應用 發布:2025-02-13 10:44:53 瀏覽:711
linux內核搶占 發布:2025-02-13 10:36:32 瀏覽:890
家裝公司源碼 發布:2025-02-13 10:35:35 瀏覽:49
aspnet更新資料庫 發布:2025-02-13 10:35:34 瀏覽:385
海爾壓縮機不工作 發布:2025-02-13 10:15:32 瀏覽:224