• 原文地址:Flutter Heroes and Villains — bringing balance to the Flutterverse.
  • 原文作者:Norbert
  • 译文出自:掘金翻译计划
  • 本文永久链接:github.com/xitu/gold-m…
  • 译者:DateBro

这是一个关于 Heroes 和 Villains 如何运行的故事。

一个 Hero 常常与多个 Villain 相伴而生。

Villain 允许你只需几行代码就可以添加上面的页面转换。

安装包在这里。你可以在项目的 README 如何使用 Villains。这篇文章更侧重于解释 Heroes 和 Villains 以及所有这些背后的思考过程。

Flutter 最令惊奇的一点是它为所有东西提供漂亮和干净的 API。我喜欢你使用 Hero 的方式。两行简单的代码,它就生效了。你只需要把 Hero 扔到这两个地方,按照标签分配,其它就不需要管了。


在你理解 Villain 之前,你必须先理解 Hero。

先简单了解一下 Hero。

我们来快速了解一下 Hero 是如何实现的。

概览

Hero 的动画涉及三个主要步骤。

1. 找到并匹配 Heroes

第一步是确定哪些 Hero 存在以及哪些 Hero 具有相同的标记。

2. 确定 Hero 位置

然后,捕获两个 Hero 的位置并准备好旅程。

3. 启动旅程

旅程始终在新屏幕上进行,而不在实际的组件中。在开始页面上的组件在旅程期间被替换成空的占位符组件 (SizedBox)。而使用 OverlayOverlay可以在所有内容上显示组件)。

整个 Hero 动画发生在正在打开的页面上。组件是完全独立,不在页面之间共享任何状态的。


NavigationObserver

可以通过 NavigationObserver 观察压入和弹出路由的事件。

/// 一个管理 [Hero] 过渡的 [Navigator] observer。
///
/// 应该在 [Navigator.observers] 中使用 [HeroController] 的实例。
/// 这由 [MaterialApp] 自动完成。
class HeroController extends NavigatorObserver
复制代码

HeroController

Hero 使用这个类开始旅程。除了能够自己添加 NavigationObservers 之外,MaterialApp 默认添加了 HeroController看一下这里。

Hero 组件

  /// 创建一个 Hero////// [tag] 和 [child] 必须非空。const Hero({Key key,@required this.tag,this.createRectTween,@required this.child,}) : assert(tag != null),assert(child != null),super(key: key);
复制代码

Hero 的构造器

Hero 组件实际上并没有做太多。它拥有 child 和 tag。除此之外,createRectTween 参数决定了 Hero 在飞往目的地时所采用的路由。默认的实现是 MaterialRectArcTween。顾名思义,它将 Hero 沿弧线移动到最终位置。

Hero 的状态也负责捕获大小并用占位符替换自己。

_allHeroesFor

元素(具体组件)放在树中。通过访客,你可以沿着树下去并收集信息。

  // 返回上下文中所有 Hero 的 map,由 hero 标记索引。static Map<Object, _HeroState> _allHeroesFor(BuildContext context) {assert(context != null);final Map<Object, _HeroState> result = <Object, _HeroState>{};void visitor(Element element) {if (element.widget is Hero) {final StatefulElement hero = element;final Hero heroWidget = element.widget;final Object tag = heroWidget.tag;assert(tag != null);assert(() {if (result.containsKey(tag)) {throw new FlutterError('There are multiple heroes that share the same tag within a subtree.\n''Within each subtree for which heroes are to be animated (typically a PageRoute subtree), ''each Hero must have a unique non-null tag.\n''In this case, multiple heroes had the following tag: $tag\n''Here is the subtree for one of the offending heroes:\n''${element.toStringDeep(prefixLineOne: "# ")}');}return true;}());final _HeroState heroState = hero.state;result[tag] = heroState;}element.visitChildren(visitor);}context.visitChildElements(visitor);return result;}
复制代码

heroes.dart

在方法内部声明了一个名为 visitor 的内联函数。context.visitChildElements(visitor) 方法和 element.visitChildren(vistor) 直到访问完上下文的所有元素才调用函数。在每次访问时,它会检查这个 child 是否为 Hero,如果是,则将其保存到 map 中。

旅程的开始

  // 在 from 和 to 中找到匹配的 Hero 对,并启动新的 Hero 旅程,// 或转移现有的 Hero 旅程。void _startHeroTransition(PageRoute<dynamic> from, PageRoute<dynamic> to, _HeroFlightType flightType) {// 如果在调用帧尾回调之前删除了导航器或其中一个路由子树,// 那么接下来实际上不会开始转换。if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {to.offstage = false; // in case we set this in _maybeStartHeroTransitionreturn;}final Rect navigatorRect = _globalBoundingBoxFor(navigator.context);// 在这一点上,toHeroes 可能是第一次建造和布局。final Map<Object, _HeroState> fromHeroes = Hero._allHeroesFor(from.subtreeContext);final Map<Object, _HeroState> toHeroes = Hero._allHeroesFor(to.subtreeContext);// 如果 `to` 路由是在屏幕外的,// 那么我们暗中将其动画值恢复到它“移到”屏幕外之前的状态。to.offstage = false;for (Object tag in fromHeroes.keys) {if (toHeroes[tag] != null) {final _HeroFlightManifest manifest = new _HeroFlightManifest(type: flightType,overlay: navigator.overlay,navigatorRect: navigatorRect,fromRoute: from,toRoute: to,fromHero: fromHeroes[tag],toHero: toHeroes[tag],createRectTween: createRectTween,);if (_flights[tag] != null)_flights[tag].divert(manifest);else_flights[tag] = new _HeroFlight(_handleFlightEnded)..start(manifest);} else if (_flights[tag] != null) {_flights[tag].abort();}}}
复制代码

heroes.dart

这会响应路由压入/弹出事件而被调用。在第 14 行和第 15 行,你可以看到 _allHeroesFor 调用,它可以在两个页面上找到所有 Hero。从第 21 行开始构建 _HeroFlightManifest 并启动旅程。从这里开始,有一堆动画的代码设置和边缘情况的处理。我建议你看一下整个类,这很有意思,里面还有很多值得学习的东西。你也可以看一下这个。


Villains 是如何运行的

Villains 要比 Hero 更简单。

Hero 和 3 个 Villain 使用(AppBar,Text,FAB)。

他们使用相同的机制来查找给定上下文的所有 Villain,他们还使用 NavigationObserver 自动对页面转换做出反应。但不是从一个屏幕到另一个屏幕的动画,而是仅在它们各自的屏幕上做的动画。

SequenceAnimation 和 自定义 TickerProvider

处理动画时,通常使用 SingleTickerProviderStateMixinTickerProviderStateMixin。在这种情况下,动画不会在 StatefulWidget 中启动,因此我们需要另一种方法来访问 TickerProvider

class TransitionTickerProvider implements TickerProvider {final bool enabled;TransitionTickerProvider(this.enabled);@overrideTicker createTicker(TickerCallback onTick) {return new Ticker(onTick, debugLabel: 'created by $this')..muted = !this.enabled;}
}
复制代码

自定义一个 ticker 非常简单。所有这一切都是为了实现 TickerProvider 接口并返回一个新的 Ticker

  static Future playAllVillains(BuildContext context, {bool entrance = true}) {List<_VillainState> villains = VillainController._allVillainssFor(context)..removeWhere((villain) {if (entrance) {return !villain.widget.animateEntrance;} else {return !villain.widget.animateExit;}});// 用于新页面动画的控制器,因为它的时间比实际页面转换更长AnimationController controller = new AnimationController(vsync: TransitionTickerProvider(TickerMode.of(context)));SequenceAnimationBuilder builder = new SequenceAnimationBuilder();for (_VillainState villain in villains) {builder.addAnimatable(anim: Tween<double>(begin: 0.0, end: 1.0),from: villain.widget.villainAnimation.from,to: villain.widget.villainAnimation.to,tag: villain.hashCode,);}SequenceAnimation sequenceAnimation = builder.animate(controller);for (_VillainState villain in villains) {villain.startAnimation(sequenceAnimation[villain.hashCode]);}//开始动画return controller.forward().then((_) {controller.dispose();});}
复制代码

首先,所有不应该展示的 Villain(那些将 animateExit/animateEntrance 设置为 false 的人)都会被过滤掉。然后创建一个带有自定义 TickerProviderAnimationController。使用 SequenceAnimation 库,每个 Villain 被分配一个动画,它们在各自的时间中运行 0.0 —— 1.0(fromto 持续时间)。最后,动画全部开始。当它们全部完成时,控制器被丢弃。

Villains 的 build() 方法

  @overrideWidget build(BuildContext context) {Widget animatedWidget = widget.villainAnimation.animatedWidgetBuilder(widget.villainAnimation.animatable.chain(CurveTween(curve: widget.villainAnimation.curve)).animate(_animation), widget.child);if (widget.secondaryVillainAnimation != null) {animatedWidget = widget.secondaryVillainAnimation.animatedWidgetBuilder(widget.secondaryVillainAnimation.animatable.chain(CurveTween(curve: widget.secondaryVillainAnimation.curve)).animate(_animation), animatedWidget);}return animatedWidget;}
复制代码

这可能看起来很可怕,但请先忍耐一下。让我们看看第 3 行和第 4 行。widget.villainAnimation.animatedWidgetBuilder 是一个自定义的 typedef:

typedef Widget AnimatedWidgetBuilder(Animation animation, Widget child);
复制代码

它的工作是返回一个根据动画绘制的组件(大多数时候返回的组件是一个 AnimatedWidget)。

它得到了 Villain 的 child 和这个动画:

widget.villainAnimation.animatable.chain(CurveTween(curve: widget.villainAnimation.curve)).animate(_animation)
复制代码

链方法首先评估 CurveTween。然后它使用该值来评估调用它的 animatable。这只是将所需的曲线添加到动画中。

这是关于 Villain 如何工作的粗略概述,请务必也查看源代码并大胆地提出你们的问题。


可变的静态变量很槽糕,让我解释一下

深夜,我坐在我的办公桌前,写下测试。几个小时后,每一次单独的测试都过去了,似乎没有 bug。就在睡觉之前,我把所有的测试都放在一起,以确保它真的没问题。然后发生了这个:

每个测试都只能单独通过。

我很困惑。每次测试都成功。果然,当我自己运行这两个测试时,它们很正常。但是当一起运行所有测试时,最后两个失败了。WTF。

第一反应显然是:“我的代码肯定没错,它一定对测试的执行方式做了些什么!也许测试是并行播放因此相互干扰?也许是因为我使用了相同的键?”

Brian Egan 向我指出,删除一个特定的测试修复了错误并将其移到顶部使得其他所有测试也失败了。如果那不是“共享数据”那么我不知道是什么。

当我发现问题是什么时,我忍不住笑了。这正是在某些情况下使用静态变量不好的原因。

基本上,预定义的动画都是静态的。我懒得为每个动画编写一个方法来获取 VillainAnimation 所需的所有参数。所以我使 VillainAnimation 是可变的(坏主意)。这样我就没有必要在方法中明确写出所有必要的参数。使用时看起来像这样:

Villain(villainAnimation: VillainAnimation.fromBottom(0.4)..to = Duration(milliseconds: 150),child: Text("HI"),
)
复制代码

打破一切的测试应该在页面转换完成后开始测试 Villain 转换。它将动画的起点设置为 1 秒。因为它是在静态引用上设置它,之后的测试使用它作为默认值。测试失败,因为动画无法在 1 秒到 750 毫秒之间运行。

修复很简单(使一切都不可变并在方法中传递参数)但我仍然觉得这个小错误非常有趣。


总结

感谢 Villain 恢复了好坏之间的平衡。

关于 #fluttervillains 的意见和讨论是受欢迎的。如果你使用 Villain 一起制作很酷的动画,我很希望看到它。

我的 Twitter: @norbertkozsir

如果发现译文存在错误或其他需要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可获得相应奖励积分。文章开头的 本文永久链接 即为本文在 GitHub 上的 MarkDown 链接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。

转载于:https://juejin.im/post/5b8e75a46fb9a019f928e678

[译] Flutter 的 Heroes 和 Villains —— 为 Flutterverse 带来平衡相关推荐

  1. [译]Flutter 响应式编程:Steams 和 BLoC 实践范例

    原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是作者 Didier Boelens 为 Reactive Program ...

  2. [译]Flutter缓存管理库flutter_cache_manager

    本文翻译自pub: flutter_cache_manager | Flutter Package (flutter-io.cn) 译时版本: flutter_cache_manager 3.3.0 ...

  3. [译] Flutter 从 0 到 1, 第二部分

    原文地址:Zero to One with Flutter, Part Two 原文作者:Mikkel Ravn 译文出自:掘金翻译计划 本文永久链接:github.com/xitu/gold-m- ...

  4. [译]Flutter持久化库drift(原moor)查看组件moor_db_viewer

    本文件翻译自 moor_db_viewer | Flutter Package (pub.dev) 肉翻多有不足~ 不吝赐教 看示例很 NB,文档稍显单薄. moor_db_viewer 该包允许我们 ...

  5. 从架构到源码:一文了解Flutter渲染机制

    简介:Flutter从本质上来讲还是一个UI框架,它解决的是一套代码在多端渲染的问题.在渲染管线的设计上更加精简,加上自建渲染引擎,相比ReactNative.Weex以及WebView等方案,具有更 ...

  6. Flutter 1.17 | 2020 首个稳定版发布!

    作者 / Chris Sells, Product Manager, Flutter developer experience 很高兴为大家带来 Flutter 1.17,这也是我们 2020 年的第 ...

  7. Google 要用 Flutter 一统移动、桌面开发江湖?

    "Flutter 的核心是一个独立的可执行二进制文件,所以它不仅能改变移动开发的世界,也能改变桌面开发的世界.你只需编写一次代码,就可以在 Android.iOS.Windows.Mac 和 ...

  8. 让阿里告诉你, iOS开发者为什么要学 Flutter !

    2019 年无疑是 Flutter 技术如火如荼发展的一年.每一个移动开发者都在为 Flutter 带来的"快速开发.富有表现力和灵活的 UI.原生性能"的特色和理念而痴狂,从超级 ...

  9. I/O Extended Android/Flutter 专场活动即将开始!

    今年 I/O 带来了丰富的 Google 技术和产品更新,在备受开发者关注的 Android,Flutter 和 Web 等技术领域,更是迎来了诸多进展. 世界上仅有为数不多的几个平台可以帮助开发者们 ...

最新文章

  1. 建校仅11年就入选“双一流” ,这所高校是凭什么做到的?
  2. SVN使用_获取某版本后改动的文件列表
  3. 年度总结和计划:去年4个1,今年5个1
  4. 杀毒软件全免费遭厂家“抵制”
  5. Python并发之协程gevent基础(5)
  6. windows下utf-8和unicode的相互转换
  7. ubuntu 12.04 eclipse 安装
  8. Android高手的六大境界
  9. iPhone又降价了!京东、苏宁安排上了 iPhone XS系列最高直降1700元
  10. webkit内核浏览器的CSS写法
  11. 题解 P3367 【【模板】并查集】
  12. js排序的时间复杂度_javascript的array.indexOf的时间复杂度是多少?
  13. ubuntu/debian-bluster 用python安装 sasl 报错解决
  14. android MVP架构分享
  15. 阿里云账号实名认证解决方案
  16. Kettle: 合并记录
  17. Linux下安装java11(亲测)
  18. 解题:BZOJ 4808 马
  19. 根据身份证号码计算生日/年龄/性别
  20. 联盛德W806最小系统开发板第一次上手准备工作

热门文章

  1. 怎么在电脑上用手机模拟器+618IP代理换不同的IP多开游戏
  2. electron安装教程
  3. C++实现的Socket接口实现自定义协议通信
  4. c语言中英文翻译 毕业设计,c语言中英文翻译资料 毕设论文.doc
  5. python雷达图详解_Python成绩单雷达图
  6. 工作系列Java开发之利用Java实现ERP系统中Excel表格的导出
  7. 玩转LinkedIn领英,我的外贸开发客户利器
  8. 后台管理系统权限管理详解
  9. 《UnityAPI.Vector2二维向量》(Yanlz+Unity+SteamVR+云技术+5G+AI+VR云游戏+Vector2+Normalized+Lerp+Dot+立钻哥哥++OK++)
  10. UIApp教程(全网最详细的教程来啦)