-
动画效果事实上 Flutter 里实现类似的动画效果很简单,甚至不需要自定义布局,只需要通过官方的内置控件就可以轻松实现。首先我们需要使用 AnimatedPositioned 和 AnimatedContainer :AnimatedPositioned 用于在 Stack 里实现位移动画效果AnimatedContainer 用于实现大小变化的动画效果接着我们定义一个 PositionItem ,将 AnimatedPositioned 和 AnimatedContainer 嵌套在一起,并且通过 PositionedItemData 用于改变它们的位置和大小。class PositionItem extends StatelessWidget { final PositionedItemData data; final Widget child; const PositionItem(this.data, {required this.child}); @override Widget build(BuildContext context) { return new AnimatedPositioned( duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn, child: new AnimatedContainer( duration: Duration(seconds: 1), curve: Curves.fastOutSlowIn, width: data.width, height: data.height, child: child, ), left: data.left, top: data.top, ); }}class PositionedItemData { final double left; final double top; final double width; final double height; PositionedItemData({ required this.left, required this.top, required this.width, required this.height, });}之后我们只需要把 PositionItem 放到通过 Stack 下,然后通过 LayoutBuilder 获得 parent 的大小,根据 PositionedItemData 调整 PositionItem 的位置和大小,就可以轻松实现开始的动画效果。child: LayoutBuilder( builder: (_, con) { var f = getIndexPosition(currentIndex % 3, con.biggest); var s = getIndexPosition((currentIndex + 1) % 3, con.biggest); var t = getIndexPosition((currentIndex + 2) % 3, con.biggest); return Stack( fit: StackFit.expand, children: [ PositionItem(f, child: InkWell( onTap: () { print("red"); }, child: Container(color: Colors.redAccent), )), PositionItem(s, child: InkWell( onTap: () { print("green"); }, child: Container(color: Colors.greenAccent), )), PositionItem(t, child: InkWell( onTap: () { print("yello"); }, child: Container(color: Colors.yellowAccent), )), ], ); },),如下图所示,只需要每次切换对应的 index ,便可以调整对应 Item 的大小和位置发生变化,从而触发 AnimatedPositioned 和 AnimatedContainer 产生动画效果,达到类似开始时动图的动画效果。计算大小 效果 完整代码可见: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/anim_switch_layout_demo_page.dart如果你对于实现原理没兴趣,那到这里就可以结束了,通过上面你已经知道了一个小技巧:改变 AnimatedPositioned 和 AnimatedContainer 的任意参数,就可以让它们产生动画效果,而它们的参数和 Positioned 与 Container 一模一样,所以使用起来可以无缝替换 Positioned 与 Container ,只需要简单配置额外的 duration 等参数。进阶学习那 AnimatedPositioned 和 AnimatedContainer 是如何实现动画效果 ?这里就要介绍一个抽象父类 ImplicitlyAnimatedWidget 。几乎所有 Animated 开头的控件都是继承于它,既然是用于动画 ,那么 ImplicitlyAnimatedWidget 就肯定是一个 StatefulWidget ,那么不出意外,它的实现逻辑主要在于 ImplicitlyAnimatedWidgetState ,而我们后续也会通过它来展开。首先我们回顾一下,一般在 Flutter 使用动画需要什么:AnimationController : 用于控制动画启动、暂停TickerProvider : 用于创建 AnimationController 所需的 vsync 参数,一般最常使用 SingleTickerProviderStateMixinAnimation : 用于处理动画的 value ,例如常见的 CurvedAnimation接收动画的对象:例如 FadeTransition简单来说,Flutter 里的动画是从 Ticker 开始,当我们在 State 里 with TickerProviderStateMixin 之后,就代表了具备执行动画的能力:每次 Flutter 在绘制帧的时候,Ticker 就会同步到执行 AnimationController 里的 _tick 方法,然后执行 notifyListeners ,改变 Animation 的 value,从而触发 State 的 setState 或者 RenderObject 的 markNeedsPaint 更新界面。举个例子,如下代码所示,可以看到实现一个简单动画效果所需的代码并不少,而且这部分代码重复度很高,所以针对这部分逻辑,官方提供了 ImplicitlyAnimatedWidget 模版。class _AnimatedOpacityState extends State<AnimatedOpacity> with TickerProviderStateMixin { late final AnimationController _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(reverse: true); late final Animation<double> _animation = CurvedAnimation( parent: _controller, curve: Curves.easeIn, ); @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Container( color: Colors.white, child: FadeTransition( opacity: _animation, child: const Padding(padding: EdgeInsets.all(8), child: FlutterLogo()), ), ); }}例如上面的 Fade 动画,换成 ImplicitlyAnimatedWidgetState 只需要实现 forEachTween 方法和 didUpdateTweens 方法即可,而不再需要关心 AnimationController 和 CurvedAnimation 等相关内容。class _AnimatedOpacityState extends ImplicitlyAnimatedWidgetState<AnimatedOpacity> { Tween<double>? _opacity; late Animation<double> _opacityAnimation; @override void forEachTween(TweenVisitor<dynamic> visitor) { _opacity = visitor(_opacity, widget.opacity, (dynamic value) => Tween<double>(begin: value as double)) as Tween<double>?; } @override void didUpdateTweens() { _opacityAnimation = animation.drive(_opacity!); } @override Widget build(BuildContext context) { return FadeTransition( opacity: _opacityAnimation, alwaysIncludeSemantics: widget.alwaysIncludeSemantics, child: widget.child, ); }}那 ImplicitlyAnimatedWidgetState 是如何做到改变 opacity 就触发动画?关键还是在于实现的 forEachTween :当 opacity 被更新时,forEachTween 会被调用,这时候内部会通过 _shouldAnimateTween 判断值是否更改,如果目标值已更改,就执行基类里的 AnimationController.forward 开始动画。这里补充一个内容:FadeTransition 内部会对 _opacityAnimation 添加兼容,当 AnimationController 开始执行动画的时候,就会触发 _opacityAnimation 的监听,从而执行 markNeedsPaint ,而如下图所示, markNeedsPaint 最终会触发 RenderObject 的重绘。所以到这里,我们知道了:通过继承 ImplicitlyAnimatedWidget 和 ImplicitlyAnimatedWidgetState 我们可以更方便实现一些动画效果,Flutter 里的很多默认动画效果都是通过它实现。另外 ImplicitlyAnimatedWidget 模版里,除了 ImplicitlyAnimatedWidgetState ,官方还提供了另外一个子类 AnimatedWidgetBaseState。事实上 Flutter 里我们常用的 Animated 都是通过 ImplicitlyAnimatedWidget 模版实现,如下图所示是 Flutter 里常见的 Animated 分别继承的 State :ImplicitlyAnimatedWidgetState AnimatedWidgetBaseState 关于这两个 State 的区别,简单来说可以理解为:ImplicitlyAnimatedWidgetState 里主要是配合各类 *Transition 控件使用,比如: AnimatedOpacity里使用了 FadeTransition 、AnimatedScale 里使用了 ScaleTransition ,因为 ImplicitlyAnimatedWidgetState 里没有使用 setState,而是通过触发 RenderObject 的 markNeedsPaint 更新界面。AnimatedWidgetBaseState 在原本 ImplicitlyAnimatedWidgetState 的基础上增加了自动 setState 的监听,所以可以做一些更灵活的动画,比如前面我们用过的 AnimatedPositioned 和 AnimatedContainer 。其实 AnimatedContainer 本身就是一个很具备代表性的实现,如果你去看它的源码,就可以看到它的实现很简单,只需要在 forEachTween 里实现参数对应的 Tween 实现即可。例如前面我们改变的 width 和 height ,其实就是改变了Container 的 BoxConstraints ,所以对应的实现也就是 BoxConstraintsTween ,而 BoxConstraintsTween 继承了 Tween ,主要是实现了 Tween 的 lerp 方法。在 Flutter 里 lerp 方法是用于实现插值:例如就是在动画过程中,在 beigin 和 end 两个 BoxConstraint 之间进行线性插值,其中 t 是动画时钟值下的变化值,例如:计算出 100x100 到 200x200 大小的过程中需要的一些中间过程的尺寸。如下代码所示,通过继承 AnimatedWidgetBaseState ,然后利用 ColorTween 的 lerp ,就可以很快实现如下文字的渐变效果。代码 效果 总结最后总结一下,本篇主要介绍了:利用 AnimatedPositioned 和 AnimatedContainer 快速实现切换动画效果介绍 ImplicitlyAnimatedWidget 和如何使用 ImplicitlyAnimatedWidgetState / AnimatedWidgetBaseState 简化实现动画的需求,并且快速实现自定义动画。那么,你还有知道什么使用 Flutter 动画的小技巧吗?原文链接:https://blog.csdn.net/ZuoYueLiang/article/details/125365460
-
动画 Flutter中的动画系统基于Animation对象的,和之前的手势不同,它不是一个Widget,这是因为Animation对象本身和UI渲染没有任何关系。Animation是一个抽象类,就相当于一个定时器,它用于保存动画的插值和状态,并执行数值的变化。widget可以在build函数中读取Animation对象的当前值, 并且可以监听动画的状态改变。AnimationController AnimationController用于控制动画,它包含动画的启动forward()、停止stop() 、反向播放 reverse()等方法。AnimationController会在动画的每一帧,就会生成一个新的值。默认情况下,AnimationController在给定的时间段内线性的生成从0.0到1.0(默认区间)的数字。AnimationController controller = AnimationController( duration: const Duration(milliseconds: 2000), //动画时间 lowerBound: 10.0, //生成数字的区间 upperBound: 20.0, //10.0 - 20.0 vsync: this //TickerProvider 动画驱动器提供者);Ticker Ticker的作用是添加屏幕刷新回调,每次屏幕刷新都会调用TickerCallback。使用Ticker来驱动动画会防止屏幕外动画(动画的UI不在当前屏幕时,如锁屏时)消耗不必要的资源。因为Flutter中屏幕刷新时会通知Ticker,锁屏后屏幕会停止刷新,所以Ticker就不会再触发。最简单的做法为将SingleTickerProviderStateMixin添加到State的定义中。import 'package:flutter/material.dart';void main() => runApp(AnimationApp());class AnimationApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "animation", home: Scaffold( appBar: AppBar( title: Text('animation'), ), body: AnimWidget(), ), ); }}// 动画是有状态的class AnimWidget extends StatefulWidget { @override State<StatefulWidget> createState() { return _AnimWidgetState(); }}class _AnimWidgetState extends State<AnimWidget> with SingleTickerProviderStateMixin { AnimationController controller; bool forward = true; @override void initState() { super.initState(); controller = AnimationController( // 动画的时长 duration: Duration(milliseconds: 2000), lowerBound: 10.0, upperBound: 100.0, // 提供 vsync 最简单的方式,就是直接混入 SingleTickerProviderStateMixin // 如果有多个AnimationController,则使用TickerProviderStateMixin。 vsync: this, ); //状态修改监听 controller ..addStatusListener((AnimationStatus status) { debugPrint("状态:$status"); }) ..addListener(() { setState(() => {}); }); debugPrint("controller.value:${controller.value}"); } @override Widget build(BuildContext context) { return Column( children: <Widget>[ Container( width: controller.value, height: controller.value, color: Colors.blue, ), RaisedButton( child: Text("播放"), onPressed: () { if (forward) { controller.forward(); } else { controller.reverse(); } forward = !forward; }, ), RaisedButton( child: Text("停止"), onPressed: () { controller.stop(); }, ) ], ); }}动画状态监听:在forword结束之后状态为completed。在reverse结束之后状态为dismissedTween 默认情况下,AnimationController对象值为:double类型,范围是0.0到1.0 。如果我们需要不同的范围或不同的数据类型,则可以使用Tween来配置动画以生成不同的范围或数据类型的值。要使用Tween对象,需要调用其animate()方法,然后传入一个控制器对象,同时动画过程中产生的数值由Tween的lerp方法决定。import 'package:flutter/material.dart';void main() => runApp(AnimationApp());class AnimationApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "animation", home: Scaffold( appBar: AppBar( title: Text('animation'), ), body: AnimWidget(), ), ); }}// 动画是有状态的class AnimWidget extends StatefulWidget { @override State<StatefulWidget> createState() { return _AnimWidgetState(); }}class _AnimWidgetState extends State<AnimWidget> with SingleTickerProviderStateMixin { AnimationController controller; bool forward = true; Tween<Color> tween; @override void initState() { super.initState(); controller = AnimationController( // 动画的时长 duration: Duration(milliseconds: 2000), // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin vsync: this, ); //使用Color tween = ColorTween(begin: Colors.blue, end: Colors.yellow); //添加动画值修改监听 tween.animate(controller)..addListener(() => setState(() {})); } @override Widget build(BuildContext context) { return Column( children: <Widget>[ Container( width: 100, height: 100, //获取动画当前值 color: tween.evaluate(controller), ), RaisedButton( child: Text("播放"), onPressed: () { if (forward) { controller.forward(); } else { controller.reverse(); } forward = !forward; }, ), RaisedButton( child: Text("停止"), onPressed: () { controller.stop(); }, ) ], ); }}Curve 动画过程默认是线性的(匀速),如果需要非线形的,比如:加速的或者先加速后减速等。Flutter中可以通过Curve(曲线)来描述动画过程。import 'package:flutter/material.dart';void main() => runApp(AnimationApp());class AnimationApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "animation", home: Scaffold( appBar: AppBar( title: Text('animation'), ), body: AnimWidget(), ), ); }}// 动画是有状态的class AnimWidget extends StatefulWidget { @override State<StatefulWidget> createState() { return _AnimWidgetState(); }}class _AnimWidgetState extends State<AnimWidget> with SingleTickerProviderStateMixin { AnimationController controller; Animation<double> animation; bool forward = true; @override void initState() { super.initState(); controller = AnimationController( // 动画的时长 duration: Duration(milliseconds: 2000), // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin vsync: this, ); //弹性 animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn); //使用Color animation = Tween(begin: 10.0, end: 100.0).animate(animation) ..addListener(() { setState(() => {}); }); } @override Widget build(BuildContext context) { return Column( children: <Widget>[ Container( //不需要转换 width: animation.value, height: animation.value, //获取动画当前值 color: Colors.blue, ), RaisedButton( child: Text("播放"), onPressed: () { if (forward) { controller.forward(); } else { controller.reverse(); } forward = !forward; }, ), RaisedButton( child: Text("停止"), onPressed: () { controller.stop(); }, ) ], ); }}AnimatedWidget 通过上面的学习我们能够感受到Animation对象本身和UI渲染没有任何关系。而通过addListener()和setState() 来更新UI这一步其实是通用的,如果每个动画中都加这么一句是比较繁琐的。AnimatedWidget类封装了调用setState()的细节,简单来说就是自动调用setState()。 Flutter中已经封装了很多动画,比如对widget进行缩放,可以直接使用ScaleTransitionimport 'package:flutter/material.dart';void main() => runApp(AnimationApp());class AnimationApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( title: "animation", home: Scaffold( appBar: AppBar( title: Text('animation'), ), body: AnimWidget(), ), ); }}// 动画是有状态的class AnimWidget extends StatefulWidget { @override State<StatefulWidget> createState() { return _AnimWidgetState(); }}class _AnimWidgetState extends State<AnimWidget> with SingleTickerProviderStateMixin { AnimationController controller; Animation<double> animation; bool forward = true; @override void initState() { super.initState(); controller = AnimationController( // 动画的时长 duration: Duration(milliseconds: 2000), // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin vsync: this, ); //弹性 animation = CurvedAnimation(parent: controller, curve: Curves.bounceIn); //使用Color animation = Tween(begin: 10.0, end: 100.0).animate(animation); } @override Widget build(BuildContext context) { return Column( children: <Widget>[ ScaleTransition( child: Container( width: 100, height: 100, color: Colors.blue, ), scale: controller, ), RaisedButton( child: Text("播放"), onPressed: () { if (forward) { controller.forward(); } else { controller.reverse(); } forward = !forward; }, ), RaisedButton( child: Text("停止"), onPressed: () { controller.stop(); }, ) ], ); }}Hero动画 Hero动画就是在路由切换时,有一个共享的Widget可以在新旧路由间切换,由于共享的Widget在新旧路由页面上的位置、外观可能有所差异,所以在路由切换时会逐渐过渡,这样就会产生一个Hero动画。import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', home: Scaffold( appBar: AppBar( title: Text("主页"), ), body: Route1()), ); }}// 路由Aclass Route1 extends StatelessWidget { @override Widget build(BuildContext context) { return Container( alignment: Alignment.topCenter, child: InkWell( child: Hero( tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同 child: CircleAvatar( backgroundImage: AssetImage( "assets/banner.jpeg", ), ), ), onTap: () { Navigator.push(context, MaterialPageRoute(builder: (_) { return Route2(); })); }, ), ); }}class Route2 extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: Hero( tag: "avatar", //唯一标记,前后两个路由页Hero的tag必须相同 child: Image.asset("assets/banner.jpeg")), ); }}组合动画有些时候我们可能会需要执行一个动画序列执行一些复杂的动画。import 'package:flutter/material.dart';void main() => runApp(MyApp());class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return new MaterialApp( title: 'Flutter Demo', home: Route(), ); }}class Route extends StatefulWidget { @override State<StatefulWidget> createState() { return RouteState(); }}class RouteState extends State<Route> with SingleTickerProviderStateMixin { Animation<Color> color; Animation<double> width; AnimationController controller; @override void initState() { super.initState(); controller = AnimationController( // 动画的时长 duration: Duration(milliseconds: 2000), // 提供 vsync 最简单的方式,就是直接继承 SingleTickerProviderStateMixin vsync: this, ); //高度动画 width = Tween<double>( begin: 100.0, end: 300.0, ).animate( CurvedAnimation( parent: controller, curve: Interval( //间隔,前60%的动画时间 1200ms执行高度变化 0.0, 0.6, ), ), ); color = ColorTween( begin: Colors.green, end: Colors.red, ).animate( CurvedAnimation( parent: controller, curve: Interval( 0.6, 1.0, //高度变化完成后 800ms 执行颜色编码 ), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("主页"), ), body: InkWell( ///1、不用显式的去添加帧监听器,再调用setState() ///2、缩小动画构建的范围,如果没有builder,setState()将会在父widget上下文调用,导致父widget的build方法重新调用,现在只会导致动画widget的build重新调用 child: AnimatedBuilder( animation: controller, builder: (context, child) { return Container( color: color.value, width: width.value, height: 100.0, ); }), onTap: () { controller.forward().whenCompleteOrCancel(() => controller.reverse()); }, ), ); }}打包 Flutter在打Release包时候回使用AOT,因此在对一个Flutter测试时候务必使用Release来进行测试。打包命令:flutter build apk 。当然我们需要打包时,还需要配置一些比如签名的内容。配置这些内容和普通Android工程没有区别,都是在build.gradle中进行,只是Flutter工程AS没有提供GUI。 在Flutter工程的android/app下面的build.gradle可以修改包名、版本等信息,这就不用多说了。获得签名文件之后,将它复制到flutter的android目录: 然后在app的build.gradle中配置:signingConfigs { release { keyAlias 'enjoy' keyPassword '123456' // 因为是放到父级的根目录,使用rootProject // 如果放在这个build.gradle的同级,直接使用file storeFile rootProject.file('enjoy.jks') storePassword '123456' } } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.release } }饼图https://github.com/google/chartsStack布局中的fit属性与Image的fit类似,表示内容的扩充情况。默认为StackFit.loose表示Stack与内容一样大。如果设置为StackFit.passthrough则表示Stack父Widget的约束会传给Stack内部非Positioned的子Widget。效果如代码中的StackFit.dart原文链接:https://blog.csdn.net/u010755471/article/details/124691809
-
flutter如何将文件上传到obs里,使用哪个插件啊,调用那些类啊,求解
-
其实现也比较清晰,对每一个 _RouteEntry,通过调用 didChangeNext 和 didChangePrevious 来建立联系,比如在 didChangeNext 中绑定当前 Route 的 secondaryAnimation 和下一个路由的 animation 进行动画联动,再比如在 didChangePrevious 中获取上一个路由的 title,这个可以用于 CupertinoNavigationBar 中 back 按钮展示上一页面的 title。然后调用 maybeNotifyRouteChange 发出通知,指定当前正在处于展示状态的 Route。最后,遍历 toBeDisposed 执行 _RouteEntry 的销毁,这个列表会保存上面循环处理过程中,确定需要移出的 _RouteEntry,通过调用 OverlayEntry remove 函数(它会将自己从 Overlay 中移除)和 OverlayEntry dispose 函数(它会调用 Route 的 dispose,进行资源释放,比如 TransitionRoute 中 AnimationController 销毁)。最后再看关于状态的处理,以下是所有的状态:enum _RouteLifecycle { staging, // we will wait for transition delegate to decide what to do with this route. // // routes that are present: // add, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages adding, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages // routes that are ready for transition. push, // we'll want to run install, didPush, etc; a route added via push() and friends pushReplace, // we'll want to run install, didPush, etc; a route added via pushReplace() and friends pushing, // we're waiting for the future from didPush to complete replace, // we'll want to run install, didReplace, etc; a route added via replace() and friends idle, // route is being harmless // // routes that are not present: // // routes that should be included in route announcement and should still listen to transition changes. pop, // we'll want to call didPop remove, // we'll want to run didReplace/didRemove etc // routes should not be included in route announcement but should still listen to transition changes. popping, // we're waiting for the route to call finalizeRoute to switch to dispose removing, // we are waiting for subsequent routes to be done animating, then will switch to dispose // routes that are completely removed from the navigator and overlay. dispose, // we will dispose the route momentarily disposed, // we have disposed the route}本质上这些状态分为三类,add(处理初始化的时候直接添加),push(与 add 类似,但是增加了动画的处理),pop(处理页面移出),remove(移出某个页面,相对 pop 没有动画,也没有位置限制)。addadd 方式添加路由目前还只用于在应用初始化是添加初始化页面使用,对应的是在 NavigatorState 的 initState 中,void initState() { super.initState(); for (final NavigatorObserver observer in widget.observers) { assert(observer.navigator == null); observer._navigator = this; } String initialRoute = widget.initialRoute; if (widget.pages.isNotEmpty) { _history.addAll( widget.pages.map((Page<dynamic> page) => _RouteEntry( page.createRoute(context), initialState: _RouteLifecycle.add, )) ); } else { // If there is no page provided, we will need to provide default route // to initialize the navigator. initialRoute = initialRoute ?? Navigator.defaultRouteName; } if (initialRoute != null) { _history.addAll( widget.onGenerateInitialRoutes( this, widget.initialRoute ?? Navigator.defaultRouteName ).map((Route<dynamic> route) => _RouteEntry( route, initialState: _RouteLifecycle.add, ), ), ); } _flushHistoryUpdates();}它会将从 onGenerateInitialRoutes 得来的所有初始路由转成 _RouteEntry 加入到 _history,此时它们的状态是 _RouteLifecycle.add,然后就是调用 _flushHistoryUpdates 进行处理。void _flushHistoryUpdates({bool rearrangeOverlay = true}) { // ... while (index >= 0) { switch (entry.currentState) { case _RouteLifecycle.add: assert(rearrangeOverlay); entry.handleAdd( navigator: this, ); assert(entry.currentState == _RouteLifecycle.adding); continue; case _RouteLifecycle.adding: if (canRemoveOrAdd || next == null) { entry.didAdd( navigator: this, previous: previous?.route, previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route, isNewFirst: next == null ); assert(entry.currentState == _RouteLifecycle.idle); continue; } break; case _RouteLifecycle.idle: if (!seenTopActiveRoute && poppedRoute != null) entry.handleDidPopNext(poppedRoute); seenTopActiveRoute = true; // This route is idle, so we are allowed to remove subsequent (earlier) // routes that are waiting to be removed silently: canRemoveOrAdd = true; break; // ... } index -= 1; next = entry; entry = previous; previous = index > 0 ? _history[index - 1] : null; } // ...}add 路线主要会调用两个函数,handleAdd 和 didAdd,void handleAdd({ @required NavigatorState navigator}) { assert(currentState == _RouteLifecycle.add); assert(navigator != null); assert(navigator._debugLocked); assert(route._navigator == null); route._navigator = navigator; route.install(); assert(route.overlayEntries.isNotEmpty); currentState = _RouteLifecycle.adding;}install 函数可以看作是 Route 的初始化函数,比如在 ModalRoute 中创建 ProxyAnimation 来管理一些动画的执行,在 TransitionRoute 中创建了用于执行切换动画的 AnimationController,在 OverlayRoute 中完成了当前 Route 的 OverlayEntry 的创建及插入。createOverlayEntries 用于创建 OverlayEntry,其实现在 ModalRoute,Iterable<OverlayEntry> createOverlayEntries() sync* { yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier); yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);}每一个 Route 都能生成两个 OverlayEntry,一个是 _buildModalBarrier,它可以生成两个页面之间的屏障,我们可以利用它给新页面设置一个背景色,同时还支持动画过渡,另一个是 _buildModalScope,它生成的就是这个页面真正的内容,外部会有多层包装,最底层就是 WidgetBuilder 创建的 widget。大致看下两个函数的实现,Widget _buildModalBarrier(BuildContext context) { Widget barrier; if (barrierColor != null && !offstage) { // changedInternalState is called if these update assert(barrierColor != _kTransparent); final Animation<Color> color = animation.drive( ColorTween( begin: _kTransparent, end: barrierColor, // changedInternalState is called if this updates ).chain(_easeCurveTween), ); barrier = AnimatedModalBarrier( color: color, dismissible: barrierDismissible, // changedInternalState is called if this updates semanticsLabel: barrierLabel, // changedInternalState is called if this updates barrierSemanticsDismissible: semanticsDismissible, ); } else { barrier = ModalBarrier( dismissible: barrierDismissible, // changedInternalState is called if this updates semanticsLabel: barrierLabel, // changedInternalState is called if this updates barrierSemanticsDismissible: semanticsDismissible, ); } return IgnorePointer( ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture child: barrier, );}ModalBarrier 是两个 Route 之间的屏障,它可以通过颜色、拦截事件来表示两个 Route 的隔离,这些都是可以配置的,这里 IgnorePointer 的作用是为了在执行切换动画的时候无法响应时间。Widget _buildModalScope(BuildContext context) { return _modalScopeCache ??= _ModalScope<T>( key: _scopeKey, route: this, // _ModalScope calls buildTransitions() and buildChild(), defined above );} Widget build(BuildContext context) { return _ModalScopeStatus( route: widget.route, isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates canPop: widget.route.canPop, // _routeSetState is called if this updates child: Offstage( offstage: widget.route.offstage, // _routeSetState is called if this updates child: PageStorage( bucket: widget.route._storageBucket, // immutable child: FocusScope( node: focusScopeNode, // immutable child: RepaintBoundary( child: AnimatedBuilder( animation: _listenable, // immutable builder: (BuildContext context, Widget child) { return widget.route.buildTransitions( context, widget.route.animation, widget.route.secondaryAnimation, IgnorePointer( ignoring: widget.route.animation?.status == AnimationStatus.reverse, child: child, ), ); }, child: _page ??= RepaintBoundary( key: widget.route._subtreeKey, // immutable child: Builder( builder: (BuildContext context) { return widget.route.buildPage( context, widget.route.animation, widget.route.secondaryAnimation, ); }, ), ), ), ), ), ), ), );}_ModalScope 需要承载用户界面的展示,它的 build 函数可以看到在 widget.route.buildPage 出用户定义的页面之上有很多层,可以一层一层看下大致作用:_ModalScopeStatus,继承自 InheritedWidget,用于给底层结点提供数据Offstage,可以通过 offstage 变量控制是否绘制PageStorage,它提供了一种存储策略,也就是 PageStorageBucket,这个类可以给某一个 BuildContext 绑定特定的数据,支持写入和读取,可用于某一个 widget 的状态存储等FocusScope,用于焦点管理用,一般只有获取焦点的控件才能接收到按键信息等RepaintBoundary,控制重绘范围,意在减少不必要的重绘AnimatedBuilder,动画控制 Widget,会根据 animation 进行 rebuildwidget.route.buildTransitions,它在不同的 Route 中可以有不同的实现,比如 Android 的默认实现是自下向上渐入,ios 的默认实现是自右向左滑动,另外也可以通过自定义 Route 或自定义 ThemeData 实现自定义的切换动画,还有一点需要说明,Route 中的动画分为 animation 和 secondaryAnimation,其中 animation 定义了自己 push 时的动画,secondaryAnimation 定义的是新页面 push 时自己的动画,举个例子,在 ios 风格中,新页面自右向左滑动,上一个页面也会滑动,此时控制上一个页面滑动的动画就是 secondaryAnimationIgnorePointer,同样是用于页面切换动画执行中,禁止用户操作RepaintBoundary,这里的考量应该是考虑到上层有一个动画执行,所以这里包一下避免固定内容重绘Builder,Builder 的唯一作用应该是提供 BuildContext,虽然说每一个 build 函数都有 BuildContext 参数,但这个是当前 Widget 的,而不是直属上级的,这可能有点抽象,比如说下面的 buildPage 需要使用 BuildContext 作为参数,那么如果它需要使用 context 的 ancestorStateOfType 的话,实际上就是从 _ModalScopeState 开始向上查找,而不是从 Builder 开始向上查找widget.route.buildPage,这个函数内部就是使用 Route 的 WidgetBuilder 创建用户界面,当然不同的 Route 可能还会在这里再次进行包装以上就是一个页面中,从 Overlay(说是 Overlay 不是那么合理,但是在此先省略中间的 _Theatre 等) 往下的布局嵌套。新的 OverlayEntry 创建完成之后,会把它们都传递到 Overlay 中,且在这个过程中会调用 Overlay 的 setState 函数,请求重新绘制,在 Overlay 中实现新旧页面的切换。以上是 install 的整个过程,执行完了之后把 currentState 置为 adding 返回。
-
flutter 应用的运行需要依赖 MaterialApp/CupertinoApp 这两个 Widget,他们分别对应着 android/ios 的设计风格,同时也为应用的运行提供了一些基本的设施,比如与路由相关的主页面、路由表等,再比如跟整体页面展示相关的 theme、locale 等。其中与路由相关的几项配置有 home、routes、initialRoute、onGenerateRoute、onUnknownRoute,它们分别对应着主页面 widget、路由表(根据路由找到对应 widget)、首次加载时的路由、路由生成器、未知路由代理(比如常见的 404 页面)。MaterialApp/CupertinoApp 的子结点都是 WidgetsApp,只不过他们给 WidgetsApp 传入了不同的参数,从而使得两种 Widget 的界面风格不一致。Navigator 就是在 WidgetsApp 中创建的,Widget build(BuildContext context) { Widget navigator; if (_navigator != null) { navigator = Navigator( key: _navigator, // If window.defaultRouteName isn't '/', we should assume it was set // intentionally via `setInitialRoute`, and should override whatever // is in [widget.initialRoute]. initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName ? WidgetsBinding.instance.window.defaultRouteName : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName, onGenerateRoute: _onGenerateRoute, onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null ? Navigator.defaultGenerateInitialRoutes : (NavigatorState navigator, String initialRouteName) { return widget.onGenerateInitialRoutes(initialRouteName); }, onUnknownRoute: _onUnknownRoute, observers: widget.navigatorObservers, ); } ...}在 WidgetsApp 的 build 中第一个创建的就是 Navigator,主要看一下它的参数,首先,_navigator 是一个 GlobalKey,使得 WidgetsApp 可以通过 key 调用 Navigator 的函数进行路由切换,也就是在 WidgetsBinding 中处理 native 的路由切换信息的时候,最终是由 WidgetsApp 完成的。另外这里的 _navigator 应该只在 WidgetsApp 中有使用,其他地方需要使用一般是直接调用 Navigator.of 获取,这个函数会沿着 element 树向上查找到 NavigatorState,所以在应用中切换路由是需要被 Navigator 包裹的,不过由于 WidgetsApp 中都有生成 Navigator,开发中也不必考虑这些。另外,就是关于底层获取上层 NavigatorElement 实例的方式,在 Element 树中有两种方式可以从底层获取到上层的实例,一种方式是使用 InheritedWidget,另一种就是直接沿着树向上查找(ancestorXXXOfExactType 系列),两种方式的原理基本是一致的,只不过 InheritedWidget 在建立树的过程中会一层层向下传递,而后者是使用的时候才向上查找,所以从这个角度来说使用 InheritedWidget 会高效些,但是 InheritedWidget 的优势不止如此,它是能够在数据发生改变的时候通知所有依赖它的结点进行更新,这也是 ancestorXXXOfExactType 系列所没有的。observers 是路由切换的监听列表,可以由外部传入,在路由切换的时候做些操作,比如 HeroController 就是一个监听者。Navigator 是一个 StatefulWidget,在 NavigatorState 的 initState 中完成了将 initRoute 转换成 Route 的过程,并调用 push 将其入栈,生成 OverlayEntry,这个会继续传递给下层负责显示页面的 Overlay 负责展示。在 push 的过程中,route 会被转换成 OverlayEntry 列表存放,每一个 OverlayEntry 中存储一个 WidgetBuilder,从某种角度来说,OverlayEntry 可以被认为是一个页面。所有的页面的协调、展示是通过 Overlay 完成的,Overlay 是一个类似于 Stack 的结构,它可以展示多个子结点。在它的 initState 中,void initState() { super.initState(); insertAll(widget.initialEntries);}会将 initialEntries 都存到 _entries 中。Overlay 作为一个能够根据路由确定展示页面的控件,它的实现其实比较简单:Widget build(BuildContext context) { // These lists are filled backwards. For the offstage children that // does not matter since they aren't rendered, but for the onstage // children we reverse the list below before adding it to the tree. final List<Widget> onstageChildren = <Widget>[]; final List<Widget> offstageChildren = <Widget>[]; bool onstage = true; for (int i = _entries.length - 1; i >= 0; i -= 1) { final OverlayEntry entry = _entries[i]; if (onstage) { onstageChildren.add(_OverlayEntry(entry)); if (entry.opaque) onstage = false; } else if (entry.maintainState) { offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry))); } } return _Theatre( onstage: Stack( fit: StackFit.expand, children: onstageChildren.reversed.toList(growable: false), ), offstage: offstageChildren, );}build 函数中,将所有的 OverlayEntry 分成了可见与不可见两部分,每一个 OverlayEntry 生成一个 _OverlayEntry,这是一个 StatefulWidget,它的作用主要是负责控制当前页重绘,都被封装成 然后再用 _Theatre 展示就完了,在 _Theatre 中,可见/不可见的子结点都会转成 Element,但是在绘制的时候,_Theatre 对应的 _RenderTheatre 只会把可见的子结点绘制出来。判断某一个 OverlayEntry 是否能够完全遮挡上一个 OverlayEntry 是通过它的 opaque 变量判断的,而 opaque 又是由 Route 给出的,在页面动画执行时,这个值会被设置成 false,然后在页面切换动画执行完了之后就会把 Route 的 opaque 参数赋值给它的 OverlayEntry,一般情况下,窗口对应的 Route 为 false,页面对应的 Route 为 true。 所以说在页面切换之后,上一个页面始终都是存在于 element 树中的,只不过在 RenderObject 中没有将其绘制出来,这一点在 Flutter Outline 工具里面也能够体现。从这个角度也可以理解为,在 flutter 中页面越多,需要处理的步骤就越多,虽然不需要绘制底部的页面,但是整个树的基本遍历还是会有的,这部分也算是开销。_routeNamedflutter 中进行页面管理主要的依赖路由管理系统,它的入口就是 Navigator,它所管理的东西,本质上就是承载着用户页面的 Route,但是在 Navigator 中有很多函数是 XXXName 系列的,它们传的不是 Route,而是 RouteName,据个人理解,这个主要是方便开发引入的,我们可以在 MaterialApp/CupertinoApp 中直接传入路由表,每一个名字对应一个 WidgetBuilder,然后结合 pageRouteBuilder(这个可以自定义,不过 MaterialApp/CupertinoApp 都有默认实现,能够将 WidgetBuilder 转成 Route),便可以实现从 RouteName 到 Route 的转换。Route<T> _routeNamed<T>(String name, { @required Object arguments, bool allowNull = false }) { if (allowNull && widget.onGenerateRoute == null) return null; final RouteSettings settings = RouteSettings( name: name, arguments: arguments, ); Route<T> route = widget.onGenerateRoute(settings) as Route<T>; if (route == null && !allowNull) { route = widget.onUnknownRoute(settings) as Route<T>; } return route;}这个过程分三步,生成 RouteSettings,调用 onGenerateRoute 从路由表中拿到对应的路由,如果无命中,就调用 onUnknownRoute 给一个类似于 404 页面的东西。onGenerateRoute 和 onUnknownRoute 在构建 Navigator 时传入,在 WidgetsApp 中实现,Route<dynamic> _onGenerateRoute(RouteSettings settings) { final String name = settings.name; final WidgetBuilder pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null ? (BuildContext context) => widget.home : widget.routes[name]; if (pageContentBuilder != null) { final Route<dynamic> route = widget.pageRouteBuilder<dynamic>( settings, pageContentBuilder, ); return route; } if (widget.onGenerateRoute != null) return widget.onGenerateRoute(settings); return null;}如果是默认的路由会直接使用给定的 home 页面(如果有),否则就直接到路由表查,所以本质上这里的 home 页面更多的是一种象征,身份的象征,没有也无所谓。另外路由表主要的产出是 WidgetBuilder,它需要经过一次包装,成为 Route 才是成品,或者如果不想使用路由表这种,也可以直接实现 onGenerateRoute 函数,根据 RouteSetting 直接生成 Route,这个就不仅仅是返回 WidgetBuilder 这么简单了,需要自己包装。onUnknownRoute 主要用于兜底,提供一个类似于 404 的页面,它也是需要直接返回 Route。_flushHistoryUpdates不知道从哪一个版本开始,flutter 的路由管理引入了状态,与之前每一个 push、pop 都单独实现不同,所有的路由切换操作都是用状态表示,同时所有的 route 都被封装成 _RouteEntry,它内部有着关于 Route 操作的实现,但都被划分为比较小的单元,且都依靠状态来执行。状态是一个具有递进关系的枚举,每一个 _RouteEntry 都有一个变量存放当前的状态,在 _flushHistoryUpdates 中会遍历所有的 _RouteEntry 然后根据它们当前的状态进行处理,同时处理完成之后会切换它们的状态,再进行其他处理,这样的好处很明显,所有的路由都放在一起处理之后,整个流程会变得更加清晰,且能够很大程度上进行代码复用,比如 push 和 pushReplacement 两种操作,这在之前是需要在两个方法中单独实现的,而现在他们则可以放在一起单独处理,不同的只有后者比前者会多一个 remove 的操作。关于 _flushHistoryUpdates 的处理步骤:void _flushHistoryUpdates({bool rearrangeOverlay = true}) { assert(_debugLocked && !_debugUpdatingPage); // Clean up the list, sending updates to the routes that changed. Notably, // we don't send the didChangePrevious/didChangeNext updates to those that // did not change at this point, because we're not yet sure exactly what the // routes will be at the end of the day (some might get disposed). int index = _history.length - 1; _RouteEntry next; _RouteEntry entry = _history[index]; _RouteEntry previous = index > 0 ? _history[index - 1] : null; bool canRemoveOrAdd = false; // Whether there is a fully opaque route on top to silently remove or add route underneath. Route<dynamic> poppedRoute; // The route that should trigger didPopNext on the top active route. bool seenTopActiveRoute = false; // Whether we've seen the route that would get didPopNext. final List<_RouteEntry> toBeDisposed = <_RouteEntry>[]; while (index >= 0) { switch (entry.currentState) { // ... } index -= 1; next = entry; entry = previous; previous = index > 0 ? _history[index - 1] : null; } // Now that the list is clean, send the didChangeNext/didChangePrevious // notifications. _flushRouteAnnouncement(); // Announces route name changes. final _RouteEntry lastEntry = _history.lastWhere(_RouteEntry.isPresentPredicate, orElse: () => null); final String routeName = lastEntry?.route?.settings?.name; if (routeName != _lastAnnouncedRouteName) { RouteNotificationMessages.maybeNotifyRouteChange(routeName, _lastAnnouncedRouteName); _lastAnnouncedRouteName = routeName; } // Lastly, removes the overlay entries of all marked entries and disposes // them. for (final _RouteEntry entry in toBeDisposed) { for (final OverlayEntry overlayEntry in entry.route.overlayEntries) overlayEntry.remove(); entry.dispose(); } if (rearrangeOverlay) overlay?.rearrange(_allRouteOverlayEntries);}以上是除了状态处理之外,一次 _flushHistoryUpdates 的全过程,首先它会遍历整个路由列表,根据状态做不同的处理,不过一般能够处理到的也不过最上层一两个,其余的多半是直接跳过的。处理完了之后,调用 _flushRouteAnnouncement 进行路由之间的前后链接,比如进行动画的联动等,void _flushRouteAnnouncement() { int index = _history.length - 1; while (index >= 0) { final _RouteEntry entry = _history[index]; if (!entry.suitableForAnnouncement) { index -= 1; continue; } final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.suitableForTransitionAnimationPredicate); if (next?.route != entry.lastAnnouncedNextRoute) { if (entry.shouldAnnounceChangeToNext(next?.route)) { entry.route.didChangeNext(next?.route); } entry.lastAnnouncedNextRoute = next?.route; } final _RouteEntry previous = _getRouteBefore(index - 1, _RouteEntry.suitableForTransitionAnimationPredicate); if (previous?.route != entry.lastAnnouncedPreviousRoute) { entry.route.didChangePrevious(previous?.route); entry.lastAnnouncedPreviousRoute = previous?.route; } index -= 1; }}
-
一、简介Flutter的深色模式以及跟随系统设置比较简单,我感觉需要注意的是开发过程中尽量使用Theme中的颜色与样式,开发过程中遇到的比较大的坑就是provider的一些问题,可能是因为我用的版本新一些,网上找了很多文章,总会遇到一些问题。本文的深色模式适配是通过修改themeMode来实现的,供诸位有缘人参考。二、环境介绍1. Flutter: 2.0.32. Dart: 2.12.03. provider: 5.0.0状态管理,用于运行时切换主题4. shared_preferences: 2.0.5数据持久化,用于保存当前选中的主题,以便下次启动时读取使用用户选择的主题environment: sdk: ">=2.12.0 <3.0.0"dependencies: flutter: sdk: flutter # 忽略了一些依赖... # shared_preferences https://pub.flutter-io.cn/packages/shared_preferences shared_preferences: ^2.0.5 # 全局状态管理 https://github.com/rrousselGit/provider/blob/master/resources/translations/zh-CN/README.md provider: ^5.0.0 三、主题1. ThemeData factory ThemeData({ Brightness brightness, // 应用主题亮度,可选(dark、light) VisualDensity visualDensity, // 视觉密度 MaterialColor primarySwatch, // 主要样式,设置primaryColor后该背景色会被覆盖 Color primaryColor, // 主要部分背景颜色(导航和tabBar等) Brightness primaryColorBrightness, // primaryColor的亮度 Color primaryColorLight, // primaryColor的浅色版 Color primaryColorDark, // primaryColor的深色版 Color accentColor, // 前景色(文本,按钮等) Brightness accentColorBrightness, // accentColor的亮度 Color canvasColor, // MaterialType.canvas 的默认颜色 Color shadowColor, // 阴影颜色 Color scaffoldBackgroundColor, // Scaffold的背景颜色。典型Material应用程序或应用程序内页面的背景颜色 Color bottomAppBarColor, // BottomAppBar的默认颜色 Color cardColor, // Card的颜色 Color dividerColor, // Divider和PopupMenuDivider的颜色,也用于ListTile之间、DataTable的行之间等。 Color focusColor, // 突出颜色 Color hoverColor, // hoverColor Color highlightColor, // 高亮颜色,选中在泼墨动画期间使用的突出显示颜色,或用于指示菜单中的项。 Color splashColor, // 墨水飞溅的颜色。InkWell InteractiveInkFeatureFactory splashFactory, // 定义由InkWell和InkResponse反应产生的墨溅的外观。 Color selectedRowColor, // 用于突出显示选定行的颜色。 Color unselectedWidgetColor, // 用于处于非活动(但已启用)状态的小部件的颜色。例如,未选中的复选框。通常与accentColor形成对比。也看到disabledColor。 Color disabledColor, // 禁用状态下部件的颜色,无论其当前状态如何。例如,一个禁用的复选框(可以选中或未选中)。 Color buttonColor, // RaisedButton按钮中使用的Material 的默认填充颜色。 ButtonThemeData buttonTheme, // 定义按钮部件的默认配置, ToggleButton
-
flutter 是一个多终端的前端框架,可以使用一套代码,在IOS,Android 上使用,目前正在支持Windows,Mac等终端,由Google开发并开源。但是由于 flutter 的包以及依赖库,都是架构在Google 自家的服务上,对于国内访问有着很大的问题。希望华为云能够架设相关的镜像,我跟 Flutter 方面联系以及沟通过,建设相关的镜像需要反代两个网址。PUB_HOSTED_URLFLUTTER_STORAGE_BASE_URL他们分别的原始地址是 https://pub.dartlang.org/https://storage.googleapis.com这是官方的介绍https://flutter.io/community/china我也手动反代过,发现需要对 https://pub.dartlang.org/页面里面的一些地址需要替换,例如https://pub.dartlang.org/packages/shared_preferences#-versions-tab-Versions 中的下载地址,需要替换成镜像后FLUTTER_STORAGE_BASE_URL的地址。希望华为云能够支持
推荐直播
-
华为AI技术发展与挑战:集成需求分析的实战指南
2024/11/26 周二 18:20-20:20
Alex 华为云学堂技术讲师
本期直播将综合讨论华为AI技术的发展现状,技术挑战,并深入探讨华为AI应用开发过程中的需求分析过程,从理论到实践帮助开发者快速掌握华为AI应用集成需求的框架和方法。
去报名 -
华为云DataArts+DWS助力企业数据治理一站式解决方案及应用实践
2024/11/27 周三 16:30-18:00
Walter.chi 华为云数据治理DTSE技术布道师
想知道数据治理项目中,数据主题域如何合理划分?数据标准及主数据标准如何制定?数仓分层模型如何合理规划?华为云DataArts+DWS助力企业数据治理项目一站式解决方案和应用实践告诉您答案!本期将从数据趋势、数据治理方案、数据治理规划及落地,案例分享四个方面来助力企业数据治理项目合理咨询规划及顺利实施。
去报名
热门标签