做爲系列文章的第九篇,本篇主要深刻了解 Widget 中繪製相關的原理,探索 Flutter 裏的 RenderObject 最後是如何走完屏幕上的最後一步,結尾再經過實際例子理解如何設計一個 Flutter 的自定義繪製。node
前文:git
在第6、第七篇中咱們知道了 Widget
、Element
、RenderObject
的關係,同時也知道了Widget
的佈局邏輯,最終全部 Widget
都轉化爲 RenderObject
對象, 它們堆疊出咱們想要的畫面。github
因此在 Flutter 中,最終頁面的 Layout
、Paint
等都會發生在 Widget 所對應的 RenderObject
子類中,而 RenderObject
也是 Flutter 跨平臺的最大的特色之一:全部的控件都與平臺無關 ,這裏簡單的人話就是: Flutter 只要求系統提供的 「Canvas」,而後開發者經過 Widget 生成 RenderObject
「直接」 經過引擎繪製到屏幕上。canvas
ps 從這裏開始篇幅略長,可能須要消費您的一點耐心。bash
咱們知道 Widget
最終都轉化爲 RenderObject
, 因此瞭解繪製咱們直接先看 RenderObject
的 paint
方法。ide
以下圖所示,全部的 RenderObject
子類都必須實現 paint
方法,而且該方法並非給用戶直接調用,須要更新繪製時,你能夠經過 markNeddsPaint
方法去觸發界面繪製。佈局
那麼,按照「國際流程」,在經歷大小和佈局等位置計算以後,最終 paint
方法會被調用,該方法帶有兩個參數: PaintingContext
和 Offset
,它們就是完成繪製的關鍵所在,那麼相信此時你們確定有個疑問就是:post
PaintingContext
是什麼?Offset
是什麼?經過飛速查閱源碼,咱們能夠首先了解到有 :測試
PaintingContext
的關鍵是 A place to paint ,同時它在父類 ClipContext
是包含有 Canvas
,而且 PaintingContext
的構造方法是 @protected
,只在 PaintingContext.repaintCompositedChild
和 pushLayer
時自動建立。動畫
Offset
在 paint
中主要是提供當前控件在屏幕的相對偏移值,提供繪製時肯定繪製的座標。
OK,繼續往下走,那麼既然 PaintingContext
叫 Context ,那它確定是存在上下文關係,那它是在哪裏開始建立的呢?
經過調試源碼可知,項目在 runApp
時經過 WidgetsFlutterBinding
啓動,而在之前的篇幅中咱們知道, WidgetsFlutterBinding
是一個「膠水類」,它會觸發 mixin 的 RendererBinding
,以下圖建立出根 node 的 PaintingContext
。
好了,那麼Offset
呢?以下圖,對於 Offset
的傳遞,是經過父控件和子控件的 offset 相加以後,一級一級的將須要繪製的座標結合去傳遞的。
目前簡單來講,經過 PaintingContext
和 Offset
,在佈局以後咱們就能夠在屏幕上準確的地方繪製會須要的畫面。
這裏咱們先作一個有趣的測試。
咱們如今屏幕上經過 Container
限制一個高爲 60 的綠色容器,以下圖,暫時忽略容器內的 Slider
控件 ,咱們圖中繪製了一個 100 x 100 的紅色方塊,這時候咱們會看到下圖右邊的效果是:納尼?爲何只有這麼小?
事實上,由於正常 Flutter 在繪製 Container
的時候,AppBar
已經幫咱們計算了狀態欄和標題欄高度誤差,但咱們這裏在用 Canvas
時直接粗暴的 drawRect
,繪製出來的紅色小方框,左部和頂部起點均爲0,實際上是從狀態欄開始計算繪製的。
那若是咱們調整位置呢?把起點 top 調整到 300,出現了以下圖的效果:納尼?紅色小方塊竟然畫出去了,明明 Container
只有綠色的大小。
其實這裏的問題仍是在於 PaintingContext
,它有一個參數是 estimatedBounds
,而 estimatedBounds
正常是在建立時經過 child.paintBounds
賦值的,可是對於 estimatedBounds
還有以下的描述:原來畫出去也是能夠。
The canvas will allow painting outside these bounds.
The [estimatedBounds] rectangle is in the [canvas] coordinate system.
複製代碼
因此到這裏你能夠通俗的總結, 對於 Flutter 而言,整個屏幕都是一塊畫布,咱們經過各類 Offset
和 Rect
肯定了位置,而後經過 PaintingContext
的Canvas
繪製上去,目標是整個屏幕區域,整個屏幕就是一幀,每次改變都是從新繪製。
固然,每次從新繪製並非徹底從新繪製 ,這裏面實際上是存在一些規制的。
還記得前面的 markNeedsPaint
方法嗎 ?咱們先從 markNeedsPaint()
開始, 總結出其大體流程以下圖,能夠看到 markNeedsPaint
在 requestVisualUpdate
時確實觸發了引擎去更新繪製界面。
接着咱們看源碼,如源碼所示,當調用 markNeedsPaint()
時,RenderObject
就會往上的父節點去查找,根據 isRepaintBoundary
是否爲 true,會決定是否從這裏開始去觸發重繪。換個說法就是,肯定要更新哪些區域。
因此其實流程應該是:經過isRepaintBoundary
往上肯定了更新區域,經過 requestVisualUpdate
方法觸發更新往下繪製。
而且從源碼中能夠看出, isRepaintBoundary
只有 get
,因此它只能被子類 override
,由子類代表是不是爲重繪的邊緣,好比 RenderProxyBox
、RenderView
、RenderFlow
等 RenderObject
的 isRepaintBoundary
都是 true。
因此若是一個區域繪製很頻繁,且能夠不影響父控件的狀況下,其實能夠將 override isRepaintBoundary
爲 true。
上文咱們知道了,當 isRepaintBoundary
爲 true 時,那麼該區域就是一個可更新繪製區域,而當這個區域造成時, 其實就會新建立一個 Layer
。
不一樣的 Layer
下的 RenderObject
是能夠獨立的工做,好比 OffsetLayer
就在 RenderObject
中用到,它就是用來作定位繪製的。
同時這也引生出了一個結論:不是每一個 RenderObject
都具備 Layer
的,由於這受 isRepaintBoundary
的影響。
其次在 RenderObject
中還有一個屬性叫 needsCompositing
,它會影響生成多少層的 Layer
,而這些 Layer
又會組成一棵 Layer Tree 。好吧,到這裏又多了一個樹,實際上這顆樹纔是所謂真正去給引擎繪製的樹。
到這裏咱們大概就瞭解了 RenderObject
的整個繪製流程,而且這個繪製時機咱們是去「觸發」的,而不是主動調用,而且更新是判斷區域的。 嗯~有點 React 的味道!
前面咱們講了那麼多繪製的流程,如今讓咱們從 Slider
這個控件的源碼,去看看一個繪製控件的設計實現吧。
整個 Slider
的實現能夠說是很 Flutter
了,大致結構以下圖。
在 _RenderSlider
中,除了 手勢 和 動畫 以外,其他的每一個繪製的部分,都是獨立的 Component 去完成繪製,而這些 Component 都是經過 SliderTheme
的 SliderThemeData
提供的。
巧合的是,SliderTheme
自己就是一個 InheritedWidget
。看過之前篇章的同窗應該會知道, InheritedWidget
通常就是用於作狀態共享的,因此若是你須要自定義 Slider
,完成能夠經過 SliderTheme
嵌套,而後經過 SliderThemeData
選擇性的自定義你須要的模塊。
而且以下圖,在 _RenderSlider
中註冊時手勢和動畫,會在監聽中去觸發 markNeedsPaint
方法,這就是爲何你的觸摸可以響應畫面的緣由了。
同時能夠看到 _SliderRender
內的參數都重寫了 get
、 set
方法, 在 set
時也會有 markNeedsPaint()
,或者調用 _updateLabelPainter
去間接調用 markNeedsLayout
。
至於 Slider
內的各類 Shape 的繪製這裏就不展開了,都是 Canvas
標準的 pathTo
、drawRect
、translate
、drawPath
等熟悉的操做了。
自此,第九篇終於結束了!(///▽///)
《Flutter完整開發實戰詳解(1、Dart語言和Flutter基礎)》
《Flutter完整開發實戰詳解(4、Redux、主題、國際化)》
《Flutter完整開發實戰詳解(6、 深刻Widget原理)》
《Flutter完整開發實戰詳解(10、 深刻圖片加載流程)》