?
?Flutter Web 穩定版本發布至今也有一年多了,經過這一年多的發展,今天就讓我們來看看 Flutter Web 究竟有什么不同之處,本篇分享主要內容是目前 Flutter 下少有較為全面的 Web 內容。 ? ?一、起源與實現
?說起 Flutter 的起源就很有意思,大家都知道早期 Flutter 最先支持的平臺是 Android 和 iOS,至今最核心的維護平臺依然是 Android 和 iOS,但是事實上 Flutter 其實起源于前端團隊。 ??另外前端的同學應該知道,Dart 起初也是為了 Web 而生,事實上?Dart 誕生至今也有 10 年了,所以可以說 Flutter 其實充滿了 Web 的基因。 ?但是作為從 Web 里誕生的框架,和 React Native/ Weex 不同的是,前者是先有了 Web 下的 React 和 Vue 實現之后才有的客戶端支持,而對于 Flutter 則是反過來,先有客戶端實現之后才支持 Web 平臺,這里其實可以和 Weex 做個簡單對照。 ?Weex 作為曾經閃耀過的跨平臺框架,它同樣支持 Android、iOS 和 Web 三個平臺,在 Android 和 iOS 上 Weex 和 React Native 差異性不大,在 Web 上 Weex 則是刪減版的 Vue 支持,而由于 API 和平臺差異性的問題,Weex 在 Web 上的支持體驗一直不是很好:?Flutter 來源于前端 Chrome 團隊,起初 Flutter 的創始人和整個團隊幾乎都是來自 Web,在 Flutter 負責人 Eric 的相關訪談中,Eric 表示 Flutter 來自 Chrome 內部的一個實驗,他們把一些亂七八糟的 Web 規范去掉后,在一些內部基準測試的性能居然能提升 20?倍,因此 Google 內部就開始立項,所以 Flutter 出現了。
?
?因為 Weex 需要依賴平臺控件實現渲染,導致一個 Text 控件需要兼顧 Android、iOS 和 Web 上原生平臺接口的邏輯,從而出現各種由于耦合帶來的兼容性問題。
而 Flutter 實現更為特別,通過 Skia 實現了獨立的渲染引擎之后,在 Android 和 iOS 上控件幾乎就與平臺無關,所以 Flutter 上的控件可以做到獨立且不同平臺上渲染一致的效果。
但是回到 Web 上又有些特殊,首先 Web 平臺完全是 html / js / css 的天下,并且 Web 平臺需要同時兼顧 PC 和 Mobile 的不同環境,這就讓 Flutter Web 成了 Flutter 所有平臺里 "最另類又奇葩" 的落地。
?
首先 Flutter Web 和其他 Flutter 平臺一樣共用一套 Framework,理論上絕大多數的控件實現都是通用的,當然如果要說最不兼容的 API 對象,那肯定就是 Canvas 了,這其實和 Flutter Web 特殊的實現有關系,后面我們會聊到這個問題。
?
而由于 Web 的特殊場景,Flutter Web 在 "幾經周折" 之后落地了兩種不同的渲染邏輯:?html 和 canvaskit,它們的不同之處在于:?
- html
-
好處:?html 的實現更輕量級,渲染實現基本依賴于 Web 平臺的各種 HTMLElement,特別是 Flutter Web 下定義的各種
實現,可以說它更貼近現在的 Web 環境,所以有時候我們也稱呼它為 DomCanvas,當然隨著 Flutter Web 的發展這個稱呼也發生了一些變化,后續我們會詳細講到這個。 - 問題: html 的問題也在于太過于貼近 Web 平臺,這就和 Weex 一樣,貼近平臺也就是耦合于平臺,事實上 DomCanvas 實現理念其實和 Flutter 并不貼切,也導致了 Flutter Web 的一些渲染效果在 html 模式下存在兼容問題,特別是?Canvas 的 API。 ?
- canvaskit
- 好處: canvaskit 的實現可以說是更貼近 Flutter 理念,因為它其實就是 Skia + WebAssembly 的實現邏輯,能和其他平臺的實現更一致,性能更好,比如滾動列表的渲染流暢度更高等。
- 問題: 很明顯使用 WebAssembly 帶來的 wasm 文件會導致體積增大不少,Web 場景下其實很講究加載速度,而在這方面 wasm 能優化的空間很小,并且 WebAssembly 在兼容上也是相對較差,另外 skia 還需要自帶字體庫等問題都挺讓人頭痛。 ?
默認情況下 Flutter Web 在打包渲染時會把 html 和 canvaskit 都打包進去,然后在 PC 端使用 canvaskit 模式,在 mobile 端使用 html 模式,當然您也可以在打包時通過 flutter build web --web-renderer html --release 之類的配置強行指定渲染模式。
?
既然這里我們講到了 Flutter Web 的打包構建,那就讓我們先從構建打包角度開始來深入介紹 Flutter Web。 ? ?二、構建和優化
?Flutter Web 雖說是和其他平臺共用一個 framework,但是它在 dart 層開始就有一套自己特殊的 engine 實現,并且這套實現是獨立于 framework 的一套特殊代碼。
?
所以在 Flutter Web 打包時,會把默認的? /flutter/bin/cache/lib/_engine 變成了 flutter/bin/cache/flutter_web_sdk/lib/_engine 的相關實現,這是因為 Flutter Web 在 framework 之下的 engine 需要一套特殊的 API。
?
下圖右側構建是指定 web 的打包路徑,和左邊默認時的對比。
?
同樣下圖所示,可以看到 web sdk 里會有如 html、canvaskit 這樣不同的實現,甚至會有一個特殊的 text 目錄,這是因為在 web 上對于文本的支持是個十分復雜的問題。
那到這里我們知道了在?_engine 層面,Flutter Web 有著自己一套獨立的實現,那構建之后的產物是什么樣的情況呢?
?
如下圖所示是 GSY 的一個簡單的開源示例項目,在部署到服務器后可以看到,默認情況下在不做任何處理時,在 PC 端打開后會使用 canvaskit 渲染,主要會有:? ?
可以看到這些文件占據了 Flutter Web 編譯后產物的大部分體積,并且從大小上看確實讓人有些無法接受,因為示例項目的代碼量并不大,結構也不復雜,這樣的體積肯定十分影響加載速度。
?
所以我們首先考慮在 html 和 canvaskit 兩種渲染模式中先選定一種,出于實用性考慮,結合前面的對比情況,選用 html 渲染模式在兼容性和可優化上會更友好,所以這里優化的第一步就是先指定 html 模式作為渲染引擎。
?
開始優化首先可以看到 CupertinoIcons.ttf 這個矢量圖標文件,雖然默認創建項目時會通過 cupertino_icons 被添加到項目里,但是由于我們不需要使用,所以可以在 yaml 文件里去除。
?
之后通過運行 flutter build web --release --web-renderer html 后,可以看到使用 html 模式加載后的產物很干凈,而需要優化的體積現在主要在 main.dart.js 和 MaterialIcons-Regular.otf 上。
?
?
雖然在項目中我們會使用到 MaterialIcons 的一些矢量圖標,但是每次加載都要全量加載一個 1.5 MB 的字體庫文件顯然并不符合邏輯,所以在 Flutter 里官方提供了 --tree-shake-icons 的命令幫助我們優化這部分的內容。
?
但是不幸的是,如下圖所示,在當前的 2.10 版本下該配置運行會有 bug,而不幸中的萬幸是,在原生平臺的編譯中 shake-icons 行為是可以正常執行。
?
?
所以我們可以先運行 flutter build apk,然后通過如下命令,將 Android 上已經 shake-icons 的 MaterialIcons-Regular.otf 資源復制到已經編譯好的 web/ 目錄下。
- ?
cp?-r?./build/app/intermediates/flutter/release/flutter_assets/?./build/web/assets
?再次打包后可以看到,經過優化后 MaterialIcons-Regular.otf 資源如今只剩下 3.2 kB,那接下來就是考慮針對 2.2 MB 的 main.dart.js 進行優化處理。
?
?
要優化 main.dart.js,我們就要講到 Flutter 里的?deferred-components,在 Flutter 里可以通過把控件定義為 "deferred component"?來實現控件的懶加載,而這個行為在 Flutter Web 上被編譯之后就會變成多個 *part.js 文本,原理上就是對 main.dart.js 進行拆包。
?
舉個例子,首先我們定義一個普通的 Flutter 控件,按照正常的控件進行實現就可以。
?import 'package:flutter/widgets.dart';
class DeferredBox extends StatelessWidget {
DeferredBox() {}
Widget build(BuildContext context) {
return Container(
height: 30,
width: 30,
color: Colors.blue,
);
}
}
在需要的地方 import 對應控件然后添加 deferred as box 關鍵字,之后在適當時機通過 box.loadLibrary() 加載控件,最后通過 box.DeferredBox() 渲染。
?import 'box.dart' deferred as box;
class MainPage extends StatefulWidget {
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
void initState() {
super.initState();
}
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: box.loadLibrary(),
builder: (BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
return box.DeferredBox();
}
return CircularProgressIndicator();
},
);
}
}
當然,這里還需要額外在 ymal 文件里添加 deferred-components 來制定對應的 libraries 路徑。
?deferred-components:
- name: crane
libraries:
- package:gsy_flutter_demo/widget/box.dart
回歸到上面的 GSY 示例項目中,通過相對極端的分包實現,這里把 GSY 示例里的每個頁面都變成一個獨立的懶加載頁面,然后在頁面跳轉時再加載顯示,最終打包部署后如下圖所示:?
?
?
可以看到拆分之后 main.dart.js 從 2.2 MB 變成了 1.6 MB,而其他內容通過 deferred components 變成了各個 part.js 的獨立文件,并且只在點擊時才動態下載對應的 part.js 文件,但是此時的 main.dart.js 依舊不小,而官方提供的能力上已經沒有太多優化的余地。
?
在這里可以通過前端的 source-map-explorer 工具去分析這個文件,首先在編譯時要添加 --source-maps 命令,這樣在打包時會生成 main.dart.js 的 source map 文件,然后就執行 source-map-explorer main.dart.js --no-border-checks ?生成對應的分析圖:?
?
?
這里只展示能夠被 mapped 的部分,可以看到 700k 幾乎就是 Flutter Web 整個 framewok + engine + vm 的大小,而這部分內容其實可以優化的空間并不大,盡管會有一些如 kIsWeb 的冗余代碼,但是其實可以調整的內容并不多,大概有 36 處可以調整和刪減的地方,實質上打包時 Flutter Web 也都有相應的優化壓縮處理,所以這部分收益并不高。
?
?
另外,如下圖所示是兩種不同 web rendder 構建后代碼上的差異,可以看到 html 和 canvaskit 單獨構建后的 engine 代碼結構差異性還是很大的。
?
而如果您在編譯時默認的 auto 模式,就會看到 html 和 canvaskit 的代碼都會打包進去,所以相對的 main.dart.js 也會增加一些。
?
?
那還有什么可以優化的地方嗎?還是有的,通過外部手段,例如通過在部署時開啟 gzip 或者 brotli 壓縮,如下圖所示,開始 gzip 后大概可以讓 main.dart.js 下降到 400k 左右。
?
?
另外也有在 index.html 里增加 loading 效果來做等待加載過程的展示,例如:?
?所以大致上以上這些就是今天關于 Flutter Web 上產物體積的優化,總結起來就是:?<html>
<head>
<meta charset="UTF-8">
<title>gsy_flutter_demotitle>
<style>
.loading {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
-ms-transform: translate(-50%, -50%);
transform: translate(-50%, -50%);
}
.loader {
border: 16px solid #f3f3f3;
border-radius: 50%;
border: 15px solid ;
border-top: 16px solid blue;
border-right: 16px solid white;
border-bottom: 16px solid blue;
border-left: 16px solid white;
width: 120px;
height: 120px;
-webkit-animation: spin 2s linear infinite;
animation: spin 2s linear infinite;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
style>
head>
<body>
<div class="loading">
<div class="loader">div>
div>
<script src="main.dart.js" type="application/javascript">script>
body>
html>
-
去除無用的 icon 引用;
-
使用 tree-shake-icons 優化引用矢量圖庫;
-
通過 deferred-components 實現懶加載分包;
-
開啟 gzip 等壓縮算法壓縮?main.dart.js。
?
?
三、渲染
?
講完構建,最后我們聊聊渲染,Flutter Web 的渲染在 Flutter 里是十分特殊的,前面我們說過它自帶了兩種渲染模式,而我們知道 Flutter 的設計理念里,所有的控件都是通過 Engine 繪制出來的,如果這時候您去 framework 里看 Canvas 的實現,就會發現它其實繼承的是?NativeFieldWrapperClass1:?
?
?
NativeFieldWrapperClass1 也就是它的邏輯是由不同平臺的 Engine 區分實現,其中編譯后的 Flutter Web 上的?Canvas 代碼應該是繼承如下所示的結構:?
?
?
可以看到在 Flutter Web 的 Canvas 里會根據邏輯判斷是使用 CanvasKitCanvas 還是 SurfaceCanvas,而相對于直接使用 skia 的 CanvasKitCanvas,更貼近 Web 平臺的 SurfaceCanvas 在實現的耦合復雜度上會更高。
?
首先如下圖所示是 Flutter Web 里 Canvas 的大致結構,而接下來我們要聊的主要也是集中在 SurfaceCanvas 上,為什么 SurfaceCanvas 層級會這么復雜,它們又是怎么分配繪制,接下來就讓我們深入揭秘它們的規則。
?
?
如果這時候我們放慢去看細節,如下動圖所示,可以看到當 item 處于不可見時
?
?
看到一個重點沒有?在這里的文本為什么是由? 標簽繪制而不是 標簽之類的呢?這就是我們重點要講的 SurfaceCanvas 渲染邏輯。
?在 Flutter Web 的?SurfaceCanvas 里,文本繪制一般都會是以這樣的情況出現,基本都是從 picture 開始進入繪制流程:??
?
那么在對應的 picture.dart 的代碼實現里可以看到,如下關鍵代碼所示,當 hasArbitraryPaint 為 true 時就會進入到 BitmapCanvas 的邏輯,不然就會使用 DomCanvas。
?那么這里有兩個問題:?BitmapCanvas 和?DomCanvas 的區別是什么?hasArbitraryPaint 的判斷邏輯是什么?void applyPaint(EngineCanvas? oldCanvas) {
if (picture.recordingCanvas!.renderStrategy.hasArbitraryPaint) {
_applyBitmapPaint(oldCanvas);
} else {
_applyDomPaint(oldCanvas);
}
}
-
首先 BitmapCanvas 和?DomCanvas 的最大的區別就是:
-
DomCanvas 會通過創建標簽來實現繪制,比如文本利用 p + span 標簽進行渲染;
-
BitmapCanvas 會考慮優先使用 canvas 渲染,如果場景需要再使用標簽來實現繪制。
?
-
在 web sdk 里 hasArbitraryPaint 參數默認是 false,但是在需要執行以下這些行為時就會被設置為 true,而這些調用上可以看出,其實大部分時候的繪制邏輯是會先進入到?BitmapCanvas 里。
?
?
回到前面的文本問題上,在 Flutter 的文本繪制一般都是通過?drawParagraph 實現,所以理論上只要有文本存在,就會進入到 BitmapCanvas 的繪制流程,那么目前看來這個結論符合上面 Item 里文本是使用 canvas 繪制的預期。
?
那 Flutter 里對于文本,在?BitmapCanvas 又是何時使用 canvas 何時使用 p+span 標簽呢?
?
我們先看如下代碼,運行后效果如下圖所示,可以看到此時的文本是直接使用 canvas 渲染的,這個結果符合我們目前的預期。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
接下來給這段代碼加上一個紅色背景,運行后可以看到,此時的文本變成了 p+span 標簽,并且紅色的背景是通過 draw-rect 標簽實現,層級里并沒有 canvas,這又是為什么呢?
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
),
child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
這里就需要先講到 BitmapCanvas 的 drawRect 實現,如下關鍵代碼所示,在 drawRect 時,如果在滿足 _useDomForRenderingFillAndStroke 這個函數條件的情況下,就會通過 buildDrawRectElement 的方式實現渲染,也就是使用 draw-rect 標簽而不是 canvas,所以我們需要先分析這個函數的判斷邏輯。
?@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
if (_useDomForRenderingFillAndStroke(paint)) {
final html.HtmlElement element = buildDrawRectElement(
rect, paint, 'draw-rect', _canvasPool.currentTransform);
_drawElement(
element,
ui.Offset(
math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
paint);
} else {
setUpPaint(paint, rect);
_canvasPool.drawRect(rect, paint.style);
tearDownPaint();
}
}
如下代碼所示,可以看到這個函數有很多的判斷條件,而得到 true 的條件就是滿足其中三大條件之一即可,下述表格里大致描述了每個條件所代表的意義。
?bool _useDomForRenderingFillAndStroke(SurfacePaintData paint) =>
_renderStrategy.isInsideSvgFilterTree ||
(_preserveImageData == false && _contains3dTransform) ||
((_childOverdraw ||
_renderStrategy.hasImageElements ||
_renderStrategy.hasParagraphs) &&
_canvasPool.isEmpty &&
paint.maskFilter == null &&
paint.shader == null);
isInsideSvgFilterTree |
例如有 ShaderMask 或者 ColorFilter 的時候為?true |
---|---|
_preserveImageData |
一般是在 toImage 的時候才會為?true |
_contains3dTransform | transformKind == TransformKind.complex 的時候,也就是矩陣包含縮放、旋轉、z 平移或透視變換 |
_childOverdraw | 有 _drawElement 或者 drawImage 的時候,大概就是使用了標簽渲染之后,需要切換畫布 |
_renderStrategy.hasImageElements | 有圖片繪制的時候,用 Image 標簽的情況 |
_renderStrategy.hasParagraphs | 有文本需要繪制的時候 |
_canvasPool.isEmpty | 簡單說就是 canvas == null 的時候 |
paint.maskFilter == null | 簡單說就是 Container 等控件沒有配置 shadow 的時候 |
paint.shader == null | 簡單說就是 Container 等控件沒有配置 gradient 的時候 |
大概流程也如圖所示,前面繪制紅色背景時并沒有添加什么特殊配置,所以會進入到 _drawElement 的邏輯,可以看到針對不同的渲染場景,BitmapCanvas 會采取不一樣的繪制邏輯,那為什么前面多了紅色背景就會導致文本也變成標簽呢?
?
?
這是因為在 BitmapCanvas 如果有使用標簽構建,也就是?_drawElement 的時候,就會執行一個 _closeCurrentCanvas 函數,該函數會把 _childOverdraw 設置為 true,并且清空 _canvasPool 里的 canvas。
?
所以我們看 drawParagraph 的實現,如下所示代碼,可以看到由于 _childOverdraw 是 true 時,文本會采用 Element 來繪制文本。
?而在?BitmapCanvas 里,有三個操作會觸發 _childOverdraw = true 和 _canvasPool Empty:?void drawParagraph(EngineParagraph paragraph, ui.Offset offset) {
····
if (paragraph.drawOnCanvas && _childOverdraw == false &&
!_renderStrategy.isInsideSvgFilterTree) {
paragraph.paint(this, offset);
return;
}
····
final html.Element paragraphElement =
drawParagraphElement(paragraph, offset);
····
}
-
_drawElement
-
drawImage/drawImageRect
-
drawParagraph
?
所以先總結一下,結合前面的流程圖,我們可以簡單認為:?在沒有 maskFilter (shadow) 和 shader (gradient) 的情況下,只要觸發了上述三種情況,就會使用標簽繪制。
?
是不是感覺有點亂?
?
不怕,先接著繼續看新的例子,在原本紅色背景實現的基礎上,這里給 Container 增加了 shadow 用于配置陰影,運行之后可以看到,不管是背景色或者文本又都變成了 canvas 渲染的情況。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
結合前面的流程看這是符合預期的,因為此時帶有 boxShadow 參數,該參數會在繪制時通過 toPaint 方法轉化為 maskFilter,所以在 maskFilter != null 的情況下,流程不會進入到 Element 的判斷,所以使用 canvas。
?
?
繼續前面的例子,如果這時候我們再加一個 ColorFiltered 控件,前面表格說過,有 ShaderMask 或者 ColorFilter 的時候,sInsideSvgFilterTree 參數就會是 true,這時候渲染就會直接進入使用 Element 繪制而無視其他條件如 BoxShadow,從運行結果上看也是如此。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: ColorFiltered(
colorFilter: ColorFilter.mode(Colors.yellow, BlendMode.hue),
child:Container(
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
),
)
可以看到此時變成了兩個 draw-rect 和 p 標簽的繪制,為什么會有這樣的邏輯,因為一些瀏覽器,例如 iOS 設備上的 Safari,它不會把 svg filter 等信息傳遞給 canvas,如果繼續使用 canvas 就會如 shader mask 等無法正常渲染,詳細可見:?#27600。
?
?
繼續這個例子,如果此時不加 ColorFiltered,而是給 Container 添加一個 transform,運行后可以看到還是 draw-rect 和 p?標簽的實現,因為此時的 transform 是屬于 TransformKind.complex 的狀態,會導致 _contains3dTransform = true,從而進入 Element 的邏輯。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
transform: Matrix4.identity()..setEntry(3, 2, 0.001) ..rotateX(100)..rotateY(100),
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
最后再來一個例子,這里回歸到只有紅色背景和陰影的情況,在之前它運行后是使用 canvas 標簽來渲染文本,因為它的 maskFilter != null,但是這時候我們給 Text 配置上 TextDecoratoin,運行之后可以看到背景顏色依然是 canvas,但是文本又變成了 p 標簽的實現。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
child: Text(
"v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
style: TextStyle(decoration: TextDecoration.lineThrough),
),
),
),
),
);
這是因為前面說過?drawParagraph,在這個函數里有另外一個判斷條件 _drawOnCanvas,在 Flutter Web 繪制文本時,當文本具備不為 none 的 TextDecoration 或者?fontFeatures 時,_drawOnCanvas 就會被設置為 fasle,從而變成使用 p 標簽渲染的情況。
?這也很好理解,例如?fontFeatures?是影響字形選擇的參數,如下圖所示,這些行為在 Web 上用 Canvas 繪制相對會麻煩很多。
?
?
前面講了那么多例子都是 BitmapCanvas,那 Domcanvas ?什么時候會用到呢?
?
還記得前面列舉的方法嗎,需要進入? _applyDomPaint 就需要 hasArbitraryPaint == false,換言之就是沒有文本,然后 drawRect 的時候沒有 shader (radient) 等就可以了。
?
依然是前面的例子,繪制一個帶有陰影的紅色方框,但是此時把文本內容去掉,運行后可以看到不是 canvas 而是 draw-rect 標簽,因為雖然此時 maskFilter != null (有 shadow),但是因為沒有文本或者 shader (gradient),所以單純普通的 drawRect 并不會觸發?hasArbitraryPaint == true,所以會直接使用 Domcanvas 繪制,完全脫離了 canvas 的渲染。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
height: 50,
decoration: BoxDecoration(
color: Colors.red,
boxShadow: [
BoxShadow(
color: Colors.black54,
blurRadius: 4.0,
offset: Offset(2, 2))
],
),
),
),
),
)
所以最后總結一下: 首先除了下圖所示之外的情況,大部分時候 Flutter Web 繪制都會進入到 BitmapCanvas。
?
?
結合前面介紹的例子,進入到 BitmapCanvas 之后的流程可以總結:?-
存在 ShaderMask 或者 ColorFilter 就會使用 Element;
-
一般情況忽略?_preserveImageData,有復雜矩陣變換時也是直接使用 Element,因為復雜矩陣變換 canvas 支持并不好;
-
_childOverdraw 經常和 _canvasPool.isEmpty 一起達成條件,一般有 picture 上有 _drawElement 之后就會調用 _closeCurrentCanvas 設置? _childOverdraw = true 并且清空 _canvasPool;
-
結合上述第三個條件的狀態,如果沒有 maskFilter 或者 shader,就會使用 Element 渲染 UI。
?
最后針對文本,在 drawParagraph 時還有特殊處理,關于 _childOverdraw 和 !isInsideSvgFilterTree 相關前面解釋過了,新增條件是在有 TextDecoration 或者 FontFeatures 時,也會觸發文本繪制變為 Element,也就是 p + span 標簽的形式。
?
?
?
四、最后
?
雖然本次介紹的東西不少,但是 Flutter Web 在 html 渲染模式下的知識點遠不止這些,而由小窺大,以 drawRect 和文本為切入點去了解 SurfaceCanvas 就是很不錯的開始。
?
另外可以看到,在 Flutter Web 里有很多的自定義的
?
?
? ? 審核編輯 :李倩?
評論