Flutter完整開發實戰詳解(9、 深刻繪製原理)

做爲系列文章的第九篇,本篇主要深刻了解 Widget 中繪製相關的原理,探索 Flutter 裏的 RenderObject 最後是如何走完屏幕上的最後一步,結尾再經過實際例子理解如何設計一個 Flutter 的自定義繪製。node

前文:git

在第6、第七篇中咱們知道了 WidgetElementRenderObject 的關係,同時也知道了Widget 的佈局邏輯,最終全部 Widget 都轉化爲 RenderObject 對象, 它們堆疊出咱們想要的畫面。github

因此在 Flutter 中,最終頁面的 LayoutPaint 等都會發生在 Widget 所對應的 RenderObject 子類中,而 RenderObject 也是 Flutter 跨平臺的最大的特色之一:全部的控件都與平臺無關 ,這裏簡單的人話就是: Flutter 只要求系統提供的 「Canvas」,而後開發者經過 Widget 生成 RenderObject 「直接」 經過引擎繪製到屏幕上。canvas

ps 從這裏開始篇幅略長,可能須要消費您的一點耐心。bash

1、繪製過程

咱們知道 Widget 最終都轉化爲 RenderObject , 因此瞭解繪製咱們直接先看 RenderObjectpaint 方法。ide

以下圖所示,全部的 RenderObject 子類都必須實現 paint 方法,而且該方法並非給用戶直接調用,須要更新繪製時,你能夠經過 markNeddsPaint 方法去觸發界面繪製。佈局

image.png

那麼,按照「國際流程」,在經歷大小和佈局等位置計算以後,最終 paint 方法會被調用,該方法帶有兩個參數: PaintingContextOffset ,它們就是完成繪製的關鍵所在,那麼相信此時你們確定有個疑問就是:post

  • PaintingContext 是什麼?
  • Offset 是什麼?

經過飛速查閱源碼,咱們能夠首先了解到有 :測試

  • PaintingContext 的關鍵是 A place to paint ,同時它在父類 ClipContext 是包含有 Canvas ,而且 PaintingContext 的構造方法是 @protected,只在 PaintingContext.repaintCompositedChildpushLayer 時自動建立。動畫

  • Offsetpaint 中主要是提供當前控件在屏幕的相對偏移值,提供繪製時肯定繪製的座標。

OK,繼續往下走,那麼既然 PaintingContext 叫 Context ,那它確定是存在上下文關係,那它是在哪裏開始建立的呢?

經過調試源碼可知,項目在 runApp 時經過 WidgetsFlutterBinding 啓動,而在之前的篇幅中咱們知道, WidgetsFlutterBinding 是一個「膠水類」,它會觸發 mixinRendererBinding ,以下圖建立出根 node 的 PaintingContext

好了,那麼Offset 呢?以下圖,對於 Offset 的傳遞,是經過父控件和子控件的 offset 相加以後,一級一級的將須要繪製的座標結合去傳遞的。

目前簡單來講,經過 PaintingContextOffset ,在佈局以後咱們就能夠在屏幕上準確的地方繪製會須要的畫面。

一、測試繪製

這裏咱們先作一個有趣的測試。

咱們如今屏幕上經過 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 而言,整個屏幕都是一塊畫布,咱們經過各類 OffsetRect 肯定了位置,而後經過 PaintingContextCanvas 繪製上去,目標是整個屏幕區域,整個屏幕就是一幀,每次改變都是從新繪製。

二、RepaintBoundary

固然,每次從新繪製並非徹底從新繪製 ,這裏面實際上是存在一些規制的。

還記得前面的 markNeedsPaint 方法嗎 ?咱們先從 markNeedsPaint() 開始, 總結出其大體流程以下圖,能夠看到 markNeedsPaintrequestVisualUpdate 時確實觸發了引擎去更新繪製界面。

繪製大體流程圖

接着咱們看源碼,如源碼所示,當調用 markNeedsPaint() 時,RenderObject 就會往上的父節點去查找,根據 isRepaintBoundary 是否爲 true,會決定是否從這裏開始去觸發重繪。換個說法就是,肯定要更新哪些區域。

因此其實流程應該是:經過isRepaintBoundary 往上肯定了更新區域,經過 requestVisualUpdate 方法觸發更新往下繪製。

markNeedsPaint

而且從源碼中能夠看出, isRepaintBoundary 只有 get ,因此它只能被子類 override ,由子類代表是不是爲重繪的邊緣,好比 RenderProxyBoxRenderViewRenderFlowRenderObjectisRepaintBoundary 都是 true。

因此若是一個區域繪製很頻繁,且能夠不影響父控件的狀況下,其實能夠將 override isRepaintBoundary 爲 true。

三、Layer

上文咱們知道了,當 isRepaintBoundary 爲 true 時,那麼該區域就是一個可更新繪製區域,而當這個區域造成時, 其實就會新建立一個 Layer

不一樣的 Layer 下的 RenderObject 是能夠獨立的工做,好比 OffsetLayer 就在 RenderObject 中用到,它就是用來作定位繪製的。

同時這也引生出了一個結論:不是每一個 RenderObject 都具備 Layer 的,由於這受 isRepaintBoundary 的影響。

其次在 RenderObject 中還有一個屬性叫 needsCompositing ,它會影響生成多少層的 Layer ,而這些 Layer 又會組成一棵 Layer Tree 。好吧,到這裏又多了一個樹,實際上這顆樹纔是所謂真正去給引擎繪製的樹。

到這裏咱們大概就瞭解了 RenderObject 的整個繪製流程,而且這個繪製時機咱們是去「觸發」的,而不是主動調用,而且更新是判斷區域的。 嗯~有點 React 的味道!

2、Slider 控件的繪製實現

前面咱們講了那麼多繪製的流程,如今讓咱們從 Slider 這個控件的源碼,去看看一個繪製控件的設計實現吧。

整個 Slider 的實現能夠說是很 Flutter 了,大致結構以下圖。

_RenderSlider 中,除了 手勢動畫 以外,其他的每一個繪製的部分,都是獨立的 Component 去完成繪製,而這些 Component 都是經過 SliderThemeSliderThemeData 提供的。

巧合的是,SliderTheme 自己就是一個 InheritedWidget 。看過之前篇章的同窗應該會知道, InheritedWidget 通常就是用於作狀態共享的,因此若是你須要自定義 Slider ,完成能夠經過 SliderTheme 嵌套,而後經過 SliderThemeData 選擇性的自定義你須要的模塊。

而且以下圖,在 _RenderSlider 中註冊時手勢和動畫,會在監聽中去觸發 markNeedsPaint 方法,這就是爲何你的觸摸可以響應畫面的緣由了。

同時能夠看到 _SliderRender內的參數都重寫了 getset 方法, 在 set 時也會有 markNeedsPaint() ,或者調用 _updateLabelPainter 去間接調用 markNeedsLayout

image.png

至於 Slider 內的各類 Shape 的繪製這裏就不展開了,都是 Canvas 標準的 pathTodrawRecttranslatedrawPath等熟悉的操做了。

自此,第九篇終於結束了!(///▽///)

資源推薦

完整開源項目推薦:
文章

《Flutter完整開發實戰詳解(1、Dart語言和Flutter基礎)》

《Flutter完整開發實戰詳解(2、 快速開發實戰篇)》

《Flutter完整開發實戰詳解(3、 打包與填坑篇)》

《Flutter完整開發實戰詳解(4、Redux、主題、國際化)》

《Flutter完整開發實戰詳解(5、 深刻探索)》

《Flutter完整開發實戰詳解(6、 深刻Widget原理)》

《Flutter完整開發實戰詳解(7、 深刻佈局原理)》

《Flutter完整開發實戰詳解(8、 實用技巧與填坑)》

《Flutter完整開發實戰詳解(9、 深刻繪製原理)》

《Flutter完整開發實戰詳解(10、 深刻圖片加載流程)》

《Flutter完整開發實戰詳解(11、全面深刻理解Stream)》

《跨平臺項目開源項目推薦》

《移動端跨平臺開發的深度解析》

《React Native 的將來與React Hooks》

咱們還會再見嗎?
相關文章
相關標籤/搜索