可能提及 Flutter 繪製,你們第一反應就是用 CustomPaint
組件,自定義 CustomPainter
對象來畫。Flutter 中全部能夠看獲得的組件,好比 Text、Image、Switch、Slider 等等,追其根源都是畫出來
的,但經過查看源碼能夠發現,Flutter 中絕大多數組件並非使用 CustomPaint
組件來畫的,其實 CustomPaint
組件是對框架底層繪製的一層封裝。這個系列即是對 Flutter 繪製的探索,經過測試
、調試
及源碼分析
來給出一些在繪製時被忽略
或從未知曉
的東西,而有些要點若是被忽略,就極可能出現問題。web
本文是第一篇,就先從 CustomPaint
開始提及。你在 Flutter 繪製中,還在使用 State#setState
來刷新畫板嗎?你會不會也有和下面這位哥們相同的疑惑?你是否是隻能將繪製抽離一個新組建來局部刷新?經過對源碼的分析和研究後,會發現對於 CustomPainter 的重繪,有一個更高效的刷新方式。本文就來分享一下這個很是重要的知識點。編程
本文的測試案例效果以下,使用 CustomPaint
組件繪製一個圓,讓其執行 3 秒紅轉藍
的動畫。canvas
ShapePainter
以下自定義一個 CustomPainter
,構造函數中傳入顏色 color。須要複寫兩個方法 paint
和 shouldRepaint
。在 paint
方法中會回調 Canvas
和 Size
對象,以供繪製使用。以下代碼,繪製一個顏色爲 color
的圓。數組
class ShapePainter extends CustomPainter {
final Color color;
ShapePainter({this.color});
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = color;
canvas.drawCircle(
Offset(size.width / 2, size.height / 2), size.width / 2, paint);
}
@override
bool shouldRepaint(covariant ShapePainter oldDelegate) {
return oldDelegate.color!=color;
}
}
複製代碼
自定義的畫板想要展現出來,須要使用 CustomPaint
組件,爲其設置 painter
屬性。以下代碼,在實例化 ShapePainter
時傳入紅色。微信
class HomePage extends StatefulWidget {
@override
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(20.0),
child: CustomPaint( //<--- 使用繪製組件
size: Size(100, 100),
painter: ShapePainter(color: Colors.red), //<--- 設置畫板
),
),
);
}
}
複製代碼
將主程序運行後,就能夠看到繪製的效果。markdown
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage());
}
}
複製代碼
setState
(不推薦)經過 ValueListenableBuilder 篇,咱們應該知道在較上級的 State
類中執行 setState
會致使更多的 Build
過程。以下代碼中經過監聽 AnimationController
,並 setState
對當前 build
方法下的節點進行更新,從而實現顏色的變化。app
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
AnimationController _ctrl;
@override
void initState() {
super.initState();
_ctrl = AnimationController(vsync: this, duration: Duration(seconds: 3))
..addListener(_update);
_ctrl.forward();
}
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
void _update() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: EdgeInsets.all(20),
child: CustomPaint(
size: Size(100, 100),
painter: ShapePainter(
color: Color.lerp(Colors.red, Colors.blue, _ctrl.value)),
),
),
);
}
}
複製代碼
局部刷新
(不推薦)那也許你會說,只要下降刷新的節點,將 畫板組件
單獨抽離
出去,或使用 ValueListenableBuilder 局部刷新不就行了嗎?若是看了 ValueListenableBuilder 的源碼就會發現,其實它的本質就是 組件抽離
,只不過對其進行封裝,回調出 builder
簡化用戶使用。以下是使用 ValueListenableBuilder 局部構建的組件,這樣能夠不使用 setState
實現組件的重建,我仍是想要着重強調一句:並非說 setState
很差,而是看它重建的範圍,ValueListenableBuilder 源碼中也是基於 State#setState
進行重構的,並非一個東西非好即壞
,還須要看使用的場景和時機。框架
---->[_HomePageState#build]----
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: ValueListenableBuilder(
valueListenable: _ctrl,
builder:(ctx,value,child) => CustomPaint(
size: Size(100, 100),
painter: ShapePainter(color: Color.lerp(Colors.red, Colors.blue, value)),
),
),
);
}
複製代碼
也許你會以爲,如今不是很好嗎,如今重建只是對於 CustomPaint
而言了,已經控制了重建的粒度。但重要的一點是 CustomPaint
被重建了,ShapePainter
也會隨之重建,以下的調試,是動畫過程當中兩次 paint 時狀況。經過下面的 this
能夠看出,當前對象的內存地址是不同,說明每次更新畫板都是不一樣的。這對於動畫來講是災難性的,每 16 ms 都會構建一次畫板,這樣的頻率,即便是局部刷新,也不是最佳選擇。那有沒有一種方式,能夠悄無聲息
的地進行繪製,而不會觸發任何組件的重構?答案是 有的!
。less
第一次 | 第二次 |
---|---|
在剛纔 ValueListenableBuilder
版的基礎上稍做修改,咱們就能夠完成這個需求。首先,剔除掉 ValueListenableBuilder
,而後將 Animation<double>
做爲 ShapePainter
的成員 factor,在構造函數中傳入。並使用 super(repaint: factor)
爲成員 repaint
賦值。repaint
是 CustomPainter 的成員,類型爲 Listenable
可監聽對象,當 repaint
值變化時,會通知畫板進行 paint 重繪。ide
---->[_HomePageState#build]----
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: CustomPaint(
size: Size(100, 100),
painter: ShapePainter(factor: _ctrl),
),
);
}
class ShapePainter extends CustomPainter {
final Animation<double> factor;
ShapePainter({this.factor}) : super(repaint: factor);
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Color.lerp(Colors.red, Colors.blue, factor.value);
canvas.drawCircle(
Offset(size.width / 2, size.height / 2), size.width / 2, paint);
}
@override
bool shouldRepaint(covariant ShapePainter oldDelegate) {
return oldDelegate.factor != factor;
}
}
複製代碼
經過這種方式,點擊時在 paint
方法斷點調試,結果以下。能夠看出,在完成顏色變化的同時,沒有任何組件的重建,ShapePainter
對象也沒有變化,是否是感受很是神奇。
第一次 | 第二次 |
---|---|
也許有人會問,這些你是怎麼知道的?當一個疑問一直縈繞心頭時,我就會想辦法去研究它,而研究它最好的途徑就是不斷測試
和分析源碼
。目標能夠是 CustomPainter
的源碼自己,也能夠是源碼中使用到CustomPainter
的地方。 其實不少知識,一直都寫在源碼中,只是不多人看到。經過 CustomPainter
的註釋能夠發現,觸發重繪最高效的方式都是基於可監聽對象
實現的。
觸發重繪的最高效方式是:
[1]:繼承 [CustomPainter] 類,並在構造函數提供一個 'repaint' 參數,
當須要從新繪製時,該對象會進行通知它的監聽者。
[2]:繼承 [Listenable] (好比經過 [ChangeNotifier])並實現 [CustomPainter],
這樣對象自己就能夠直接提供通知。
複製代碼
CustomPainter
在 Flutter 框架中的應用其實 CustomPainter 在 Flutter 框架源碼中的應用並非很是多,一共也就下面的 20 處。這些都是源碼中對 CustomPainter
的使用,就表示這些使用的方式相對而言是 最正規
的。
_CupertinoActivityIndicatorPainter
第一次的 悟道
,是在 _CupertinoActivityIndicatorPainter
源碼中,也就是那個 iOS
的菊花轉的繪製畫板。 position 是一個 Animation<double>
類型的對象,Animation
也是一個 Listenable
。當時發現 CupertinoActivityIndicator
中沒有使用 setState
卻能夠觸發界面的刷新,我是很是驚喜的,通過分析和研究它的實現方式,我終於發現了 CustomPainter
中 repaint
祕密。
上面說的第二種是經過繼承自 Listenable
並實現 CustomPainter
的方式,如源碼中的 ScrollbarPainter
。它是用來繪製 ScrollBar
組件的,經過這種方式可讓 ScrollbarPainter
即處理繪製,又處理通知。
這樣,在 _CupertinoScrollbarState
中就能夠將 ScrollbarPainter
做爲成員變量,和 State 擁有一樣的生命長度。並在某些恰當的時刻,使用該對象觸發相應方法進行畫布重繪。
_GlowingOverscrollIndicatorPainter
當時還有一個疑惑是,repaint
中只是傳入一個 Listenable
對象,那麼多個屬性如何去監聽呢,好比多個動畫同時執行。因而看到 _GlowingOverscrollIndicatorPainter
時便豁然開朗。它畫的是滑動到頂底光暈的那個東西。 其中傳入的 leadingController
、trailingController
兩個可監聽對象。除此以外,額外傳入 repaint
。
能夠經過 Listenable.merge
將多個可監聽對象融合。
_PlaceholderPainter
但當我以爲 repaint
無敵之時,仍會發現,源碼中有不少繪製的類並沒使用 repaint
,而是向外界暴露屬性進行設置。好比 _PlaceholderPainter
的矩形×,_GridPaperPainter
的網格,因而陷入沉思。
_GridPaperPainter 的源碼,只是向外界暴露繪製相關屬性。
最終發現了一個共性:當繪製中含有動畫和滑動處理時,都會使用 repaint 設置監聽對象來觸發刷新
,對於僅是靜態的繪製,則使用時將繪製屬性暴露出去,交由外界處理,須要刷新的話,只能經過重建畫板對象。其實這也很容易理解: 動畫
和 滑動
的觸發頻率很是高,因此纔會用特殊的方式進行重繪。
那麼畫板的重繪必須只是經過 可監聽對象
嗎?並不是如此,雖然能夠經過可監聽對象來觸發畫布刷新,好比_PlaceholderPainter
中 color 成員變爲 ValueNotifier<Color>
,但這樣就會增長用戶使用的複雜性。對於非頻繁刷新的場景,局部刷新也就夠了,這應該就是源碼中,在非 動畫和滑動
中不使用 repaint
的緣由。但對於頻繁觸發的繪製,如 動畫
和 滑動
必定要用。
最後想說一句:任何東西都不會天衣無縫。成年人的世界,沒有對錯,只有適合與不適合。在一切的困惑、質疑、反駁
以前,你應作的是 多測、多想、多看
。本文就到這裏,應該算是說清楚了 CustomPainter
正確的刷新姿式,但這也僅是 繪製探索
的冰山一角,CustomPainter
與 CustomPaint
背後還有不少值得探尋的東西,會隨着以後的探索,爲你展開一個更加豐滿的 Flutter 世界。
@張風捷特烈 2021.01.11 未允禁轉
個人公衆號:編程之王
聯繫我--郵箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~