博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
谈一谈Flutter中的共享元素动画Hero
阅读量:7104 次
发布时间:2019-06-28

本文共 8798 字,大约阅读时间需要 29 分钟。

  如果你是一名安卓开发者,应该很熟悉 **共享元素变换(Shared Element Transition)**这个概念,它可以通过几行代码,就在两个Activity或者Fragment之间做出流畅的转场动画。

  Google把这个概念也带到了Flutter里面,这就是我们今天要讲的主角——Hero控件。通过Hero,我们可以在两个路由之间,做出流畅的转场动画。注意,是两个路由(Route),在Flutter里面,Dialog也是路由,因此完全可以使用在Dialog的切换上。
  我们看下效果图:

Hero的使用

  我们现在有两个元素:源控件和目标控件。要实现元素共享,首先,我们要将两个控件分别用Hero包裹,同时为它们设置相同的tag。

  源路由中的Hero:

Hero(        tag: 'hero',        child: Container(          color: Colors.lightGreen,          width: 50.0,          height: 50.0,        ));复制代码

  目标路由中的Hero:

Hero(        tag: 'hero',        child: Container(          color: Colors.orange,          width: 150.0,          height: 120.0,        ));复制代码

  接着,给源路由页面添加路由跳转逻辑:

GestureDetector(      child: Hero(          tag: 'hero',          child: Container(             color: Colors.orange,             width: 150.0,             height: 120.0,          )),      onTap: () {        Navigator.of(context).push(MaterialPageRoute(builder: (_) {          return ElementDetailPage();        }));      },    );复制代码

  就是这么简单,只需两步,你就可以完成这个Hero过度动画了,是不是超级方便呢?

Hero变换时做了什么?

  Hero就是一个动画,所以我们将其拆分成三部分来说:动画开始时、动画进行中和动画结束时。

动画开始时:t=0.0

在这个时间点,Flutter做了三件事:

  • 计算目标Hero的位置,然后算出对应的Rect;
  • 把源Hero复制一份,绘制到Overlay上(就是绘制一个与源Hero大小、位置完全相同的Hero,作为目标Hero),然后改变它的Z轴属性,让它能显示在所有路由之上;
  • 把源Hero移出屏幕。

动画进行时

动画的进行是依靠了 来实现的,这个东西在写动画时总是会用到,大家应该不陌生;通过Hero的
createRectTween属性,将这个变换Tween传给Hero,Hero内部进行移动动画的操作。默认情况下,使用的变换是
MaterialRectArcTween,注意,这个
默认的变换路径是一条曲线

动画结束时:t=1.0

当移动结束时:

  • Flutter将Overlay中的Hero移除,现在Overlay中就是空白的了;
  • 目标Hero出现在目标路由的最终位置;
  • 源Hero在源路由中被恢复。

此处划重点!!

  源Hero与目标Hero大小应一致,否则会出现溢出(overflow)!! overflow这个警告我们应该不陌生了,Flutter中必须随时遵循布局原则,一不小心就会给你送上overflow大礼包。

createRectTween是个什么东西

  我们通过自定义createRectTween,可以改变转换动画。下面是一个很简单的设置createRectTween属性的例子:

createRectTween: (Rect begin, Rect end) {              return RectTween(                begin: Rect.fromLTRB(                    begin.left, begin.top, begin.right, begin.bottom),                end: Rect.fromLTRB(end.left, end.top, end.right, end.bottom),              );            }复制代码

  至于如何自定义createRectTween,可以看一下默认的MaterialRectArcTween的实现,主要是重写下面三个方法:

@override    set begin(Rect value) { }  @override    set end(Rect value) { }  @override    Rect lerp(double t) { }复制代码

  自定义一个RectTween很复杂,这里不展开讲了。

  这里要注意一个坑:createRectTween属性会优先选用目标Hero中的配置。

Tween
_doCreateRectTween(Rect begin, Rect end) { final CreateRectTween createRectTween = manifest.toHero.widget.createRectTween ?? manifest.createRectTween; if (createRectTween != null) return createRectTween(begin, end); return RectTween(begin: begin, end: end); }复制代码

  Hero的默认变换为MaterialRectArcTween

  所以,如果你想要push、pop都遵循自定义的RectTween,请给fromHero和toHero都设置createRectTween属性。如果只设置fromHero的createRectTween属性,则push时执行自定义createRectTween,pop时执行默认的MaterialRectArcTween。

Hero的实现

  Hero中所有的变换,都是通过HeroController来实现的。但是,打开Hero类的源码,你会发现,这个Hero控件内部什么事情也没有做,也没有没有绑定HeroController,只是纯粹地在build方法中创建了一个普通的widget。

  但是,思考一下,Hero是一个与路由相关的动画控件,它并不是一个简单的Widget,能管理路由切换动画。这么看来,Hero似乎应该属于一个App级别的全局控件(准确地说应该是HeroController)。不知道Flutter团队是不是这么想的,实际上,HeroController确实是在App级别就被初始化,并且和NavigatorObserver绑定了。这样,每次Navigator进行push/pop操作时,HeroController都会收到通知。
  我们可以打开MaterialApp的源码:

@override  void initState() {    super.initState();    _heroController = HeroController(createRectTween: _createRectTween);    _updateNavigator();  }RectTween _createRectTween(Rect begin, Rect end) {    return MaterialRectArcTween(begin: begin, end: end);  }void _updateNavigator() {    if (widget.home != null ||        widget.routes.isNotEmpty ||        widget.onGenerateRoute != null ||        widget.onUnknownRoute != null) {      _navigatorObservers = List
.from(widget.navigatorObservers) ..add(_heroController); } else { _navigatorObservers = null; } }复制代码

  在MaterialApp初始化状态的时候,就初始化好了_heroController,并且在_updateNavigator()方法中将其与_navigatorObservers绑定。_createRectTween返回的是一个MaterialRectArcTween,这解释了之前提到的一个知识点:默认的Hero动画的Rect是一个MaterialRectArcTween。

  那么新的疑问又来了,我们现在有了_heroController,这个_heroController是怎么和我们布局中的Hero控件联系起来的呢?
  我们来看HeroController的源码:

@override  void didPush(Route
route, Route
previousRoute) { ······ _maybeStartHeroTransition(previousRoute, route, HeroFlightDirection.push); } @override void didPop(Route
route, Route
previousRoute) { ······ _maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop); }复制代码

  在页面push和pop的时候,都调用了同一个方法_maybeStartHeroTransition()

void _maybeStartHeroTransition(Route
fromRoute, Route
toRoute, , HeroFlightDirection flightType) { ······ WidgetsBinding.instance.addPostFrameCallback((Duration value) { _startHeroTransition(from, to, animation, flightType); }); } }复制代码

  这里的WidgetsBinding的作用,就是将源路由与目标路由,和_heroController关联起来。WidgetsBinding.instance.addPostFrameCallback这个监听,会在当前帧绘制完成后触发。

void _startHeroTransition(    PageRoute
from, PageRoute
to, Animation
animation, HeroFlightDirection flightType, ) { // If the navigator or one of the routes subtrees was removed before this // end-of-frame callback was called, then don't actually start a transition. 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); // At this point the toHeroes may have been built and laid out for the first time. final Map
fromHeroes = Hero._allHeroesFor(from.subtreeContext); final Map
toHeroes = Hero._allHeroesFor(to.subtreeContext); // If the `to` route was offstage, then we're implicitly restoring its // animation value back to what it was before it was "moved" offstage. to.offstage = false; for (Object tag in fromHeroes.keys) { if (toHeroes[tag] != null) { final HeroFlightShuttleBuilder fromShuttleBuilder = fromHeroes[tag].widget.flightShuttleBuilder; final HeroFlightShuttleBuilder toShuttleBuilder = toHeroes[tag].widget.flightShuttleBuilder; final _HeroFlightManifest manifest = _HeroFlightManifest( type: flightType, overlay: navigator.overlay, navigatorRect: navigatorRect, fromRoute: from, toRoute: to, fromHero: fromHeroes[tag], toHero: toHeroes[tag], createRectTween: createRectTween, shuttleBuilder: toShuttleBuilder ?? fromShuttleBuilder ?? _defaultHeroFlightShuttleBuilder, ); if (_flights[tag] != null) _flights[tag].divert(manifest); else _flights[tag] = _HeroFlight(_handleFlightEnded)..start(manifest); } else if (_flights[tag] != null) { _flights[tag].abort(); } } }复制代码

   _startHeroTransition()的内容比较多,而且都很重要,我就直接全部贴上来了。首先,通过_allHeroesFor()找到源路由和目标路由页面中所有的Hero控件,然后对比Tag,如果找到了tag一致的Hero,那么就构建一份_HeroFlightManifest,这个清单里面包括了页面变换所需要的各种属性。最后,调用_flights[tag] = _HeroFlight(_handleFlightEnded)..start(manifest);函数,开始变换。至于变换的具体动画实现,这里就不多说了,主要是通过start()函数开启动画,更新Hero的位置:

void start(_HeroFlightManifest initialManifest) {    ······    if (manifest.type == HeroFlightDirection.pop)      _proxyAnimation.parent = ReverseAnimation(manifest.animation);    else      _proxyAnimation.parent = manifest.animation;    manifest.fromHero.startFlight();    manifest.toHero.startFlight();    heroRectTween = _doCreateRectTween(      _globalBoundingBoxFor(manifest.fromHero.context),      _globalBoundingBoxFor(manifest.toHero.context),    );    overlayEntry = OverlayEntry(builder: _buildOverlay);    manifest.overlay.insert(overlayEntry);  }复制代码

   结束动画时,我们可以看到,overlayEntry中的控件被remove掉了。

void _handleAnimationUpdate(AnimationStatus status) {    if (status == AnimationStatus.completed || status == AnimationStatus.dismissed) {      _proxyAnimation.parent = null;      assert(overlayEntry != null);      overlayEntry.remove();      overlayEntry = null;      manifest.fromHero.endFlight();      manifest.toHero.endFlight();      onFlightEnded(this);    }  }复制代码

   当目标路由被pop的时候又会发生什么呢?因为pop的时候,也是执行的_startHeroTransition()方法,跟push的时候是一样的,只不过执行的动画是反着的,就不多说了:

void _startHeroTransition(    PageRoute
from, PageRoute
to, Animation
animation, HeroFlightDirection flightType, ) { ······ _flights[tag] = _HeroFlight(_handleFlightEnded)..start(manifest); ·······}void start(_HeroFlightManifest initialManifest) { ······ if (manifest.type == HeroFlightDirection.pop) _proxyAnimation.parent = ReverseAnimation(manifest.animation); ······}复制代码

小练习

   在Dribble上找到了这个设计图,我觉得用来联系Hero转换再适合不过了,大家可以按照这个设计来练练手。

具体设计稿请看:
参考Demo:

转载地址:http://bwuhl.baihongyu.com/

你可能感兴趣的文章
11.22 访问日志不记录静态文件 11.23 访问日志切割 11.24 静态元素过期时间
查看>>
文档查看cat_more_less_head_tail
查看>>
jdk8重新认识hashmap
查看>>
Spring Cloud Alibaba迁移指南(二):零代码替换 Eureka
查看>>
Visual Paradigm 教程[UML]:如何绘制封装图?(下)
查看>>
初探AngularJS6.x---目录结构说明
查看>>
kafka解决了什么问题?
查看>>
android流式布局、待办事项应用、贝塞尔曲线、MVP+Rxjava+Retrofit、艺术图片应用等源码...
查看>>
ppwjs之bootstrap文字排版:<pre>元素 [scroll](预格式元素 [带滚动条)
查看>>
Spring经典的面试题,你值得拥有!
查看>>
Ember.js 属性值模糊查询
查看>>
squid配置
查看>>
OSChina 周三乱弹 —— 生活要懂得苦中作乐
查看>>
前端那些事之react--redux篇
查看>>
Ubuntu 16.04 U盘安装过程
查看>>
UIApplication、AppDelegate、委托
查看>>
hadoop单机安装
查看>>
Android实用笔记——使用GridView以表格的形式显示多张图片
查看>>
内部类使用外部类的成员属性
查看>>
基于const的重载
查看>>