zoukankan      html  css  js  c++  java
  • 给 Flutter 界面切换来点特效

    本文微信公众号「AndroidTraveler」首发。

    背景

    我们知道页面之间如果直接切换,会比较生硬,还会让用户觉得很突兀,用户体验不是很好。

    因此一般情况下,页面之间的切换为了达到平滑过渡,都会添加动画。

    另外,有时候我们不喜欢系统的默认动画,希望能够自定义动画。

    基于此,本篇主要讲述如何给 Flutter 的页面切换增加自定义动画。

    默认效果

    首先我们看看默认效果是怎样的?

    看起来似乎还不错。代码如下:

    import 'package:flutter/material.dart';
    
    void main() => runApp(MaterialApp(
          home: MyApp(),
        ));
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return _getCenterWidget(RaisedButton(
            child: Text('Go to next page->'),
            onPressed: () {
              Navigator.of(context).push(_createRoute());
            }));
      }
    }
    
    Route _createRoute() {
      return MaterialPageRoute(builder: (BuildContext context) => Page2());
    }
    
    class Page2 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return _getCenterWidget(Text('Page2'));
      }
    }
    
    Widget _getCenterWidget(Widget child) {
      return Scaffold(
        appBar: AppBar(),
        body: Center(
          child: child,
        ),
      );
    }
    

    可以看到创建了两个页面 MyApp 和 Page2。

    第一个页面 MyApp 有一个按钮,第二个页面 Page2 有一个文本。

    关键的切换就在 _createRoute() 这个路由创建方法里面。

    我们点进去 MaterialPageRoute 源码,可以看到

      @override
      Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
        final PageTransitionsTheme theme = Theme.of(context).pageTransitionsTheme;
        return theme.buildTransitions<T>(this, context, animation, secondaryAnimation, child);
      }
    

    加上一开始的注释,可以知道这个就是默认的界面切换过渡效果。

    /// See also:
    ///
    ///  * [PageTransitionsTheme], which defines the default page transitions used
    ///    by [MaterialPageRoute.buildTransitions].
    

    另外这里可以看到默认的动画时长为 300ms,而且我们不能自定义。

      @override
      Duration get transitionDuration => const Duration(milliseconds: 300);
    

    接下来我们就说说如何自定义我们的界面切换过渡效果,并且自定义动画时长。

    自定义动画

    1. 设置 PageRouteBuilder

    由上面的分析我们知道最关键的地方在创建路由方法 _createRoute() 中。

    因此我们首先修改一下,不使用默认的 MaterialPageRoute,我们使用 PageRouteBuilder

    Route _createRoute() {
      return PageRouteBuilder(
          pageBuilder: (context, animation, secondaryAnimation) => Page2(),
          transitionsBuilder:(context, animation, secondaryAnimation, child) {
            return child;
          }
      );
    }
    

    可以看到我们通过 pageBuilder 指定路由页面,通过 transitionsBuilder 指定页面过渡效果。

    另外说明一下,这里的参数大家不用死记硬背,我们点进去源码一看就知道了,如下:

    /// Signature for the function that builds a route's primary contents.
    /// Used in [PageRouteBuilder] and [showGeneralDialog].
    ///
    /// See [ModalRoute.buildPage] for complete definition of the parameters.
    typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
    
    /// Signature for the function that builds a route's transitions.
    /// Used in [PageRouteBuilder] and [showGeneralDialog].
    ///
    /// See [ModalRoute.buildTransitions] for complete definition of the parameters.
    typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
    

    如果我们运行代码,由于直接返回 child,所以应该是没有动画效果的。我们运行之后,效果如下:

    2. 添加 Tween 和 SlideTransition

    默认的过渡效果是从右边往左过来,我们这里自定义的演示效果就从下面往上过渡好了。

    需要了解一下的是 Tween 是一个介于开始和结束值的线性插值器。

    另外我们跟进上面的 transitionsBuilder 可以知道他的第一个 animation 参数取值为 0.0 到 1.0。

    我们这边是从下往上,所以 y 轴的偏移就是由 1.0 到 0.0,表示竖直方向距离顶部一整个页面到不存在偏移(已经在顶部)。

    因此关于 Tweenanimation 我们可以得到:

    var begin = Offset(0.0, 1.0);
    var end = Offset(0.0, 0.0);
    var tween = Tween(begin: begin, end: end);
    var offsetAnimation = animation.drive(tween);
    

    因为我们是要实现滑动,因此将这个确定好的偏移动画通过 SlideTransition 处理并返回,可以得到修改后的路由代码如下:

    Route _createRoute() {
      return PageRouteBuilder(
        transitionDuration: Duration(seconds: 5),
          pageBuilder: (context, animation, secondaryAnimation) => Page2(),
          transitionsBuilder:(context, animation, secondaryAnimation, child) {
            var begin = Offset(0.0, 1.0);
            var end = Offset(0.0, 0.0);
            var tween = Tween(begin: begin, end: end);
            var offsetAnimation = animation.drive(tween);
    
            return SlideTransition(
              position: offsetAnimation,
              child: child,
            );
          }
      );
    }
    

    效果如下:

    看到上面效果,可能有小伙伴会有疑问。

    问题一:你打开页面是从下到上我可以理解,但是返回为什么是反过来的从上到下呢?

    我们跟进去 transitionsBuilder 的源码,可以看到

      /// Used to build the route's transitions.
      ///
      /// See [ModalRoute.buildTransitions] for complete definition of the parameters.
      final RouteTransitionsBuilder transitionsBuilder;
    

    我们跟进去(通过点击)注释里面的 buildTransitions,可以看到 animation 的说明如下:

      ///  * [animation]: When the [Navigator] pushes a route on the top of its stack,
      ///    the new route's primary [animation] runs from 0.0 to 1.0. When the [Navigator]
      ///    pops the topmost route this animation runs from 1.0 to 0.0.
    

    可以看到入栈和出栈的动画效果是相反的,而这个也符合我们的认知。

    问题二:现在的效果是从下到上,如果我要实现从上到下,是不是将 begin 和 end 的 Offset 交换一下就可以?

    其实如果你理解我上面说的这句话

    我们这边是从下往上,所以 y 轴的偏移就是由 1.0 到 0.0,表示竖直方向距离顶部一整个页面到不存在偏移(已经在顶部)。

    你就会知道,改成从 0.0 到 1.0 会是什么情况。

    我们改一下,通过实际演示效果来说明。

    修改后的代码如下:

    Route _createRoute() {
      return PageRouteBuilder(
          pageBuilder: (context, animation, secondaryAnimation) => Page2(),
          transitionsBuilder:(context, animation, secondaryAnimation, child) {
            var begin = Offset(0.0, 0.0);
            var end = Offset(0.0, 1.0);
            var tween = Tween(begin: begin, end: end);
            var offsetAnimation = animation.drive(tween);
    
            return SlideTransition(
              position: offsetAnimation,
              child: child,
            );
          }
      );
    }
    

    仅仅是 begin 和 end 的 Offset 做了交换。

    运行效果如下:

    虽然能够看出一点端倪,但是我们前面讲过,默认动画时长是 300 ms,所以比较快,这样不好分析。

    我们可以通过 PageRouteBuildertransitionDuration 属性来设置动画的时长。

    我们设置 3s 来看下效果,代码如下:

    Route _createRoute() {
      return PageRouteBuilder(
          transitionDuration: Duration(seconds: 3),
          pageBuilder: (context, animation, secondaryAnimation) => Page2(),
          transitionsBuilder:(context, animation, secondaryAnimation, child) {
            var begin = Offset(0.0, 0.0);
            var end = Offset(0.0, 1.0);
            var tween = Tween(begin: begin, end: end);
            var offsetAnimation = animation.drive(tween);
    
            return SlideTransition(
              position: offsetAnimation,
              child: child,
            );
          }
      );
    }
    

    运行效果如下:

    看到了吧,确实是反过来了,从一开始距离顶部为 0.0,到距离顶部 1.0(100%)。

    那么如果我想实现从上到下进入怎么办呢?

    我们给一张图,相信看完你就懂了。

    从这张图我们知道,如果我们从下往上,y 应该从 1.0 变到 0.0。如果要从上往下,y 应该从 -1.0 变到 0.0。

    所以我们修改代码,如下:

    Route _createRoute() {
      return PageRouteBuilder(
          transitionDuration: Duration(seconds: 3),
          pageBuilder: (context, animation, secondaryAnimation) => Page2(),
          transitionsBuilder:(context, animation, secondaryAnimation, child) {
            var begin = Offset(0.0, -1.0);
            var end = Offset(0.0, 0.0);
            var tween = Tween(begin: begin, end: end);
            var offsetAnimation = animation.drive(tween);
    
            return SlideTransition(
              position: offsetAnimation,
              child: child,
            );
          }
      );
    }
    

    运行效果为:

    3. 通过 CurveTween 来点加速度

    当我们将动画时长设置为 3s 之后,我们可以明显的看到我们的动画速度似乎是匀速的。

    那么如果我想修改下动画的速度,比如进来快,结束慢,可不可以呢?

    答案是肯定的。

    我们通过 CurveTween 可以来实现这个需求。

    使用的重点在于 curve 的选择,所以我们要选择哪种 curve 呢?

    其实 Flutter 我比较喜欢的一个点就是代码注释详细,并且还有 demo 演示。

    我们进入 Curves 源码,以 Curves.ease 为例:

      /// A cubic animation curve that speeds up quickly and ends slowly.
      ///
      /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease.mp4}
      static const Cubic ease = Cubic(0.25, 0.1, 0.25, 1.0);
    

    注释说了启动快,结束慢,而且还有一个 mp4 链接,点击可以看到如下效果:

    我们可以看出它的变化趋势,通过斜率可以看出前期快,后期慢,而且右边还有四种动画的效果预览。

    我们设置 CurveTween 代码如下:

    var curveTween = CurveTween(curve: Curves.ease);
    

    可以看到很简单,选择一种你想要的变化趋势即可。

    4. 组合 Tween 和 CurveTween

    这个也比较简单,通过 Tween 自带的 chain 方法即可,如下:

    var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: Curves.ease));
    

    另外一般 Offset(0.0, 0.0) 可以直接写为 Offset.zero。

    修改后代码为:

    Route _createRoute() {
      return PageRouteBuilder(
          transitionDuration: Duration(seconds: 3),
          pageBuilder: (context, animation, secondaryAnimation) => Page2(),
          transitionsBuilder:(context, animation, secondaryAnimation, child) {
            var begin = Offset(0.0, 1.0);
            var end = Offset.zero;
            var tween = Tween(begin: begin, end: end).chain(CurveTween(curve: Curves.ease));
    
            return SlideTransition(
              position: animation.drive(tween),
              child: child,
            );
          }
      );
    }
    

    运行效果如下:

    5. 完整例子

    有了上面的基础,我们就可以将四个方向的动画效果都加上,当然我们这边就不延时了。另外为了演示方便,就直接打开后 delay 1s 返回。

    代码如下:

    import 'package:flutter/material.dart';
    
    void main() => runApp(MaterialApp(
          home: MyApp(),
        ));
    
    class MyApp extends StatelessWidget {
      // This widget is the root of your application.
      @override
      Widget build(BuildContext context) {
        return _getCenterWidget(Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            _getBtn(context, 'right in',
                Tween(begin: Offset(1.0, 0.0), end: Offset.zero)),
            _getBtn(context, 'left in',
                Tween(begin: Offset(-1.0, 0.0), end: Offset.zero)),
            _getBtn(context, 'bottom in',
                Tween(begin: Offset(0.0, 1.0), end: Offset.zero)),
            _getBtn(context, 'top in',
                Tween(begin: Offset(0.0, -1.0), end: Offset.zero)),
          ],
        ));
      }
    }
    
    Widget _getBtn(BuildContext context, String textContent, Tween<Offset> tween) {
      return RaisedButton(
          child: Text(textContent),
          onPressed: () {
            Navigator.of(context).push(_createRoute(tween));
          });
    }
    
    Route _createRoute(Tween<Offset> tween) {
      return PageRouteBuilder(
          pageBuilder: (context, animation, secondaryAnimation) => Page2(),
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            return SlideTransition(
              position:
                  animation.drive(tween.chain(CurveTween(curve: Curves.ease))),
              child: child,
            );
          });
    }
    
    class Page2 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        Future.delayed(Duration(seconds: 1), () {
          Navigator.of(context).pop();
        });
        return _getCenterWidget(Text('Page2'));
      }
    }
    
    Widget _getCenterWidget(Widget child) {
      return Scaffold(
        appBar: AppBar(),
        body: Center(
          child: child,
        ),
      );
    }
    

    效果如下:

    结语

    到了这里,基本就把 Flutter 界面之间的过渡说清楚了。

    其他的比如旋转、缩放、透明度甚至组合动画,相信有了上面的基础,你也可以自行进行 DIY。

    这里附上缩放的效果如下:

    具体代码见 GitHub:
    flutter_page_transition

    参考链接:
    Animate a page route transition
    Tween class

    更多阅读:
    Flutter 即学即用系列博客
    Flutter & Dart

  • 相关阅读:
    Why Choose Jetty?
    Jetty 的工作原理以及与 Tomcat 的比较
    Tomcat设计模式
    Servlet 工作原理解析
    Tomcat 系统架构
    spring boot 打包方式 spring boot 整合mybaits REST services
    wireshark udp 序列号 User Datagram Protocol UDP
    Maven 的聚合(多模块)和 Parent 继承
    缓存策略 半自动化就是mybaitis只支持数据库查出的数据映射到pojo类上,而实体到数据库的映射需要自己编写sql语句实现,相较于hibernate这种完全自动化的框架我更喜欢mybatis
    Mybatis解决sql中like通配符模糊匹配 构造方法覆盖 mybits 增删改
  • 原文地址:https://www.cnblogs.com/nesger/p/11489579.html
Copyright © 2011-2022 走看看