如果你是一名安卓开发者,应该很熟悉 共享元素变换(Shared Element Transition)这个概念,它可以通过几行代码,就在两个Activity或者Fragment之间做出流畅的转场动画。
  Google把这个概念也带到了Flutter里面,这就是我们今天要讲的主角——Hero控件。通过Hero,我们可以在两个路由之间,做出流畅的转场动画。注意,是两个路由(Route),在Flutter里面,Dialog也是路由,因此完全可以使用在Dialog的切换上。
  我们看下效果图:

shared_element.gif

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移出屏幕。

动画进行时

动画的进行是依靠了 Tween<Rect> 来实现的,这个东西在写动画时总是会用到,大家应该不陌生;通过Hero的createRectTween属性,将这个变换Tween<Rect>传给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的实现,主要是重写下面三个方法:

  @overrideset begin(Rect value) { }@overrideset end(Rect value) { }@overrideRect lerp(double t) { }

  自定义一个RectTween很复杂,这里不展开讲了。
  这里要注意一个坑:createRectTween属性会优先选用目标Hero中的配置。

Tween<Rect> _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的源码:

@overridevoid 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<NavigatorObserver>.from(widget.navigatorObservers)..add(_heroController);} else {_navigatorObservers = null;}}

  在MaterialApp初始化状态的时候,就初始化好了_heroController,并且在_updateNavigator()方法中将其与_navigatorObservers绑定。_createRectTween返回的是一个MaterialRectArcTween,这解释了之前提到的一个知识点:默认的Hero动画的Rect是一个MaterialRectArcTween。
  那么新的疑问又来了,我们现在有了_heroController,这个_heroController是怎么和我们布局中的Hero控件联系起来的呢?
  我们来看HeroController的源码:

@overridevoid didPush(Route<dynamic> route, Route<dynamic> previousRoute) {······_maybeStartHeroTransition(previousRoute, route, HeroFlightDirection.push);}@overridevoid didPop(Route<dynamic> route, Route<dynamic> previousRoute) {······_maybeStartHeroTransition(route, previousRoute, HeroFlightDirection.pop);}

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

void _maybeStartHeroTransition(Route<dynamic> fromRoute, Route<dynamic> toRoute, , HeroFlightDirection flightType) {······WidgetsBinding.instance.addPostFrameCallback((Duration value) {_startHeroTransition(from, to, animation, flightType);});}}

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

  void _startHeroTransition(PageRoute<dynamic> from,PageRoute<dynamic> to,Animation<double> 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 _maybeStartHeroTransitionreturn;}final Rect navigatorRect = _globalBoundingBoxFor(navigator.context);// At this point the toHeroes may have been built and laid out for the first time.final Map<Object, _HeroState> fromHeroes = Hero._allHeroesFor(from.subtreeContext);final Map<Object, _HeroState> 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<dynamic> from,PageRoute<dynamic> to,Animation<double> animation,HeroFlightDirection flightType,) {······_flights[tag] = _HeroFlight(_handleFlightEnded)..start(manifest);·······
}void start(_HeroFlightManifest initialManifest) {······if (manifest.type == HeroFlightDirection.pop)_proxyAnimation.parent = ReverseAnimation(manifest.animation);······
}

小练习

dribbble_shopper.gif

   在Dribble上找到了这个设计图,我觉得用来联系Hero转换再适合不过了,大家可以按照这个设计来练练手。
具体设计稿请看:https://dribbble.com/shots/5409882-Shop-Drop-shopping-app/attachments/1172372
参考Demo:https://gitee.com/yumi0629/FlutterUI/tree/master/lib/sharedelement

谈一谈Flutter中的共享元素动画Hero相关推荐

  1. android 延时播放动画,HollyTransition: 让APP丝滑般流畅:深入解读Android过渡动画Transition. 共享元素动画、场景动画、过场动画、延时动画...

    HollyTransition 零.导读 深入解读Android过渡动画Transition: 页面切换动画(过场动画) 共享元素动画 延时动画 场景动画 一.Transition前世今生 为了支持各 ...

  2. android共享元素动画_Android共享元素过渡动画

    android共享元素动画 In this tutorial we'll implement a different kind of animation transition namely Share ...

  3. 微信小程序第五篇:页面弹出效果及共享元素动画

    系列文章传送门: 微信小程序第一篇:自定义组件详解 微信小程序第二篇:七种主流通信方法详解 微信小程序第三篇:获取页面节点信息 微信小程序第四篇:生成图片并保存到手机相册 目录 一.page-caon ...

  4. Android 虚拟按键隐藏或显示之后共享元素动画异常解决方案

    背景 本篇算是共享元素的第三篇文章.主要还是因为第一篇才会衍生出来了第二篇和第三篇文章,后两篇均属于bug的分析和解决. 1.Android 仿微信朋友圈图片拖拽返回 2.Android 共享元素动画 ...

  5. Android 共享元素动画

    Andriod 5.0及之后,开始支持共享元素动画,该动画主要用于两个activity之间,两个Activity可以共享某些控件,Activity A跳转到Activity B的时候,A的某个控件能自 ...

  6. android共享元素动画兼容低版本

    文章目录 共享元素动画 5.0以上系统方案 详细代码流程 5.0以下兼容方案 简述流程 详细代码流程 上篇: activity转场动画 一款APP让人赏心悦目的APP,动画是它并不可少的部分,Andr ...

  7. 【译】Flutter中的花式背景动画

    原文链接:https://medium.com/@felixblaschke/fancy-background-animations-in-flutter-4163d50f5c37 本文主要介绍如何使 ...

  8. Flutter入门:Hero共享元素

    前言 在Android中可以设置共享元素,这样两个页面相同的元素在转场时就会有一个过渡动画,效果炫酷,用户体验也更好. 那么在Flutter中有同样的功能么? 答案是一定的,在Flutter它就是He ...

  9. 深入理解Android L新特性之 页面内容amp;共享元素过渡动画

    今天我们来聊聊Android L(5.0)引入的新特性:页面内容过渡动画和页面共享动画,这两个特性都是基于我们前面已经说过的Transition动画,如果你对Transition动画不太属性,请先看我 ...

最新文章

  1. 集合嵌套存储和遍历元素的示例
  2. 【Android 逆向】Android 进程注入工具开发 ( 远程进程注入动态库文件操作 | 注入动态库 加载 业务动态库 | 业务动态库启动 | pthread_create 线程开发 )
  3. 1.10 卷积神经网络示例-深度学习第四课《卷积神经网络》-Stanford吴恩达教授
  4. rust(51)-rust工具,prelude
  5. Python——蟒蛇绘制
  6. 他曾经负债2.5亿,如今身价超过500亿
  7. ife 零基础学院 day 2
  8. python定义对象的比较方法
  9. ubuntu中vscode配置python_ubuntu下vs code的python虚拟环境的配置
  10. 卷积神经网络之AlexNet
  11. 【codevs1228】苹果树,哦
  12. 基于LDAP的WebLogic虚拟化统一用户权限管理
  13. 判断输入几位数的正则_判断是几位数,并逆向输出此数的程序算法和说明
  14. 1.UiDevice API 详细介绍
  15. 教你编写高性能的mysql语法
  16. caffe matlab 崩溃,终于搞定caffe了(window官方版win7+VS2013)
  17. 【笔记】TAOCP Vol4 - Combination
  18. 服务器网卡相关知识点
  19. 刚性捆绑,无线运营新模式
  20. Fedora13 添加 网易镜像源

热门文章

  1. 22年的梦想《仙剑奇侠传》
  2. 谷歌浏览器不能使用opener属性的问题和解决
  3. 30系显卡怎么配置rangenet++和SuMa++
  4. 40st-m00330 型伺服电机STM32单片机PWM脉冲控制
  5. linux操作系统c语言编程,Linux操作系统下C语言编程从零开始
  6. python与c语言混合编程_python和C语言混编的几种方式
  7. linux ps -ef命令输出详解,Linux-ps命令学习
  8. 【Java】将输入的 24 小时制时间转换为 12 小时制时间
  9. 嵌入式C设计模式---状态机设计模式
  10. AD---通过组策略推送软件给客户端