当前位置: 首页 > news >正文

Flutter中的动效实现方式

1. 动效实现方式

1.1 动效实现方式

  实现方式示例    示例:使用 AnimatedOpacity widget 进行透明度动画

  1.1.1 隐式动画

  1. 定义

    1. 通过使用 Flutter 的 动画库,你可以为 UI 中的组件添加运动和创建视觉效果。你可以使用库中的一套组件来管理动画,这些组件统称为隐式动画隐式动画组件,其名称源于它们都实现了 ImplicitlyAnimatedWidget 类。使用隐式动画,你可以通过设置一个目标值,驱动 widget 的属性进行动画变换;每当目标值发生变化时,属性会从旧值逐渐更新到新值。通过这种方式,隐式动画内部实现了动画控制,从而能够方便地使用— 隐式动画组件会管理动画效果,用户不需要再进行额外的处理。

  实现方式示例    示例:使用 AnimatedOpacity widget 进行透明度动画

  1. 选择要进行动画的 widget 属性

  2. 想要创建淡入效果,可以使用 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'),],),),]);
    }
  3. 为动画属性初始化一个状态变量

    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;}),
    ),

        使用动画曲线

    1. 隐式动画还允许你在 duration 时长内控制动画的 速率 变化。用来定义这种速率变化的参数是 Curve,或者 Curves 这些已经预定义的曲线。

    2. 在 上面的示例中可以添加一个 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. 实现SingleTickerProviderStateMixinvsync 对象(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

    • 成熟且被广泛采用:更大的社区和更多的可用资源。

    • 更简单的工作流程:特别是如果已经使用 After Effects 制作动画。

    • 非常适合预建动画:非常适合集成现有动画或在线找到的动画。

    • 交互性有限:对于需要响应用户输入或动态改变的动画来说并不理想。

    • 较大的文件大小:可能会影响低端设备上的应用程序性能。

    • 对动画状态的控制较少:管理具有多种状态的复杂动画可能更加棘手。

    rive

    • 文件更小:通常会在所有设备上带来更好的性能。

    • 更多交互功能:内置状态机允许对用户输入或应用程序状态做出反应的动画。

    • 更好地控制动画状态:更容易管理具有多种变化的复杂动画。

    • 较新的技术:与 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. 使用AnimatedBuilderImage.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 性能需求

    1. 使用V9.0.0线上版本baseline进行本地修改,使用SoloX进行性能数据抓取(参照SoloX(2.5.3)使用说明_mac)。

    2. 分别对比不添加动效和添加动效的内存消耗

      1. 将新建的账号添加一台智能体脂秤设备,在release模式下重启app后的数据作为基准数据

      2. 对比进入基准数据、动画执行、动画执行完毕稳定后的数据

    3. 结果

      动效类型

      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

      支持网络加载

      不支持缓存

      使用方便,Lottie.asset

      Lottie.network

      动效组件rive

      动效文件更小;

      能更好的控制动效状态和做出更多的交互

      需要修改ndkVersion "25.1.8937393"

      三种颜色的水波纹放大:3KB

      支持网络加载

      使用难度高,涉及到artboard、animations、stateMachines多种控制

      图片动效资源 webp/gif

      直接使用Image.asset展示时多次执行的情况下会导致fd占用增加且执行完成后不回落

      开关机:gif:11/14KB

      webp:58/152KB

      节能状态:gif:3.7MB

      webp:5.8MB

      支持网络加载

      支持缓存

      使用方便

      Image.asset

      加了控制器的webp/gif动效

      与非受控相比执行时fd不会增加,且组件移除后快速回落

      加载时导致fd大量增加

      支持网络加载

      不支持缓存

      使用方式一般

      Gif(controller: controller, image: NetworkImage())

      序列帧动效

      执行时不会导致fd和内存增加

      需要大量图片资源,不适用与server动态配置;

      执行时会导致fd和内存增加

      开关机:2KB*10/2KB*22

      节能状态:82KB*119

      不便于网络加载

      使用复杂

      FrameAnimationImage

      3. 参考资料

      1. https://docs.flutter.cn/codelabs/implicit-animations

      2. [Proposal] Add AnimatedImageController to communicate with animated images (GIF, AVIF, APNG, etc)#111150

      3. Improve Images in Flutter to allow for more control over GIFs such as playback status and speed.#59605

      4. Displaying GIFs causing memory increase and app crash#65815

      5. [Performance] Gif will make GC frequently, and it'll make the phone heat up#80702

      6. https://docs.flutter.cn/ui/animations/tutorial#monitoring-the-progress-of-the-animation

      7. https://github.com/xvrh/lottie-flutter/blob/master/example/lib/examples/animation_full_control.dart

      8. https://rive.app/community/doc/animation-playback/docDKKxsr7ko

      9. 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

      10. https://medium.com/@sandeepkella23/lottie-vs-rive-to-help-you-decide-which-is-better-for-your-android-development-needs-457341366544 

      4. 团队介绍

      三翼鸟数字化技术平台-网器场景」负责网器设备基础数据和计算、规则引擎、网器绑定、网器控制、安防音视频、网器跨平台接入验证等业务,服务产业及海尔智家线上用户;负责网器管理平台建设,提供产业设备基础数据底座、研发产业跨平台网器管理工具等,致力于提升用户交互体验和网器产品的智能化水平。

      http://www.dtcms.com/a/469687.html

      相关文章:

    4. Agent 的感知-决策-行动循环实现
    5. Azure托管标识完整指南:安全无密码的云身份验证
    6. Azure Front Door 在中国区正式上线
    7. 基础 - 正则表达式
    8. 旅游网站系统网站上设置多语言怎么做
    9. 第三方软件验收测试公司【如何深入理解SSL/TLS证书】
    10. JavaWeb——ServletConfig
    11. QwenVL - 202310版-论文阅读
    12. 如何从 FastReport .NET 将报表导出为 JPEG / PNG / BMP / GIF / TIFF / EMF
    13. .NET MCP Server 开发教程
    14. LeetCode 124. 二叉树中的最大路径和(困难)
    15. 建设南大街小学网站wordpress首页调用指定文章列表
    16. 大型语言模型(LLM)基础:从原理到核心概念详解(GPT-4 / 文心一言 / 通义千问)
    17. python高级03——多任务编程
    18. 树模型优劣大比拼xgboost/lightgbm/RF/catboost,股价预测怎么选模型
    19. 哈尔滨快速建站公司推荐营销型网站建设实战》
    20. 4.3-中间件之Kafka
    21. 方寸之间见天地:新兴高端印章的当代破局与价值重构
    22. 如何改善基于深度学习的场重构
    23. Maven 进行项目构建settings.xml 配置教程
    24. 磁力搜索网站怎么做的网站和app设计区别
    25. 西安网站建设公司都有哪些网站设计开发文档模板下载
    26. C++设计模式_结构型模式_桥接模式Bridge
    27. 关于flutter插件的存储位置问题
    28. 把“Mixed Content”吃干抹净——一次 https→http 踩坑实录
    29. 中山大学联合项目 论文解读 | iManip:面向机器人操作的技能增量学习
    30. Unity:Json笔记——Json文件格式、JsonUtlity序列化和反序列化
    31. 第八章 惊喜15 小萍收获初会
    32. RabbitMQ基础知识与Spring Boot 3.x集成案例
    33. 租房网站建设多少钱网站域名怎么改