?
?Flutter Web 穩(wěn)定版本發(fā)布至今也有一年多了,經(jīng)過這一年多的發(fā)展,今天就讓我們來看看 Flutter Web 究竟有什么不同之處,本篇分享主要內(nèi)容是目前 Flutter 下少有較為全面的 Web 內(nèi)容。 ? ?一、起源與實現(xiàn)
?說起 Flutter 的起源就很有意思,大家都知道早期 Flutter 最先支持的平臺是 Android 和 iOS,至今最核心的維護平臺依然是 Android 和 iOS,但是事實上 Flutter 其實起源于前端團隊。 ??另外前端的同學應(yīng)該知道,Dart 起初也是為了 Web 而生,事實上?Dart 誕生至今也有 10 年了,所以可以說 Flutter 其實充滿了 Web 的基因。 ?但是作為從 Web 里誕生的框架,和 React Native/ Weex 不同的是,前者是先有了 Web 下的 React 和 Vue 實現(xiàn)之后才有的客戶端支持,而對于 Flutter 則是反過來,先有客戶端實現(xiàn)之后才支持 Web 平臺,這里其實可以和 Weex 做個簡單對照。 ?Weex 作為曾經(jīng)閃耀過的跨平臺框架,它同樣支持 Android、iOS 和 Web 三個平臺,在 Android 和 iOS 上 Weex 和 React Native 差異性不大,在 Web 上 Weex 則是刪減版的 Vue 支持,而由于 API 和平臺差異性的問題,Weex 在 Web 上的支持體驗一直不是很好:?Flutter 來源于前端 Chrome 團隊,起初 Flutter 的創(chuàng)始人和整個團隊幾乎都是來自 Web,在 Flutter 負責人 Eric 的相關(guān)訪談中,Eric 表示 Flutter 來自 Chrome 內(nèi)部的一個實驗,他們把一些亂七八糟的 Web 規(guī)范去掉后,在一些內(nèi)部基準測試的性能居然能提升 20?倍,因此 Google 內(nèi)部就開始立項,所以 Flutter 出現(xiàn)了。
?
?因為 Weex 需要依賴平臺控件實現(xiàn)渲染,導(dǎo)致一個 Text 控件需要兼顧 Android、iOS 和 Web 上原生平臺接口的邏輯,從而出現(xiàn)各種由于耦合帶來的兼容性問題。
而 Flutter 實現(xiàn)更為特別,通過 Skia 實現(xiàn)了獨立的渲染引擎之后,在 Android 和 iOS 上控件幾乎就與平臺無關(guān),所以 Flutter 上的控件可以做到獨立且不同平臺上渲染一致的效果。
但是回到 Web 上又有些特殊,首先 Web 平臺完全是 html / js / css 的天下,并且 Web 平臺需要同時兼顧 PC 和 Mobile 的不同環(huán)境,這就讓 Flutter Web 成了 Flutter 所有平臺里 "最另類又奇葩" 的落地。
?
首先 Flutter Web 和其他 Flutter 平臺一樣共用一套 Framework,理論上絕大多數(shù)的控件實現(xiàn)都是通用的,當然如果要說最不兼容的 API 對象,那肯定就是 Canvas 了,這其實和 Flutter Web 特殊的實現(xiàn)有關(guān)系,后面我們會聊到這個問題。
?
而由于 Web 的特殊場景,Flutter Web 在 "幾經(jīng)周折" 之后落地了兩種不同的渲染邏輯:?html 和 canvaskit,它們的不同之處在于:?
- html
-
好處:?html 的實現(xiàn)更輕量級,渲染實現(xiàn)基本依賴于 Web 平臺的各種 HTMLElement,特別是 Flutter Web 下定義的各種
實現(xiàn),可以說它更貼近現(xiàn)在的 Web 環(huán)境,所以有時候我們也稱呼它為 DomCanvas,當然隨著 Flutter Web 的發(fā)展這個稱呼也發(fā)生了一些變化,后續(xù)我們會詳細講到這個。 - 問題: html 的問題也在于太過于貼近 Web 平臺,這就和 Weex 一樣,貼近平臺也就是耦合于平臺,事實上 DomCanvas 實現(xiàn)理念其實和 Flutter 并不貼切,也導(dǎo)致了 Flutter Web 的一些渲染效果在 html 模式下存在兼容問題,特別是?Canvas 的 API。 ?
- canvaskit
- 好處: canvaskit 的實現(xiàn)可以說是更貼近 Flutter 理念,因為它其實就是 Skia + WebAssembly 的實現(xiàn)邏輯,能和其他平臺的實現(xiàn)更一致,性能更好,比如滾動列表的渲染流暢度更高等。
- 問題: 很明顯使用 WebAssembly 帶來的 wasm 文件會導(dǎo)致體積增大不少,Web 場景下其實很講究加載速度,而在這方面 wasm 能優(yōu)化的空間很小,并且 WebAssembly 在兼容上也是相對較差,另外 skia 還需要自帶字體庫等問題都挺讓人頭痛。 ?
默認情況下 Flutter Web 在打包渲染時會把 html 和 canvaskit 都打包進去,然后在 PC 端使用 canvaskit 模式,在 mobile 端使用 html 模式,當然您也可以在打包時通過 flutter build web --web-renderer html --release 之類的配置強行指定渲染模式。
?
既然這里我們講到了 Flutter Web 的打包構(gòu)建,那就讓我們先從構(gòu)建打包角度開始來深入介紹 Flutter Web。 ? ?二、構(gòu)建和優(yōu)化
?Flutter Web 雖說是和其他平臺共用一個 framework,但是它在 dart 層開始就有一套自己特殊的 engine 實現(xiàn),并且這套實現(xiàn)是獨立于 framework 的一套特殊代碼。
?
所以在 Flutter Web 打包時,會把默認的? /flutter/bin/cache/lib/_engine 變成了 flutter/bin/cache/flutter_web_sdk/lib/_engine 的相關(guān)實現(xiàn),這是因為 Flutter Web 在 framework 之下的 engine 需要一套特殊的 API。
?
下圖右側(cè)構(gòu)建是指定 web 的打包路徑,和左邊默認時的對比。
?
同樣下圖所示,可以看到 web sdk 里會有如 html、canvaskit 這樣不同的實現(xiàn),甚至會有一個特殊的 text 目錄,這是因為在 web 上對于文本的支持是個十分復(fù)雜的問題。
那到這里我們知道了在?_engine 層面,F(xiàn)lutter Web 有著自己一套獨立的實現(xiàn),那構(gòu)建之后的產(chǎn)物是什么樣的情況呢?
?
如下圖所示是 GSY 的一個簡單的開源示例項目,在部署到服務(wù)器后可以看到,默認情況下在不做任何處理時,在 PC 端打開后會使用 canvaskit 渲染,主要會有:? ?
可以看到這些文件占據(jù)了 Flutter Web 編譯后產(chǎn)物的大部分體積,并且從大小上看確實讓人有些無法接受,因為示例項目的代碼量并不大,結(jié)構(gòu)也不復(fù)雜,這樣的體積肯定十分影響加載速度。
?
所以我們首先考慮在 html 和 canvaskit 兩種渲染模式中先選定一種,出于實用性考慮,結(jié)合前面的對比情況,選用 html 渲染模式在兼容性和可優(yōu)化上會更友好,所以這里優(yōu)化的第一步就是先指定 html 模式作為渲染引擎。
?
開始優(yōu)化首先可以看到 CupertinoIcons.ttf 這個矢量圖標文件,雖然默認創(chuàng)建項目時會通過 cupertino_icons 被添加到項目里,但是由于我們不需要使用,所以可以在 yaml 文件里去除。
?
之后通過運行 flutter build web --release --web-renderer html 后,可以看到使用 html 模式加載后的產(chǎn)物很干凈,而需要優(yōu)化的體積現(xiàn)在主要在 main.dart.js 和 MaterialIcons-Regular.otf 上。
?
?
雖然在項目中我們會使用到 MaterialIcons 的一些矢量圖標,但是每次加載都要全量加載一個 1.5 MB 的字體庫文件顯然并不符合邏輯,所以在 Flutter 里官方提供了 --tree-shake-icons 的命令幫助我們優(yōu)化這部分的內(nèi)容。
?
但是不幸的是,如下圖所示,在當前的 2.10 版本下該配置運行會有 bug,而不幸中的萬幸是,在原生平臺的編譯中 shake-icons 行為是可以正常執(zhí)行。
?
?
所以我們可以先運行 flutter build apk,然后通過如下命令,將 Android 上已經(jīng) shake-icons 的 MaterialIcons-Regular.otf 資源復(fù)制到已經(jīng)編譯好的 web/ 目錄下。
- ?
cp?-r?./build/app/intermediates/flutter/release/flutter_assets/?./build/web/assets
?再次打包后可以看到,經(jīng)過優(yōu)化后 MaterialIcons-Regular.otf 資源如今只剩下 3.2 kB,那接下來就是考慮針對 2.2 MB 的 main.dart.js 進行優(yōu)化處理。
?
?
要優(yōu)化 main.dart.js,我們就要講到 Flutter 里的?deferred-components,在 Flutter 里可以通過把控件定義為 "deferred component"?來實現(xiàn)控件的懶加載,而這個行為在 Flutter Web 上被編譯之后就會變成多個 *part.js 文本,原理上就是對 main.dart.js 進行拆包。
?
舉個例子,首先我們定義一個普通的 Flutter 控件,按照正常的控件進行實現(xiàn)就可以。
?import 'package:flutter/widgets.dart';
class DeferredBox extends StatelessWidget {
DeferredBox() {}
Widget build(BuildContext context) {
return Container(
height: 30,
width: 30,
color: Colors.blue,
);
}
}
在需要的地方 import 對應(yīng)控件然后添加 deferred as box 關(guān)鍵字,之后在適當時機通過 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 來制定對應(yīng)的 libraries 路徑。
?deferred-components:
- name: crane
libraries:
- package:gsy_flutter_demo/widget/box.dart
回歸到上面的 GSY 示例項目中,通過相對極端的分包實現(xiàn),這里把 GSY 示例里的每個頁面都變成一個獨立的懶加載頁面,然后在頁面跳轉(zhuǎn)時再加載顯示,最終打包部署后如下圖所示:?
?
?
可以看到拆分之后 main.dart.js 從 2.2 MB 變成了 1.6 MB,而其他內(nèi)容通過 deferred components 變成了各個 part.js 的獨立文件,并且只在點擊時才動態(tài)下載對應(yīng)的 part.js 文件,但是此時的 main.dart.js 依舊不小,而官方提供的能力上已經(jīng)沒有太多優(yōu)化的余地。
?
在這里可以通過前端的 source-map-explorer 工具去分析這個文件,首先在編譯時要添加 --source-maps 命令,這樣在打包時會生成 main.dart.js 的 source map 文件,然后就執(zhí)行 source-map-explorer main.dart.js --no-border-checks ?生成對應(yīng)的分析圖:?
?
?
這里只展示能夠被 mapped 的部分,可以看到 700k 幾乎就是 Flutter Web 整個 framewok + engine + vm 的大小,而這部分內(nèi)容其實可以優(yōu)化的空間并不大,盡管會有一些如 kIsWeb 的冗余代碼,但是其實可以調(diào)整的內(nèi)容并不多,大概有 36 處可以調(diào)整和刪減的地方,實質(zhì)上打包時 Flutter Web 也都有相應(yīng)的優(yōu)化壓縮處理,所以這部分收益并不高。
?
?
另外,如下圖所示是兩種不同 web rendder 構(gòu)建后代碼上的差異,可以看到 html 和 canvaskit 單獨構(gòu)建后的 engine 代碼結(jié)構(gòu)差異性還是很大的。
?
而如果您在編譯時默認的 auto 模式,就會看到 html 和 canvaskit 的代碼都會打包進去,所以相對的 main.dart.js 也會增加一些。
?
?
那還有什么可以優(yōu)化的地方嗎?還是有的,通過外部手段,例如通過在部署時開啟 gzip 或者 brotli 壓縮,如下圖所示,開始 gzip 后大概可以讓 main.dart.js 下降到 400k 左右。
?
?
另外也有在 index.html 里增加 loading 效果來做等待加載過程的展示,例如:?
?所以大致上以上這些就是今天關(guān)于 Flutter Web 上產(chǎn)物體積的優(yōu)化,總結(jié)起來就是:?<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 優(yōu)化引用矢量圖庫;
-
通過 deferred-components 實現(xiàn)懶加載分包;
-
開啟 gzip 等壓縮算法壓縮?main.dart.js。
?
?
三、渲染
?
講完構(gòu)建,最后我們聊聊渲染,F(xiàn)lutter Web 的渲染在 Flutter 里是十分特殊的,前面我們說過它自帶了兩種渲染模式,而我們知道 Flutter 的設(shè)計理念里,所有的控件都是通過 Engine 繪制出來的,如果這時候您去 framework 里看 Canvas 的實現(xiàn),就會發(fā)現(xiàn)它其實繼承的是?NativeFieldWrapperClass1:?
?
?
NativeFieldWrapperClass1 也就是它的邏輯是由不同平臺的 Engine 區(qū)分實現(xiàn),其中編譯后的 Flutter Web 上的?Canvas 代碼應(yīng)該是繼承如下所示的結(jié)構(gòu):?
?
?
可以看到在 Flutter Web 的 Canvas 里會根據(jù)邏輯判斷是使用 CanvasKitCanvas 還是 SurfaceCanvas,而相對于直接使用 skia 的 CanvasKitCanvas,更貼近 Web 平臺的 SurfaceCanvas 在實現(xiàn)的耦合復(fù)雜度上會更高。
?
首先如下圖所示是 Flutter Web 里 Canvas 的大致結(jié)構(gòu),而接下來我們要聊的主要也是集中在 SurfaceCanvas 上,為什么 SurfaceCanvas 層級會這么復(fù)雜,它們又是怎么分配繪制,接下來就讓我們深入揭秘它們的規(guī)則。
?
?
先看例子,如下圖所示,可以看到在 html 渲染模式下,F(xiàn)lutter Web 是有一大堆自定義的
?
?
如果這時候我們放慢去看細節(jié),如下動圖所示,可以看到當 item 處于不可見時
?
?
看到一個重點沒有?在這里的文本為什么是由? 標簽繪制而不是 標簽之類的呢?這就是我們重點要講的 SurfaceCanvas 渲染邏輯。
?在 Flutter Web 的?SurfaceCanvas 里,文本繪制一般都會是以這樣的情況出現(xiàn),基本都是從 picture 開始進入繪制流程:??
?
那么在對應(yīng)的 picture.dart 的代碼實現(xiàn)里可以看到,如下關(guān)鍵代碼所示,當 hasArbitraryPaint 為 true 時就會進入到 BitmapCanvas 的邏輯,不然就會使用 DomCanvas。
?那么這里有兩個問題:?BitmapCanvas 和?DomCanvas 的區(qū)別是什么?hasArbitraryPaint 的判斷邏輯是什么?void applyPaint(EngineCanvas? oldCanvas) {
if (picture.recordingCanvas!.renderStrategy.hasArbitraryPaint) {
_applyBitmapPaint(oldCanvas);
} else {
_applyDomPaint(oldCanvas);
}
}
-
首先 BitmapCanvas 和?DomCanvas 的最大的區(qū)別就是:
-
DomCanvas 會通過創(chuàng)建標簽來實現(xiàn)繪制,比如文本利用 p + span 標簽進行渲染;
-
BitmapCanvas 會考慮優(yōu)先使用 canvas 渲染,如果場景需要再使用標簽來實現(xiàn)繪制。
?
-
在 web sdk 里 hasArbitraryPaint 參數(shù)默認是 false,但是在需要執(zhí)行以下這些行為時就會被設(shè)置為 true,而這些調(diào)用上可以看出,其實大部分時候的繪制邏輯是會先進入到?BitmapCanvas 里。
?
?
回到前面的文本問題上,在 Flutter 的文本繪制一般都是通過?drawParagraph 實現(xiàn),所以理論上只要有文本存在,就會進入到 BitmapCanvas 的繪制流程,那么目前看來這個結(jié)論符合上面 Item 里文本是使用 canvas 繪制的預(yù)期。
?
那 Flutter 里對于文本,在?BitmapCanvas 又是何時使用 canvas 何時使用 p+span 標簽?zāi)?/span>?
?
我們先看如下代碼,運行后效果如下圖所示,可以看到此時的文本是直接使用 canvas 渲染的,這個結(jié)果符合我們目前的預(yù)期。
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
接下來給這段代碼加上一個紅色背景,運行后可以看到,此時的文本變成了 p+span 標簽,并且紅色的背景是通過 draw-rect 標簽實現(xiàn),層級里并沒有 canvas,這又是為什么呢?
? ?Scaffold(
body: Container(
alignment: Alignment.center,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.red,
),
child: Text( "v333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333",
),
),
),
),
)
這里就需要先講到 BitmapCanvas 的 drawRect 實現(xiàn),如下關(guān)鍵代碼所示,在 drawRect 時,如果在滿足 _useDomForRenderingFillAndStroke 這個函數(shù)條件的情況下,就會通過 buildDrawRectElement 的方式實現(xiàn)渲染,也就是使用 draw-rect 標簽而不是 canvas,所以我們需要先分析這個函數(shù)的判斷邏輯。
?@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();
}
}
如下代碼所示,可以看到這個函數(shù)有很多的判斷條件,而得到 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 的時候,也就是矩陣包含縮放、旋轉(zhuǎn)、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 會采取不一樣的繪制邏輯,那為什么前面多了紅色背景就會導(dǎo)致文本也變成標簽?zāi)兀?/span>
?
?
這是因為在 BitmapCanvas 如果有使用標簽構(gòu)建,也就是?_drawElement 的時候,就會執(zhí)行一個 _closeCurrentCanvas 函數(shù),該函數(shù)會把 _childOverdraw 設(shè)置為 true,并且清空 _canvasPool 里的 canvas。
?
所以我們看 drawParagraph 的實現(xiàn),如下所示代碼,可以看到由于 _childOverdraw 是 true 時,文本會采用 Element 來繪制文本。
?而在?BitmapCanvas 里,有三個操作會觸發(fā) _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
?
所以先總結(jié)一下,結(jié)合前面的流程圖,我們可以簡單認為:?在沒有 maskFilter (shadow) 和 shader (gradient) 的情況下,只要觸發(fā)了上述三種情況,就會使用標簽繪制。
?
是不是感覺有點亂?
?
不怕,先接著繼續(xù)看新的例子,在原本紅色背景實現(xiàn)的基礎(chǔ)上,這里給 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",
),
),
),
),
)
結(jié)合前面的流程看這是符合預(yù)期的,因為此時帶有 boxShadow 參數(shù),該參數(shù)會在繪制時通過 toPaint 方法轉(zhuǎn)化為 maskFilter,所以在 maskFilter != null 的情況下,流程不會進入到 Element 的判斷,所以使用 canvas。
?
?
繼續(xù)前面的例子,如果這時候我們再加一個 ColorFiltered 控件,前面表格說過,有 ShaderMask 或者 ColorFilter 的時候,sInsideSvgFilterTree 參數(shù)就會是 true,這時候渲染就會直接進入使用 Element 繪制而無視其他條件如 BoxShadow,從運行結(jié)果上看也是如此。
? ?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 設(shè)備上的 Safari,它不會把 svg filter 等信息傳遞給 canvas,如果繼續(xù)使用 canvas 就會如 shader mask 等無法正常渲染,詳細可見:?#27600。
?
?
繼續(xù)這個例子,如果此時不加 ColorFiltered,而是給 Container 添加一個 transform,運行后可以看到還是 draw-rect 和 p?標簽的實現(xiàn),因為此時的 transform 是屬于 TransformKind.complex 的狀態(tài),會導(dǎo)致 _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 標簽的實現(xiàn)。
? ?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,在這個函數(shù)里有另外一個判斷條件 _drawOnCanvas,在 Flutter Web 繪制文本時,當文本具備不為 none 的 TextDecoration 或者?fontFeatures 時,_drawOnCanvas 就會被設(shè)置為 fasle,從而變成使用 p 標簽渲染的情況。
?這也很好理解,例如?fontFeatures?是影響字形選擇的參數(shù),如下圖所示,這些行為在 Web 上用 Canvas 繪制相對會麻煩很多。
?
?
前面講了那么多例子都是 BitmapCanvas,那 Domcanvas ?什么時候會用到呢?
?
還記得前面列舉的方法嗎,需要進入? _applyDomPaint 就需要 hasArbitraryPaint == false,換言之就是沒有文本,然后 drawRect 的時候沒有 shader (radient) 等就可以了。
?
依然是前面的例子,繪制一個帶有陰影的紅色方框,但是此時把文本內(nèi)容去掉,運行后可以看到不是 canvas 而是 draw-rect 標簽,因為雖然此時 maskFilter != null (有 shadow),但是因為沒有文本或者 shader (gradient),所以單純普通的 drawRect 并不會觸發(fā)?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))
],
),
),
),
),
)
所以最后總結(jié)一下: 首先除了下圖所示之外的情況,大部分時候 Flutter Web 繪制都會進入到 BitmapCanvas。
?
?
結(jié)合前面介紹的例子,進入到 BitmapCanvas 之后的流程可以總結(jié):?-
存在 ShaderMask 或者 ColorFilter 就會使用 Element;
-
一般情況忽略?_preserveImageData,有復(fù)雜矩陣變換時也是直接使用 Element,因為復(fù)雜矩陣變換 canvas 支持并不好;
-
_childOverdraw 經(jīng)常和 _canvasPool.isEmpty 一起達成條件,一般有 picture 上有 _drawElement 之后就會調(diào)用 _closeCurrentCanvas 設(shè)置? _childOverdraw = true 并且清空 _canvasPool;
-
結(jié)合上述第三個條件的狀態(tài),如果沒有 maskFilter 或者 shader,就會使用 Element 渲染 UI。
?
最后針對文本,在 drawParagraph 時還有特殊處理,關(guān)于 _childOverdraw 和 !isInsideSvgFilterTree 相關(guān)前面解釋過了,新增條件是在有 TextDecoration 或者 FontFeatures 時,也會觸發(fā)文本繪制變?yōu)?Element,也就是 p + span 標簽的形式。
?
?
?
四、最后
?
雖然本次介紹的東西不少,但是 Flutter Web 在 html 渲染模式下的知識點遠不止這些,而由小窺大,以 drawRect 和文本為切入點去了解 SurfaceCanvas 就是很不錯的開始。
?
另外可以看到,在 Flutter Web 里有很多的自定義的
?
?
? ? 審核編輯 :李倩?
評論
查看更多