[译] Flutter 的 Heroes 和 Villains —— 为 Flutterverse 带来平衡

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: KaTeX parse error: Undefined control sequence: \n at position 4: tag\̲n̲' 'Here is the …{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 from, PageRoute to, _HeroFlightType flightType) {
// 如果在调用帧尾回调之前删除了导航器或其中一个路由子树,
// 那么接下来实际上不会开始转换。
if (navigator == null || from.subtreeContext == null || to.subtreeContext == null) {
to.offstage = false; // in case we set this in _maybeStartHeroTransition
return;
}

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

@override
Ticker 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(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() 方法

@override
Widget 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)

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

【Android高级架构视频学习资源】

Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
点击传送门,即可获取!

技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值