diff --git a/FWREADME.md b/FWREADME.md index 0e612f4..f781fc6 100644 --- a/FWREADME.md +++ b/FWREADME.md @@ -86,6 +86,8 @@ * [Flutter III 之你不知道的 PlatformView 的混乱之治](Flutter-N30.md) * [掘力计划|Flutter 混合开发的混乱之治【直播回顾】](Flutter-JL2023.md) * [社区说|Flutter 一知半解,带你拨云见月](Flutter-SQS2023.md) +* [Flutter 小技巧之 3.13 全新生命周期 AppLifecycleListener ](Flutter-N31.md) +* [Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗?](Flutter-Rive.md) diff --git a/Flutter-N31.md b/Flutter-N31.md new file mode 100644 index 0000000..10961c8 --- /dev/null +++ b/Flutter-N31.md @@ -0,0 +1,188 @@ +# Flutter 小技巧之 3.13 全新生命周期 AppLifecycleListener + +Flutter 3.13 在 Framework 里添加了 `AppLifecycleListener` 用于监听应用生命周期变化,并响应退出应用的请求等支持,那它有什么特殊之处?和老的相比又有什么不同? + +简单说,在 Flutter 3.13 之前,我们一般都是用 `WidgetsBindingObserver` 的 `didChangeAppLifecycleState` 来实现生命周期的监听,只是 `didChangeAppLifecycleState` 方法比较「粗暴」,直接返回 `AppLifecycleState` 让用户自己处理,使用的时候需要把整个 `WidgetsBindingObserver` 通过 `mixin` 引入。 + +![](http://img.cdn.guoshuyu.cn/20230821_FL/image1.png) + +而 `AppLifecycleListener` 则是在 `WidgetsBindingObserver.didChangeAppLifecycleState` 的基础上进行了封装,再配合当前 `lifecycleState` 形成更完整的生命周期链条,对于开发者来说就是使用更方便,并且 API 相应更直观。 + +![](http://img.cdn.guoshuyu.cn/20230821_FL/image2.png) + +首先 `AppLifecycleListener` 是一个完整的类,所以使用它无需使用 `mixin` ,你只需要在使用的地方创建一个 `AppLifecycleListener` 对象即可。 + +```dart +late final AppLifecycleListener _listener; +late AppLifecycleState? _state; +@override +void initState() { + super.initState(); + _state = SchedulerBinding.instance.lifecycleState; + _listener = AppLifecycleListener( + onShow: () => _handleTransition('show'), + onResume: () => _handleTransition('resume'), + onHide: () => _handleTransition('hide'), + onInactive: () => _handleTransition('inactive'), + onPause: () => _handleTransition('pause'), + onDetach: () => _handleTransition('detach'), + onRestart: () => _handleTransition('restart'), + // This fires for each state change. Callbacks above fire only for + // specific state transitions. + onStateChange: _handleStateChange, + ); +} +void _handleTransition(String name) { + print("########################## main $name"); +} +``` + +其次,`AppLifecycleListener` 根据 `AppLifecycleState` 区分好了所有 Callback 调用,调用编排更加直观。 + +最后,`AppLifecycleListener` 可以更方便去判断和记录整个生命周期的链条变化,因为它已经帮你封装好回调方法,例如: + +- 从 `inactive` 到 `resumed` 调用的是 `onResume` +- 从 `detached` 到 `resumed` 调用的是 `onStart` + +![](http://img.cdn.guoshuyu.cn/20230821_FL/image3.png) + +现在通过 `AppLifecycleListener` 的回调,我们可以更方便和直观的感知到整个生命周期变化的链条,并且 3.13 正式版中还引入了一个全新的状态 : 「`hidden`」,当然它其实在 Android/iOS 上是不工作的。 + +![](http://img.cdn.guoshuyu.cn/20230821_FL/image4.png) + +因为 `hidden` 这个概念在移动 App 上并不实际存在,例如它定义在这里只是为了对齐统一所有状态。 + +虽然在移动 App 平台虽然没有 `hidden` 这个状态,但是例如你在 Android 平台使用 `AppLifecycleListener` ,却还是可以收到 `hidden` 的状态回调,为什么会这样我们后面解释。 + +首先我们简单看下 `AppLifecycleState` 的几个状态: + +#### detached + +App 可能还存有 Flutter Engine ,但是视图并不存在,例如没有 `FlutterView` ,Flutter 初始化之前所处的默认状态。 + +也就是其实没有视图的情况下 Engine 还可以运行,一般来说这个状态仅在 iOS 和 Android 上才有,尽管所有平台上它是开始运行之前的默认状态,一般不严谨要求的情况下,可以简单用于退出 App 的状态监听。 + +#### resumed + +表示 App 处于具有输入焦点且可见的正在运行的状态。 + +例如在 iOS 和 macOS 上对应于在前台活动状态。 + +Android 上无特殊情况对应 `onResume` 状态,但是其实和 `Activity.onWindowFocusChanged ` 有关系。 + +例如当存在多 Activity 时: + +- 只有 Focus 为 true 的 Activity ,进入 `onResume` 才会是 `resumed` +- 其他 Focus 为 false 的 Activity,进入 `onResume` 会是 `inactive` + +只要还是看 `Activity.onWindowFocusChanged ` 回调里是否 Foucs,只是默认情况下 Flutter 只有单 Activity ,所以才说无特殊情况对应 `onResume` 状态。 + +![](http://img.cdn.guoshuyu.cn/20230821_FL/image5.png) + + + +#### inactive + +App 至少一个视图是可见的,但没有一个视图具 Focus。 + +- 在非 Web 桌面平台上,这对应于不在前台但仍具有可见窗口的应用。 +- 在 Web ,这对应没有焦点的窗口或 tab 里运行的应用。 +- 在 iOS 和 macOS 上,对应在前台非活动状态下运行的 Flutter 视图,例如出现电话、生物认证、应用切换、控制中心时。 +- 在 Android 上,这对应 Activity.onPause 已经被调用或 `onResume` 时没有 Focus 的状态。(分屏、被遮挡、画中画) + +> 在 Android 和 iOS 上, inactive 可以认为它们马上会进入 hidden 和 paused 状态。 + +#### paused + +App 当前对用户不可见,并且不响应用户行为。 + +当应用程序处于这个状态时,Engine 不会调用 `PlatformDispatcher.onBeginFrame` 和`PlatformDispatcher.onDrawFrame` 回调。 + +> 仅在 iOS 和 Android 上进入此状态。 + +![](http://img.cdn.guoshuyu.cn/20230821_FL/image6.png) + +#### hidden + +App 的所有视图都被隐藏。 + +- 在 iOS 和 Android 上说明马上要进入 paused。 + +- 在 PC 上说明最小化或者不再可见的桌面上。 +- 在 Web 上说明在不可见的窗口或选项卡中。 + + + +所以从上面可以看到,其实不同平台的生命周期还是存在差异的,而 `AppLifecycleState` 的作用就是屏蔽这些差异,并且由于历史原因,目前 Flutter 的状态名称并不与所平台上的状态名称一一对应,例如 : + +> 在 Android 上,当系统调用 Activity.onPause 时,Flutter 会进入 inactive 状态;但是当 Android 调用 Activity.onStop,Flutter会进入 paused 状态。 + +![](http://img.cdn.guoshuyu.cn/20230821_FL/image7.png) + +> 当然,如果 App 被任务管理器、crash、kill signal 等场景销毁时,用户是无法收到任何回调通知的。 + +那么这时候,你再回过头来看 `hidden` ,就会知道为什么它在 Android 和 iOS 上并没有实际意义,因为它是为了 PC 端(最小化/不可见)而存在,但是如果你通过 `AppLifecycleListener` 进行监听,你会发现其实是可以收到 `hidden` 的回调,例如在 Android 和 iOS 上 : + +- 前台到后台: inactive - hide - pause + +- 后台回前台:restart - show - resume + +明明在原生 Android 和 iOS 上并没有 `hidden` ,那为什么 Dart 里又会触发呢? + +这是因为 Flutter 在 Framework 为了保证 Dart 层面生命周期的一致性,会对生命周期调用进去「补全」。 + +例如在退到后台时,native 端只发送了 `inactive` 和 `pause` 两个状态,但是收到 `pause` 时,在 `_generateStateTransitions` 方法里,会根据 `pause` 在 `AppLifecycleState` 里的位置(pause 和 inactive 之间还有 hidden) ,在代码里「手动」加入 `hidden` 从而触发 `onHide` 调用。 + +![](http://img.cdn.guoshuyu.cn/20230821_FL/image8.png) + +![](http://img.cdn.guoshuyu.cn/20230821_FL/image9.png) + +所以,在 Android 和 iOS 端使用 `AppLifecycleState` 时,我们一般不要去依赖 `onHide` 回调,因为本质上它并不适用于移动端的生命周期。 + +最后,`AppLifecycleState` 还提供了 `onExitRequested` 方法,但是它并不支持类似 Android 的 back 返回拦截场景,而是需要通过 `ServicesBinding.instance.exitApplication(AppExitType exitType)` 触发的退出请求,才可以被 `onExitRequested` 拦截,前提是调用时传入了 `AppExitType.cancelable` 。 + +> 也就是 `ServicesBinding.instance.exitApplication(AppExitType.cancelable);` 这样的调用才会触发 `onExitRequested` ,另外目前 `System.exitApplication` 的响应只在 PC 端实现,移动端不支持。 + +```dart +@override +void initState() { + super.initState(); + _listener = AppLifecycleListener( + onExitRequested: _handleExitRequest, + ); +} + +Future _handleExitRequest() async { + var result = await showDialog( + context: context, + builder: (context) => AlertDialog.adaptive( + title: const Text('Exit'), + content: const Text('Exit'), + actions: [ + TextButton( + child: const Text('No'), + onPressed: () { + Navigator.of(context).pop(false); + }, + ), + TextButton( + child: const Text('Yes'), + onPressed: () { + Navigator.of(context).pop(true); + }, + ), + ], + )); + final AppExitResponse response = + result ? AppExitResponse.exit : AppExitResponse.cancel; + return response; +} +``` + +最后做个总结: + +- `AppLifecycleListener` 的好处就是不用 mixin ,并且通过回调可以判断生命周期链条。 +- `AppLifecycleState` 的状态和命名与原生端并不一定对应。 +- Flutter 在单页面和多页面下可能会出现不同的状态相应。 +- hidden 在 Android 和 iOS 端并不存在,它仅仅是为了统一而手动插入的中间过程。 +- `onExitRequested` 只作用于 PC 端。 \ No newline at end of file diff --git a/Flutter-Rive.md b/Flutter-Rive.md new file mode 100644 index 0000000..a3c00d5 --- /dev/null +++ b/Flutter-Rive.md @@ -0,0 +1,376 @@ +# Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗? + +说到 **[rive](https://rive.app)** ,非 Flutter 开发者可能会感觉比较陌生,而做过 Flutter 开发的可能对 rive 会有所耳闻,因为 rive 在一开始叫 flare ,是 2dimensions 公司的开源动画产品,在发布之初由于和 Flutter 团队有深入合作,所以在初期一直是 Flutter 官方推荐的动画框架之一。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image1.gif) + +# 前言 + +rive 作为一个面向设计师的动画框架,他支持在 **Web Editor** 里进行 UI 编排和动画绘制,当然现在他也支持 PC 客户端开发,整体开发环境需求上相对 Lottie 会轻量化很多。 + +| ![](http://img.cdn.guoshuyu.cn/20230905_Rive/image2.gif) | ![](http://img.cdn.guoshuyu.cn/20230905_Rive/image3.png) | +| -------------------------------------------------------- | -------------------------------------------------------- | + +另外, rive 是通过导出矢量的动画数据文件(也可以包含一些静态资源),然后利用平台的 `Canvas `来实现动画效果,所以它的资源占用体积也不会很大。 + +当然,rive 其实并不是只针对 Flutter, rive 现在也是全平台支持, **Android、 iOS、Web、Desktop、Flutter 、React、Vue、C++** 等等都在支持范围之内。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image4.png) + +关于 rive 的设计端的简单使用,可以看我之前的 [《给掘金 Logo 快速添加动画效果》](https://juejin.cn/post/7126661045564735519) ,其实对于程序员来说,rive 其实很好上手,打开一个 WebEdit 就可以编辑调整。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image5.gif) + +> PS,第二代 rive 和第一代 flare 存在断档不兼容,而且基本可以忽略迁移的可能,当然, **flare 和 rive 其实可以同时存在一个项目不会冲突,所以也不需要当心旧动画的升级问题**。 + +# Rive Flutter + +开始进入主题,其实 rive 比 flare 使用起来更加简单,如下代码所示,只需要通过 `RiveAnimation.asset` 就可以实现一个下图里炫酷的动画效果, + +```dart +dependencies: + rive: 0.9.0 + +import 'package:rive/rive.dart'; +RiveAnimation.asset('static/file/launch.riv'), +``` + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image6.gif) + +当然,除了上面的 `asset` ,还可以通过 `file` 还有 `network` 等方式这加载,这也算是比较常规的集成方式。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image7.png) + +那么使用 rive ,作为开发者端,需要简单知道的几个概念: + +- Artboards:画布,rive 里至少会有一块画布,当然一个 riv 动画文件可以有多个画布 +- animations:需要播放的动画 +- StateMachine:状态机,可以将动画连接在一起并定义切换条件的支持 +- Inputs:StateMachine 的输入,然后可用于与 StateMachine 交互并修改动画切换的状态 + +如下代码所示,**一般情况下我们不需要关心上述设定,因为只要在设计时考虑好默认情况**,那么只需要简单引入就可以播放动画。 + +```dart +RiveAnimation.asset('assets/33333.riv') +``` + +**但是如果你需要更灵活的控制时,就需要理解上述这些设定的作用,后续才能和动画设计师进行有效的沟通和对接**。 + +如下图所示就是对应的设定解读,例如: + +- 知道了画布名称,就可以通过 `artboard` 切换画布 +- 知道动画名称,就可以通过 `animations` 指定动画 +- 知道了状态机名称,就可以通过 `stateMachines` 切换状态机 +- 知道了状态条件,就可以通过 `findInput` 来切换条件变量 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image8.png) + +## animations + +我们先看 animations ,默认情况下 `33333.riv` 这个 riv 动画播放的是 `Shaking` 效果,从上图左下角可以看到 `Shaking` 是一个有循环♻️标识的动画,所以如下图所示车辆动画处于都懂状态。 + +```dart +RiveAnimation.asset('assets/33333.riv') +``` + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image9.gif) + +接着我们更新代码,添加了 `animations` 选择播放 `"Jump"` ,可以看到,车辆播放到了 Jump 效果的动画,并停留不动,因为 Jump 不是循环动画,所以只会播放一次,然后可以看到 `Shaking` 也没有了,因为我们只选中了 `Jump` 。 + +```dart +RiveAnimation.asset( + 'assets/33333.riv', + animations: [ + "Jump", + ], +) +``` + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image10.gif) + +同样,如果我们多选中一个 `Wheel` 动画,可以看到车轮开始动起来,因为 `Wheel` 也是一个循环♻️动画,所以车轮可以一直滚动。 + +```dart +RiveAnimation.asset( + 'assets/33333.riv', + animations: [ + "Jump", + "Wheel", + ], +) +``` + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image11.gif) + +> 所以通过 animations 我们可以快捷组合需要播放的动画效果。 + +## stateMachines & Inputs + +前面我们知道了可以通过 `animations` 配置动画,那么接下来再看看如何通过 `stateMachines` 来控制动画效果。 + +和 `animations` 一样,`stateMachines` 同样是一个`List`,也就是可以配置多个状态,例如通过前面编辑器我们知道,此时 `33333.riv` 的状态机只有一个 `State Machine 1` ,所以我们只需要配置上对应的 `stateMachines` ,就可以看到此时车辆动起来,进入状态机动画模式,也即是 `Entry` 。 + +```dart +RiveAnimation.asset( + 'assets/33333.riv', + stateMachines: [ + "State Machine 1" + ], +``` + + + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image12.gif) + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image13.png) + +那配置 `stateMachines` 只是进入 `Entry`,如果要控制状态变化该怎么办?这就要说到 `Inputs` 。 + +获取 `Inputs` 我们需要在 `_onRiveInit` 回调里去获取,如下代码所示: + +- 首先通过 `StateMachineController.fromArtboard` 获取到状态机的控制器,这样我们使用的是默认画板,所以直接使用初始化时传入的 `artboard` 即可 +- `fromArtboard` 时通过 `State Machine 1` 指定了状态机,然后通过`onStateChange` 监听状态机变化 +- 通过 `addController` 将获取到的状态机控制器添加到画布 +- 通过 `findInput` 找到对应的控制状态 `SMIBool` +- 调用 `change` 改变 ` SMIBool` 的 value 来切换动画状态 + +```dart +RiveAnimation.asset( + 'assets/33333.riv', + onInit: _onRiveInit, +) + +SMIBool? _skin; +void _onRiveInit(Artboard artboard) { + final controller = StateMachineController.fromArtboard( + artboard, + 'State Machine 1', + onStateChange: _onStateChange, + ); + + artboard.addController(controller!); + _skin = controller.findInput('Boolean 1') as SMIBool; +} + +void _onStateChange(String stateMachineName, String stateName) { + print("stateMachineName $stateMachineName stateName $stateName"); +} + +void _swapSkin() { + _skin?.change(!_skin!.value); +} +``` + +为什么这里是 `SMIBool` ? 因为在该状态机设定里用的是 Bool 类型条件。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image14.png) + +当然,除了 Bool 还可以用数字作为判断条件,对应的 Type 类型也会变成 `SMINumber` 。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image15.png) + +另外还有 `SMITrigger` 类型, `SMITrigger` 只需要通过 `fire` 和 `advance` 去控制动画的前后切换,变化也只能单路径模式一个一个切换。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image16.png) + +回到最初的设定里,通过 `_skin?.change(!_skin!.value);` 切换 Bool 值的变化,可以看到此时车辆开始在 Jump 和 Down 进行变化,这就是最简单的状态机和 Input 的示例效果。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image17.gif) + +当然,如下图变高变胖的人就是通过 `SMINumber` 随意切换状态的效果,而小黑人换皮肤,就是通过 `SMITrigger` 单路径模式一个一个切换的动画效果。 + +| ![777777-2](http://img.cdn.guoshuyu.cn/20230905_Rive/image18.gif) | ![](http://img.cdn.guoshuyu.cn/20230905_Rive/image19.gif) | +| ------------------------------------------------------------ | --------------------------------------------------------- | + +当然,动画里也可能会包含多个不同类型的 Input ,你可以通过 `StateMachineController` 的 `Inputs` 参数去获取所有你需要的 Input 去控制动画效果。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image20.png) + + + +## 其他 + +### 布局调整 + +其实了解上面哪些,大致上你就基本学会完美使用 rive 了,剩下的一些参数支持就都是小事,例如: + +```dart +RiveAnimation.network( + 'https://cdn.rive.app/animations/vehicles.riv', + fit: BoxFit.fitWidth, + alignment: Alignment.topCenter, +); +``` + +这里会用到 `fit` 和 `alignment` ,他们都是 Flutter 里常见的配置支持,这里就不多赘述,**默认情况下是 `BoxFit.Contain` 和 `Alignment.Center`** 。 + +### 文本支持 + +新版的 rive 支持运行过程中替换动画文件里的文本内容,**前提是使用新版导出,然后需要编辑器中手动设置名称的文本才能支持该能力**。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image21.png) + +代码上简单说来说,就是在 `onInit` 的时候通过自定义的文本名称,然后通过 `artboard` 获取该节点,从而修改文本内容。 + +```dart +extension _TextExtension on Artboard { + TextValueRun? textRun(String name) => component(name); +} + +RiveAnimation.asset( + 'assets/hello_world_text.riv', + animations: const ['Timeline 1'], + onInit: (artboard) { + final textRun = artboard.textRun('MyRun')!; // find text run named "MyRun" + print('Run text used to be ${textRun.text}'); + textRun.text = 'Hi Flutter Runtime!'; + + }, + +) +``` + +### 播放控制 + +现在的 rive 自带的 `RiveAnimationController` 对比 flare 弱化了很多,基本上就是用来实现简单的 `play` 、`pause ` 和 `stop` 等,默认官方提供了 `SimpleAnimation` 和 `OneShotAnimation ` 两种 `RiveAnimationController` 默认实现。 + +> 一般用不上自定义。 + + `SimpleAnimation` 主要是提供单个动画的简单播放控制,如 play、 pause (`isActive`) 和 reset ,以下是官方 Demo 的示例, + +```dart +class PlayPauseAnimation extends StatefulWidget { + const PlayPauseAnimation({Key? key}) : super(key: key); + + @override + State createState() => _PlayPauseAnimationState(); +} + +class _PlayPauseAnimationState extends State { + /// Controller for playback + late RiveAnimationController _controller; + + /// Toggles between play and pause animation states + void _togglePlay() => + setState(() => _controller.isActive = !_controller.isActive); + + /// Tracks if the animation is playing by whether controller is running + bool get isPlaying => _controller.isActive; + + @override + void initState() { + super.initState(); + _controller = SimpleAnimation('idle'); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Animation Example'), + ), + body: RiveAnimation.asset( + 'assets/off_road_car.riv', + fit: BoxFit.cover, + controllers: [_controller], + // Update the play state when the widget's initialized + onInit: (_) => setState(() {}), + ), + floatingActionButton: FloatingActionButton( + onPressed: _togglePlay, + tooltip: isPlaying ? 'Pause' : 'Play', + child: Icon( + isPlaying ? Icons.pause : Icons.play_arrow, + ), + ), + ); + } +} +``` + +> 主要就是通过 `isActive` 来控制动画的暂停或者播放。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image22.gif) + + + +`OneShotAnimation` 主要用于在播放完一次动画后自动停止并重置动画,以下是官方 Demo 的示例,其实 `OneShotAnimation` 就是继承了 `SimpleAnimation` ,然后在其基础上增加了监听,在播放结束时调用 `reset` 重制动画而已。 + +```dart +/// Demonstrates playing a one-shot animation on demand +class PlayOneShotAnimation extends StatefulWidget { + const PlayOneShotAnimation({Key? key}) : super(key: key); + + @override + State createState() => _PlayOneShotAnimationState(); +} + +class _PlayOneShotAnimationState extends State { + /// Controller for playback + late RiveAnimationController _controller; + + /// Is the animation currently playing? + bool _isPlaying = false; + + @override + void initState() { + super.initState(); + _controller = OneShotAnimation( + 'bounce', + autoplay: false, + onStop: () => setState(() => _isPlaying = false), + onStart: () => setState(() => _isPlaying = true), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('One-Shot Example'), + ), + body: Center( + child: RiveAnimation.asset( + 'assets/vehicles.riv', + animations: const ['idle', 'curves'], + fit: BoxFit.cover, + controllers: [_controller], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _isPlaying ? null : _controller.isActive = true, + tooltip: 'Bounce', + child: const Icon(Icons.arrow_upward), + ), + ); + } +} +``` + +上述代码就是在行驶过程中,点击是触发 `'bounce'` 的一次性跳跃效果,`OneShotAnimation` 主要就是用在类似的一次性动画场景上, + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image23.gif) + +# 最后 + +可以看到 Rive 的使用其实很简单,但是因为状态机的实现,它又可以很灵活地去控制不同动画的效果。 + +一个 riv 文件内可以包含多个画板,画板里可以包含多个动画,多个状态机和输入条件,从而实现多样化的动画效果,甚至实现 Rive 版本的 Flutter 小游戏场景。 + +而且 Rive 并不只是支持 Flutter ,它如今几乎支持所有你能想到的平台,那么这样的一个优秀的平台有什么缺点呢? + +**那就是 Rive 最近开始收费了**,完全的商业化产品, 其实不给钱你也可以用,**只是 Free 模式下已经不是以前那个眉清目秀的 Rive 了**。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image24.png) + +Free 模式的 Rive 会有多个如下图所示的 `Make with Rive` 的水印,同时现在 Free 模式不支持 Share links 了,也就是你自己体验一下,要投入生产使用还是得付费。 + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image25.png) + +![](http://img.cdn.guoshuyu.cn/20230905_Rive/image26.png) + +那么有机智的小伙伴可能就要说了, Rive 不是开源的吗?那我们可以自己弄一套免费的吗? + +答案是可以,但是成本无疑巨大,**因为 Rive 的门槛不在于它开源的端侧 SDK ,而是在于设计端和产出端**,目前的水印是在导出时强制加上的,所以对于使用 Rive 的用户来说,自己搭一套明显不现实。 + +那么,最后,你会愿意为这样一套产品而付费吗?反正我是已经付费ing了。 \ No newline at end of file diff --git a/README.md b/README.md index f25d6b0..798f2e8 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,8 @@ * [Flutter III 之你不知道的 PlatformView 的混乱之治](Flutter-N30.md) * [掘力计划|Flutter 混合开发的混乱之治【直播回顾】](Flutter-JL2023.md) * [社区说|Flutter 一知半解,带你拨云见月](Flutter-SQS2023.md) + * [Flutter 小技巧之 3.13 全新生命周期 AppLifecycleListener ](Flutter-N31.md) + * [Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗?](Flutter-Rive.md) [Flutter 工程化选择](GCH.md) diff --git a/SUMMARY.md b/SUMMARY.md index 67b986c..98e09a8 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -241,6 +241,10 @@ * [社区说|Flutter 一知半解,带你拨云见月](Flutter-SQS2023.md) + * [Flutter 小技巧之 3.13 全新生命周期 AppLifecycleListener ](Flutter-N31.md) + + * [Flutter 最优秀动画库「完全商业化」,Rive 2 你全面了解过吗?](Flutter-Rive.md) + * [Flutter 工程化选择](GCH.md) * [Flutter 工程化框架选择——搞定 Flutter 动画](Z1.md) * [Flutter 工程化框架选择 — 搞定 UI 生产力](Z3.md)