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;
}
在方法内部声明了一个名为 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();
}
}
}
这会响应路由压入/弹出事件而被调用。在第 14 行和第 15 行,你可以看到 _allHeroesFor
调用,它可以在两个页面上找到所有 Hero。从第 21 行开始构建 _HeroFlightManifest
并启动旅程。从这里开始,有一堆动画的代码设置和边缘情况的处理。我建议你看一下整个类,这很有意思,里面还有很多值得学习的东西。你也可以看一下这个。
Villains 是如何运行的
Villains 要比 Hero 更简单。
Hero 和 3 个 Villain 使用(AppBar,Text,FAB)。
他们使用相同的机制来查找给定上下文的所有 Villain,他们还使用 NavigationObserver
自动对页面转换做出反应。但不是从一个屏幕到另一个屏幕的动画,而是仅在它们各自的屏幕上做的动画。
SequenceAnimation 和 自定义 TickerProvider
处理动画时,通常使用 SingleTickerProviderStateMixin
或 TickerProviderStateMixin
。在这种情况下,动画不会在 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 的人)都会被过滤掉。然后创建一个带有自定义 TickerProvider
的 AnimationController
。使用 SequenceAnimation 库,每个 Villain
被分配一个动画,它们在各自的时间中运行 0.0 —— 1.0(from
和 to
持续时间)。最后,动画全部开始。当它们全部完成时,控制器被丢弃。
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)
最后
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。
因此我收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
提升的进阶课程,基本涵盖了95%以上Android开发知识点!不论你是刚入门Android开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门**
如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!