Flutter封装-让Canvas绘制变得更灵活
在封装之前我们先看看基础的Canvas绘制,再试着封装。
如果你知道如何使用CustomPaint
直接查看下面的封装过程
CustomPaint绘制
class _MyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: CustomPaint(
painter: _MyPainter(),
),
);
}
}
绘制需要两个元素:
CustomPaint
负责提供画布CustomPainter
负责绘制
搞过Android的都知道,Android的绘制实在一个对象View中进行的,可以将任何参数动画在一个class中创建和释放。
但是Flutter却需要两个对象,并且每次setState的之后,都会生成新的Widget,这就导致如果你在CustomPainter
中创建了一些变量参数,在下一次setState的时候可能就不能保存了。所以CustomPainter
中不能创建与状态相关的数据。但是你可以通过外部传入。
例如:
class _MyPainter extends CustomPainter {
int counter;
_MyPainter(this.counter);
//...
}
有个极端的场景,如果你想要绘制动画呢?
动画不就是数据的不断变更然后根据变更的数据进行绘制形成的动画吗。比较直接的就是,在Widget中不断的setState,然后把变更的数据传递给Painter。这样确实没问题,但是,反复的setState也会导致整个Widget下的所有组件反复的构建,如果有哪个组件没处理好,直接反复构建几百下。
另一种方式是使用CustomPainter
的repaint
参数,让Painter观察数据的变化自己进行重新绘制:
ValueNotifier<int> valueNotifier = ValueNotifier(0);
void _incrementCounter() {
// setState(() {
// _counter++;
// });
valueNotifier.value+=1;
}
CustomPaint(
painter: _MyPainter(valueNotifier),
)
class _MyPainter extends CustomPainter {
ValueNotifier<int> counter;
_MyPainter(this.counter):super(repaint: counter);
@override
void paint(Canvas canvas, Size size) {
var value = counter.value;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
这样刷新绘制内容的时候,就不需要依赖shouldRepaint
以及setState
了。
如果是做动画也是没问题的。
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(vsync: this,duration: const Duration(seconds: 1));
_animation = CurveTween(curve: Curves.bounceIn).animate(_controller);
}
class _MyPainter extends CustomPainter {
ValueNotifier<int> counter;
Animation<double> animation;
_MyPainter(this.counter,this.animation):super(repaint: Listenable.merge([counter,animation]));
@override
void paint(Canvas canvas, Size size) {
var value = counter.value;
var value2 = animation.value;
}
}
看到这里是不是会发现什么问题。如果绘制的内容中,动画元素过多,如果动画元素也存在个执行先后,这样是不是要传入一大堆的Animation
或Listenable
。并且处理动画逻辑也会非常的复杂。
这就是我决定自己封装的原因。
封装过程
在封装之前,首先我们要讨论一下问题,我要怎么用才方便?如何灵活的扩展绘制内容?如何处理动画交互逻辑?
封装思路
如果要封装的话, 我们将绘制单独独立出来,通过组合模式,将不同的绘制内容组合成一整个,这样不同的绘制内容可以单独处理自己的动画逻辑,交互逻辑,我们可以采用组合模式进行封装,我们可以将每个元素独立绘制再组合,让绘制变得更灵活。
DrawableLayer 图层的封装。
既然要将绘制独立,那么可以将绘制的内容看成一个 图层
,为了可以更好的让每个图层控制自身的绘制逻辑,动画逻辑。在为图层设计一个生命周期,从开始到结束,让图层在结束的时候自己释放资源。
abstract class DrawableLayer {
DrawableLayer({this.label});
final LayerLogger _logger = LayerLogger.create(tag: "Layer");
String? label;
List<Listenable> get listenables;
void init() {}
void attachLayer() {}
void draw(Canvas canvas, Size size);
FutureOr<void> detachLayer() {}
///用于销毁资源
void dispose() {}
@override
String toString() {
return 'DrawableLayer{label: $label,hashCode:$hashCode}';
}
}
listenables
用于控制CustomPainter
的重绘。 (动画控制器,可监听对象)init()
用于当前图层的数据初始化。attachLayer()
是图层被附加到CustomPainter
之前调用。draw()
顾名思义。FutureOr detachLayer()
当图层不再进行绘制,和画布Canvas分离的时候触发。dispose()
顾名思义。
生命周期的执行顺序:init
->attachLayer
->draw
->deachLayer
->dispose
。
细心的朋友可能注意到FutureOr detachLayer()
为什么有个返回值。没错,这个方法允许你延缓图层分离的分离。比如你想做个退出动画,等待某个动画的完成或结束。
经过图层
的封装我们就可以将绘制独立出来,让绘制图层自己处理动画或者资源的初始化和销毁。
CompositeDrawableLayer 图层组合管理的封装。
///组合可绘制图层。
abstract class CompositeDrawableLayer {
///所有可以让CustomPainter重绘的[Listenable]对象合集
List<Listenable> get listenables;
///所有图层列表
List<DrawableLayer> get drawableLayers;
///能力管理者列表
List<AbilityManager> get abilityManagers;
///添加图层
void addLayer(DrawableLayer drawable, {int index = -1});
///移除图层
void removeLayer(DrawableLayer drawable);
///绘制。
void draw(Canvas canvas, Size size);
}
方法注释解释了其作用。
简单说一下abilityManagers
吧,主要是对图层所使用到的部分外部资源进行扩展,让某些功能可以重复利用,比如管理绘制动画的AnimationController
的初始化和销毁,比如图层之间相互通信的EventBus
的处理。之后会详细说明的。
如何去使用这个CompositeDrawableLayer
:
class DrawableLayerPainter extends CustomPainter {
final CompositeDrawableLayer composite;
DrawableLayerPainter({
required this.composite,
}) : super(repaint: Listenable.merge(composite.listenables));
@override
void paint(Canvas canvas, Size size) {
composite.draw(canvas, size);
}
///因为触发绘制已经全部交由repaint控制,所以这里返回false。
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
在CompositeDrawableLayer
的draw
方法中,我们将drawableLayers
进行遍历调用DrawableLayer
的draw
方法。
关于AbilityManager,图层能力的扩展。
我们都知道绘制动画需要AnimationController
,初始化动画控制器需要TickerProvider
。然而我们封装的图层中并不具备这个能力。所以我又设计了一个功能扩展AbilityManager
。
abstract class AbilityManager {
///此方法会在[DrawableLayer]的init方法之前执行。
void onAddDrawableLayer(CompositeDrawableLayer compositeDrawableLayer,
DrawableLayer drawableLayer);
///此方法会在[DrawableLayer]的生命周期的dispose方法之后并且图层列表彻底remove后执行。
void onRemoveDrawableLayer(CompositeDrawableLayer compositeDrawableLayer,
DrawableLayer drawableLayer);
}
在调用CompositeDrawableLayer
的addLayer``removeLayer
的时候会将要添加和要移除的图层通知给AbilityManager
。
当然光只有一个AbilityManager
是无法为图层-DrawableLayer
提供TickerProvider
的所以还需要一个与之对应的混入类,让DrawableLayer
混入想要的功能。
以动画能力为例:
class AnimationAbilityManager implements AbilityManager {
///为动画提供
final TickerProvider _tickerProvider;
AnimationAbilityManager(TickerProvider tickerProvider)
: assert(tickerProvider is TickerProviderStateMixin,
"当使用图层的动画能力时,请使用TickerProviderStateMixin"),
_tickerProvider = tickerProvider;
@override
void onAddDrawableLayer(CompositeDrawableLayer compositeDrawableLayer,
DrawableLayer drawableLayer) {
if (drawableLayer is AnimationAbilityMixin) {
drawableLayer._initTickerProvider(_tickerProvider);
drawableLayer.initAnim();
}
}
@override
void onRemoveDrawableLayer(CompositeDrawableLayer compositeDrawableLayer,
DrawableLayer drawableLayer) {}
}
///为绘制图层提供动画能力,并自动销毁所有动画资源。
///对应[AnimationAbilityManager]
mixin AnimationAbilityMixin on DrawableLayer implements TickerProvider {
TickerProvider? _tickerProvider;
///初始化TickerProvider,此方法由[AnimationAbilityManager]调用。
void _initTickerProvider(TickerProvider tickerProvider) {
_tickerProvider = tickerProvider;
}
///初始化动画,子类图层所有的动画资源的创建都应在当前方法中执行。
///此方法由[AnimationAbilityManager]调用。
void initAnim();
@override
Ticker createTicker(TickerCallback onTick) {
assert(
_tickerProvider != null,
"为图层添加了Animation能力,"
"但是与之对应的AnimationAbilityManager并未添加到CompositeDrawableLayerManager");
return _tickerProvider!.createTicker(onTick);
}
@override
void dispose() {
//在当前图层销毁的时候回收所有动画资源。
for (var listenable in listenables) {
if (listenable is AnimationController) {
try {
listenable.dispose();
} catch (e) {
_logger.log(e.toString());
}
}
}
super.dispose();
}
}
我们从外部也就是Widget中混入TickerProviderStateMixin
并创建AnimationAbilityManager
交给CompositeDrawableLayer
。
然后在我们自己的绘制图层中混入动画能力。
class MyLayer extends DrawableLayer
with AnimationAbilityMixin, EventBusAbilityMixin{
late AnimationController _controller;
@override
List<AnimationController> get listenables => [_controller];
@override
void initAnim(){
_controller =
AnimationController(vsync: this, duration: const Duration(seconds: 1));
}
@override
void draw(Canvas canvas,Size size){}
@override
void dispose() {
super.dispose();
//混入的动画能力,已经自动将资源释放了。不需要主动释放。
//_controller.dispose();
}
}
这样我们就把基础的功能封装完成了。
DrawableLayer
是独立的绘制图层。CompositeDrawableLayer
管理所有图层。AbilityManager
为图层提供各种扩展功能。
绘制的时候有时候也需要在不同的图层间进行通信,实现一些复杂的绘制交互逻辑,于是我还为DrawableLayer
提供了EventBus
的能力。允许图层进行订阅和发布事件。还有感知其他图层添加和移除的能力。扩展丰富了图层的功能,但是也会让图层之间出现耦合。好不容易独立出来的图层,又稀里糊涂的出现了业务逻辑的耦合,但这也是没办法的事,已经寻求不到更好的方案了。
图层的EventBus能力
///EventBus能力管理
///对应[EventBusAbilityMixin]
class EventBusAbilityManager implements AbilityManager {
///事件管理
final LayerEventBus _layerEventBus;
EventBusAbilityManager({LayerEventBus? eventBus})
: _layerEventBus = eventBus ?? LayerEventManagerImp();
@override
void onAddDrawableLayer(CompositeDrawableLayer compositeDrawableLayer,
DrawableLayer drawableLayer) {
if (drawableLayer is EventBusAbilityMixin) {
drawableLayer._initEventBus(_layerEventBus);
drawableLayer.subscribeEvent();
}
}
@override
void onRemoveDrawableLayer(CompositeDrawableLayer compositeDrawableLayer,
DrawableLayer drawableLayer) {}
}
///图层事件混入类,为绘制图层对象提供EventBus能力,并且自动根据生命周期解除event订阅。
///对应 [EventBusAbilityManager]
mixin EventBusAbilityMixin on DrawableLayer implements LayerEventBus {
///事件管理。每个图层都可以使用此事件管理对象。
LayerEventBus? _layerEventBus;
final List<LayerEvent> _autoUnsubscribeList = [];
///初始化event对象
void _initEventBus(LayerEventBus layerEventBus) {
_layerEventBus = layerEventBus;
}
///注册事件,绘制图层的所有事件的订阅代码都应在该方法中实现。
void subscribeEvent();
@override
LayerEvent subscribe<T>(Function(T event) listener) {
assert(_layerEventBus != null);
var layerEvent = _layerEventBus!.subscribe(listener);
_autoUnsubscribeList.add(layerEvent);
return layerEvent;
}
@override
void unsubscribe(LayerEvent event) {
_layerEventBus?.unsubscribe(event);
_autoUnsubscribeList.remove(event);
}
@override
void publish<T>(T event) {
_layerEventBus?.publish(event);
}
@override
void dispose() {
assert(
_layerEventBus != null,
"为图层添加了EventBus能力,"
"但是与之对应的EventBusAbilityManager并未添加到CompositeDrawableLayerManager");
for (var layerEvent in _autoUnsubscribeList) {
_layerEventBus!.unsubscribe(layerEvent);
}
_autoUnsubscribeList.clear();
super.dispose();
}
}
感知其他图层添加和移除的能力
///图层感知存在管理器类。
///对应[LayerExistAbilityMixin]
class LayerExistAbilityManager implements AbilityManager {
@override
void onAddDrawableLayer(CompositeDrawableLayer compositeDrawableLayer,
DrawableLayer drawableLayer) {
//会通知所有图层包括还没有被删除的图层,但是不会通知当前被添加的图层。
var drawableLayers = compositeDrawableLayer.drawableLayers;
for (var dl in drawableLayers) {
if (dl != drawableLayer && dl is LayerExistAbilityMixin) {
dl.onDrawableLayerAdd(drawableLayer);
}
}
}
@override
void onRemoveDrawableLayer(CompositeDrawableLayer compositeDrawableLayer,
DrawableLayer drawableLayer) {
//会通知所有图层包括还没有被删除的图层,但是不会通知当前被删除的图层。
var drawableLayers = compositeDrawableLayer.drawableLayers;
for (var dl in drawableLayers) {
if (dl != drawableLayer && dl is LayerExistAbilityMixin) {
dl.onDrawableLayerRemove(drawableLayer);
}
}
}
}
///感知其他图层的存在,图层可以接收到其他图层被添加和被移除的通知。
///对应 [LayerExistAbilityManager]
mixin LayerExistAbilityMixin on DrawableLayer {
///当新的图层被添加的时候,会调用此方法。
///[drawableLayer] - 被添加的图层。
void onDrawableLayerAdd(DrawableLayer drawableLayer) {}
///当图层被移除的时候,会调用此方法。
///[drawableLayer] - 被移除的图层。
void onDrawableLayerRemove(DrawableLayer drawableLayer) {}
}
封装效果
我找到了一个非常符合我封装业务的功能。绘制一组天气效果。
参考了站酷-天气不错的UI设计。
将天气的每个元素都独立成一个图层。这样下雪,下雨,雨夹雪,多云。可以复用每个元素合理组合雨,雪,天空,云,太阳,月亮。实现不同的天气效果。
晴朗的天空
将画面分为了两个图层,背景SkyLayer
,太阳SunLayer
。
![Flutter封装-让Canvas绘制变得更灵活](https://img.blogweb.cn/article/17a809900b5348c48f994da333b47976.webp)
晴朗的夜晚
将画面分为三个图层,我们直接复用背景SkyLayer
, 星空StarLayer
, 流星MeteorLayer
。
![Flutter封装-让Canvas绘制变得更灵活](https://img.blogweb.cn/article/ec300691f7c1403f92c4276744e180a8.webp)
雨夹雪
将画面分为四个图层,复用背景SkyLayer
,乌云CloudLayer
,雨RainLayer
,雪SnowLayer
。
![Flutter封装-让Canvas绘制变得更灵活](https://img.blogweb.cn/article/b4f5acb0ca2c431184baec68fad5a81c.webp)
两张动态图片
![]() | ![]() |
---|---|
![]() | ![]() |
![]() | ![]() |
代码已经开源:Dboy233/nice_weather: Flutter 封装 Canvas 绘制天气效果 (github.com)
如果有想尝试一下封装的代码,直接复制项目中的nice_weather/lib/drawable_layer文件夹。到你的项目中。示例程序直接查看nice_weather/lib/review_weather_anim.dart。
转载自:https://juejin.cn/post/7304538454876340251