Flutter中的动效实现方式
1. 动效实现方式
1.1 动效实现方式
实现方式示例 示例:使用 AnimatedOpacity widget 进行透明度动画
1.1.1 隐式动画
-
定义
-
通过使用 Flutter 的 动画库,你可以为 UI 中的组件添加运动和创建视觉效果。你可以使用库中的一套组件来管理动画,这些组件统称为隐式动画或隐式动画组件,其名称源于它们都实现了 ImplicitlyAnimatedWidget 类。使用隐式动画,你可以通过设置一个目标值,驱动 widget 的属性进行动画变换;每当目标值发生变化时,属性会从旧值逐渐更新到新值。通过这种方式,隐式动画内部实现了动画控制,从而能够方便地使用— 隐式动画组件会管理动画效果,用户不需要再进行额外的处理。
-
实现方式示例 示例:使用 AnimatedOpacity widget 进行透明度动画
-
选择要进行动画的 widget 属性
-
想要创建淡入效果,可以使用
AnimatedOpacity
widget 对opacity
属性进行动画。将Column
widget 换成AnimatedOpacity
widget:@override Widget build(BuildContext context) {return ListView(children: <Widget>[Image.network(owlUrl),TextButton(child: const Text('Show Details',style: TextStyle(color: Colors.blueAccent),),onPressed: () => {},),const Column(children: [Text('Type: Owl'),Text('Age: 39'),Text('Employment: None'),],),AnimatedOpacity(child: const Column(children: [Text('Type: Owl'),Text('Age: 39'),Text('Employment: None'),],),),]); }
-
为动画属性初始化一个状态变量
class _FadeInDemoState extends State<FadeInDemo> {double opacity = 0;@overrideWidget build(BuildContext context) {return ListView(children: <Widget>[// ...AnimatedOpacity(opacity: opacity,child: const Column(
4. 为动画设置一个时长
除了 opacity
参数以外,AnimatedOpacity
还需要为动画设置 duration。在下面的例子中,动画会以两秒的时长运行:
AnimatedOpacity(duration: const Duration(seconds: 2),opacity: opacity,child: const Column(
5. 为动画设置一个触发器,并选择一个结束值
TextButton(child: const Text('Show Details',style: TextStyle(color: Colors.blueAccent),),onPressed: () => {},onPressed: () => setState(() {opacity = 1;}),
),
使用动画曲线
-
隐式动画还允许你在
duration
时长内控制动画的 速率 变化。用来定义这种速率变化的参数是Curve
,或者Curves
这些已经预定义的曲线。 -
在 上面的示例中可以添加一个
curve
参数,然后将常量easeInOutBack
传递给curve
,即可以自定义动效曲线
AnimatedOpacity(duration: const Duration(seconds: 2),opacity: opacity,curve: Curves.easeInOutBack,child: const Column(children: [Text('Type: Owl'),Text('Age: 39'),Text('Employment: None'),],),
),
curve_ease_in_out_back
3. 其他动效曲线都可以在Curves
类中查看各种曲线的定义
1.1.2 动画控制器
介绍
AnimationController
是个特殊的 Animation
对象,每当硬件准备新帧时,他都会生成一个新值。默认情况下,AnimationController
在给定期间内会线性生成从 0.0 到 1.0 的数字。
基本动画类
-
Animation
对象在一段时间内,持续生成介于两个值之间的插入值 -
CurvedAnimation
定义动画进程为非线性曲线。 -
AnimationController
是个特殊的Animation
对象,每当硬件准备新帧时,他都会生成一个新值 -
Tween
定义动画插入不同的范围或数据类型。
实现方式示例
简单的元素放大示例
1. 实现SingleTickerProviderStateMixin
以 vsync
对象(vsync
的存在防止后台动画消耗不必要的资源)
class _LogoAppState extends State<LogoApp> with SingleTickerProviderStateMixin
2. 定义动画控制器AnimationController
AnimationController controller =AnimationController(duration: const Duration(seconds: 2), vsync: this);
3. 定义动画Animation
Animation<double> animation = Tween<double>(begin: 0, end: 300).animate(controller);
4. widget上的使用
Container(height: 300,width: 300,height: animation.value,width: animation.value,child: const FlutterLogo(),)
监控动画过程
使用 addStatusListener()
作为动画状态的变更提示,比如开始,结束,或改变方向。
animation = Tween<double>(begin: 0, end: 300).animate(controller)..addStatusListener((status) => print('$status'));
上面的例子在起始或结束时,使用 addStatusListener()
反转动画。制造“呼吸”效果
animation = Tween<double>(begin: 0, end: 300).animate(controller)..addStatusListener((status) {if (status == AnimationStatus.completed) {controller.reverse();} else if (status == AnimationStatus.dismissed) {controller.forward();}})
1.1.3 动效组件 Lottie rive
Lottie
使用方式
Lottie. asset ('assets/images/loading_img.json',height: 24.w,width: 24.w,package: Constant. package ,
)控制动画播放// 定义控制器
AnimationController _controller = AnimationController(vsync: this);// 初始化时控制器赋值
Lottie.asset('assets/LottieLogo1.json',controller: _controller,height: 300,onLoaded: (composition) {setState(() {_controller.duration = composition.duration;});},
);// 使用控制器,同:动画控制器
_controller.forward();
_controller.stop();
Rive
介绍:Rive 是一个实时交互式设计和动画工具,该库允许您使用高级 API 完全控制 Rive 文件,以实现简单的交互和动画,以及使用低级 API 在单个画布中为多个画板、动画和状态机创建自定义渲染循环。
使用方式:使用animation进行控制
动画定义:
动画控制实现:
// 定义画板
Artboard? _artboard;// 初始化画板赋值
RiveAnimation.asset('assets/circle_color_test.riv',animations: [_riveAnimation],fit: BoxFit.cover,onInit: (artboard) {_artboard = artboard;},
);// 调用画板修改Animation
SimpleAnimation _animationBlue = SimpleAnimation('blue')
_artboard?.addController(_animationBlue)
// 或者直接修改Animation
setState(() {_riveAnimation = 'blue';
});// 调用Artboard控制动画播放
_artboard?.pause();
_artboard?.play();
使用状态机进行控制:
动画定义
动画控制实现
// 定义状态机属性
SMINumber? color;// 初始化状态机赋值
void _onRiveInit(Artboard artboard) {final StateMachineController? controller =StateMachineController.fromArtboard(artboard, 'State Machine 1');artboard.addController(controller!);color = controller.getNumberInput('color');
}RiveAnimation.asset('packages/package/assets/theme/color_sel.riv',fit: BoxFit.cover,onInit: _onRiveInit,
)// 使用状态机控制动画播放
color?.change(1);
color?.change(2);
组合使用示例:
7MHXxJGHc9s4savv81yem8sg8amwb
https://hackernoon.com/lang/zh/rive-Animation-for-flutter-%E8%BD%BB%E6%9D%BE%E6%9E%84%E5%BB%BA%E4%BB%A4%E4%BA%BA%E6%83%8A%E5%8F%B9%E7%9A%84%E5%8A%A8%E7%94%BB%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E6%8C%87%E5%8D%97
Screenrecorder
对比
优点 | 缺点 | |
---|---|---|
Lottie |
|
|
rive |
|
|
参考:https://medium.com/@sandeepkella23/lottie-vs-rive-to-help-you-decide-which-is-better-for-your-android-development-needs-457341366544
1.1.4图片动效资源
实现方式
针对gif和webp等图片资源实现的动效,flutter官方SDK提供的Image.asset
就可以正常显示:
Image.asset('assets/theme/88.webp',height: 42.w,width: 42.w,fit: BoxFit.cover,gaplessPlayback: false,bundle: PlatformAssetBundle(),package: 'package',
)
优化
但是官方SDK只能显示,无法进行播放进度控制和循环播放等操作(目前已有提案,但是仍未实现)
[Proposal] Add AnimatedImageController to communicate with animated images (GIF, AVIF, APNG, etc)#111150
Improve Images in Flutter to allow for more control over GIFs such as playback status and speed.#59605
因此使用第三方的组件进行播放进度控制:gif: ^2.3.0
实现原理
1. 使用PaintingBinding
将gif/webp等动效文件的帧信息获取并暂存
bytes = provider.bytes;final buffer = await ImmutableBuffer.fromUint8List(bytes);
Codec codec = await PaintingBinding.instance.instantiateImageCodecWithSize(buffer,
);List<ImageInfo> infos = [];
Duration duration = Duration();for (int i = 0; i < codec.frameCount; i++) {FrameInfo frameInfo = await codec.getNextFrame();infos.add(ImageInfo(image: frameInfo.image));duration += frameInfo.duration;
}
2. 使用GifController
(AnimationController
)控制当前在哪一帧,然后获取对应的帧数据进行展示
// ......setState(() {_frameIndex = _frames.isEmpty? 0: ((_frames.length - 1) * _controller.value).floor();});
// ......@override
Widget build(BuildContext context) {final RawImage image = RawImage(image: _frame?.image,width: widget.width,height: widget.height,scale: _frame?.scale ?? 1.0,color: widget.color,colorBlendMode: widget.colorBlendMode,fit: widget.fit,alignment: widget.alignment,repeat: widget.repeat,centerSlice: widget.centerSlice,matchTextDirection: widget.matchTextDirection,);return image;
}
存在的问题
Displaying GIFs causing memory increase and app crash#65815
[Performance] Gif will make GC frequently, and it'll make the phone heat up#80702
1.1.5 序列帧动效
实现方式:
1. UI将动效导出序列帧切图,定义prefix和对应的index
2. 定义帧数使用的Tween Animation
AnimationController _animationController =AnimationController(vsync: this, duration: widget.duration);
Animation<int> _animation = IntTween(begin: beginIndex,end: endIndex).animate(_animationController!);
3. 使用AnimatedBuilder
和Image.asset
渲染动效
AnimatedBuilder(animation: _animation!,builder: (BuildContext context, Widget? child) {return Image.asset('${imagePrefix}${_animation?.value}.${imageType}',package: Constant.package,gaplessPlayback: true,fit: BoxFit.cover,scale: _dpr(),width: double.infinity,height: double.infinity,);});
1.2 性能需求
-
使用V9.0.0线上版本baseline进行本地修改,使用SoloX进行性能数据抓取(参照SoloX(2.5.3)使用说明_mac)。
-
分别对比不添加动效和添加动效的内存消耗
-
将新建的账号添加一台智能体脂秤设备,在release模式下重启app后的数据作为基准数据
-
对比进入基准数据、动画执行、动画执行完毕稳定后的数据
-
-
结果
动效类型
FD/sync_file
CPU
内存
基准
527/57
2.92
378.16
隐式动画、路由动画
524/56
3.12
多次执行6.19
387.07
控制器动画
AnimationController
523/56
5.06
380.33
动效组件 Lottie
529/59
6.69
391.51
动效组件 Lottie循环执行
526/57
7.23
382.85
动效组件rive
525/52
4.48
377(+12)
图片动效资源 webp/gif
530/60
多次执行:620/171
5.34
379.11
多次执行:459.58
加了控制器的webp/gif动效
594/123
多次执行无变化
动效移除后:521.52/52
1.35→5.3
394.65→405.16
序列帧动效(10)
540/63
6.16
382.67
序列帧动效(100)
643/174
节能3.48
389(355)
1.3 扩展
1.3.1与路由组件结合实现页面切换效果
1Screenrecorder
2Screenrecorder
代码
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';class TransformRoute<T> extends PageRoute<T> {final WidgetBuilder builder;final BuildContext childContext;TransformRoute({required this.transitionDuration,required this.useRootNavigator,required this.builder,required this.childContext}): super();@overridefinal Duration transitionDuration;final bool useRootNavigator;// Defines the position and the size of the (opening) [OpenContainer] within// the bounds of the enclosing [Navigator].final RectTween _rectTween = RectTween();AnimationStatus? _lastAnimationStatus;AnimationStatus? _currentAnimationStatus;@overrideTickerFuture didPush() {_takeMeasurements(navigatorContext: childContext);animation!.addStatusListener((AnimationStatus status) {_lastAnimationStatus = _currentAnimationStatus;_currentAnimationStatus = status;});return super.didPush();}@overridebool didPop(T? result) {_takeMeasurements(navigatorContext: subtreeContext!,delayForSourceRoute: true,);return super.didPop(result);}@overridevoid dispose() {super.dispose();}void _takeMeasurements({required BuildContext navigatorContext,bool delayForSourceRoute = false,}) {final RenderBox navigator = Navigator.of(navigatorContext,rootNavigator: useRootNavigator,).context.findRenderObject()! as RenderBox;final Size navSize = _getSize(navigator);_rectTween.end = Offset.zero & navSize;void takeMeasurementsInSourceRoute([Duration? _]) {if (!navigator.attached) {return;}_rectTween.begin = _getRect(childContext, navigator);}if (delayForSourceRoute) {SchedulerBinding.instance.addPostFrameCallback(takeMeasurementsInSourceRoute);} else {takeMeasurementsInSourceRoute();}}Size _getSize(RenderBox render) {assert(render.hasSize);return render.size;}Rect _getRect(BuildContext context, RenderBox ancestor) {final RenderBox render = context.findRenderObject()! as RenderBox;assert(render.hasSize);return MatrixUtils.transformRect(render.getTransformTo(ancestor),Offset.zero & render.size,);}bool get _transitionWasInterrupted {bool wasInProgress = false;bool isInProgress = false;switch (_currentAnimationStatus) {case AnimationStatus.completed:case AnimationStatus.dismissed:isInProgress = false;case AnimationStatus.forward:case AnimationStatus.reverse:isInProgress = true;case null:break;}switch (_lastAnimationStatus) {case AnimationStatus.completed:case AnimationStatus.dismissed:wasInProgress = false;case AnimationStatus.forward:case AnimationStatus.reverse:wasInProgress = true;case null:break;}return wasInProgress && isInProgress;}void closeContainer({T? returnValue}) {Navigator.of(subtreeContext!).pop(returnValue);}@overrideWidget buildPage(BuildContext context,Animation<double> animation,Animation<double> secondaryAnimation,) {return Align(alignment: Alignment.topLeft,child: AnimatedBuilder(animation: animation,builder: (BuildContext context, Widget? child) {final Animation<double> curvedAnimation = CurvedAnimation(parent: animation,curve: Curves.fastOutSlowIn,reverseCurve:_transitionWasInterrupted ? null : Curves.fastOutSlowIn.flipped,);final Rect rect = _rectTween.evaluate(curvedAnimation)!;return SizedBox.expand(child: Align(alignment: Alignment.topLeft,child: Transform.translate(offset: Offset(rect.left, rect.top),child: SizedBox(width: rect.width,height: rect.height,child: Material(clipBehavior: Clip.antiAlias,animationDuration: Duration.zero,child: Stack(fit: StackFit.passthrough,children: <Widget>[// Open child fading in.FittedBox(fit: BoxFit.fitWidth,alignment: Alignment.topLeft,child: SizedBox(width: _rectTween.end!.width,height: _rectTween.end!.height,child: Builder(builder: (BuildContext context) {return builder(context);},),),),],),),),),),);},),);}@overridebool get maintainState => true;@overrideColor? get barrierColor => null;@overridebool get opaque => true;@overridebool get barrierDismissible => false;@overrideString? get barrierLabel => null;
}
1.3.2 与Overlay组件结合实现消息横幅
RPReplay_Final
主要代码
void _createAnimation() {// 此处通过key去获取Widget的Size属性RenderBox renderBox =_childKey.currentContext?.findRenderObject() as RenderBox;Size size = renderBox.size;double deltaY = size.height; // 该值为位移动画需要的位移值// 如果fade动画不存在,则创建一个新的fade动画_fade = Tween<double>(begin: _fadeAnimate ? 0.0 : 1.0, end: 1.0).animate(CurvedAnimation(parent: controller!, curve: Curves.ease));_translate = Tween<double>(begin: -deltaY, end: 0).animate(CurvedAnimation(parent: controller!, curve: Curves.ease)); // 前15%的时间用于执行平移动画
}AnimationController createAnimationController() {return AnimationController(duration: const Duration(milliseconds: 400),debugLabel: debugLabel,vsync: navigator!,);
}Widget _buildAnimation(BuildContext context, Widget? child) {return Transform.translate(offset: Offset(0, _translate?.value ?? 0),child: Opacity(opacity: _fade?.value ?? 0,child: child), // 此处使用translate.value不断取值来刷新child的偏移量);
}child: AnimatedBuilder(builder: _buildAnimation,animation: controller!,child: child,),
2. 总结
动效类型 | 优点 | 缺点 | 动效文件大小 | 是否支持网络加载 | 使用难度 | 是否支持控制器 |
---|---|---|---|---|---|---|
隐式动画 | 使用方便,通过state控制 | 无法实现复杂的动效效果 | / | / | 使用方便,通过state控制 | 否 |
控制器动画 AnimationController | 使用复杂,需要自定义参数和曲线以及运行时机 | / | / | 使用复杂,需要自定义参数和曲线以及运行时机 | 是 | |
动效组件 Lottie | 实现方便,无需复杂切图; 执行完成后性能恢复正常; 不会导致fd增加 | 执行时占用CPU,执行完成后释放 | 开关机:7KB 节能状态:128KB | 支持网络加载 不支持缓存 | 使用方便,
| 是 |
动效组件rive | 动效文件更小; 能更好的控制动效状态和做出更多的交互 | 需要修改ndkVersion "25.1.8937393" | 三种颜色的水波纹放大:3KB | 支持网络加载 | 使用难度高,涉及到artboard、animations、stateMachines多种控制 | 是 |
图片动效资源 webp/gif | 直接使用 | 开关机:gif:11/14KB webp:58/152KB 节能状态:gif:3.7MB webp:5.8MB | 支持网络加载 支持缓存 | 使用方便
| 否 | |
加了控制器的webp/gif动效 | 与非受控相比执行时fd不会增加,且组件移除后快速回落 | 加载时导致fd大量增加 | 支持网络加载 不支持缓存 | 使用方式一般
| 是 | |
序列帧动效 | 执行时不会导致fd和内存增加 | 需要大量图片资源,不适用与server动态配置; 执行时会导致fd和内存增加 | 开关机:2KB*10/2KB*22 节能状态:82KB*119 | 不便于网络加载 | 使用复杂
| 否 |
3. 参考资料
-
https://docs.flutter.cn/codelabs/implicit-animations
-
[Proposal] Add AnimatedImageController to communicate with animated images (GIF, AVIF, APNG, etc)#111150
-
Improve Images in Flutter to allow for more control over GIFs such as playback status and speed.#59605
-
Displaying GIFs causing memory increase and app crash#65815
-
[Performance] Gif will make GC frequently, and it'll make the phone heat up#80702
-
https://docs.flutter.cn/ui/animations/tutorial#monitoring-the-progress-of-the-animation
-
https://github.com/xvrh/lottie-flutter/blob/master/example/lib/examples/animation_full_control.dart
-
https://rive.app/community/doc/animation-playback/docDKKxsr7ko
-
https://hackernoon.com/lang/zh/rive-Animation-for-flutter-%E8%BD%BB%E6%9D%BE%E6%9E%84%E5%BB%BA%E4%BB%A4%E4%BA%BA%E6%83%8A%E5%8F%B9%E7%9A%84%E5%8A%A8%E7%94%BB%E7%9A%84%E5%88%9D%E5%AD%A6%E8%80%85%E6%8C%87%E5%8D%97
-
https://medium.com/@sandeepkella23/lottie-vs-rive-to-help-you-decide-which-is-better-for-your-android-development-needs-457341366544
4. 团队介绍
「三翼鸟数字化技术平台-网器场景」负责网器设备基础数据和计算、规则引擎、网器绑定、网器控制、安防音视频、网器跨平台接入验证等业务,服务产业及海尔智家线上用户;负责网器管理平台建设,提供产业设备基础数据底座、研发产业跨平台网器管理工具等,致力于提升用户交互体验和网器产品的智能化水平。