Flutter 过渡出口

Posted

技术标签:

【中文标题】Flutter 过渡出口【英文标题】:Flutter Transition Exit 【发布时间】:2019-03-16 15:42:40 【问题描述】:

android API上我们可以使用

overridePendingTransition(int enterAnim, int exitAnim) 

定义进入和退出过渡。

如何在 Flutter 中实现?

我已经实现了这段代码

class SlideLeftRoute extends PageRouteBuilder 
  final Widget enterWidget;
  SlideLeftRoute(this.enterWidget)
      : super(
      pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) 
        return enterWidget;
      ,
      transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) 
        return SlideTransition(
          position: new Tween<Offset>(
            begin: const Offset(1.0, 0.0),
            end: Offset.zero,
          ).animate(animation),
          child: child
        );
      ,

  );

但它只定义了输入转换。如何定义 de exit 过渡?

更新

想象一下,当我执行时,我有两个屏幕(Screen1 和 Screen2)

 Navigator.push(
        context, SlideLeftRoute(enterWidget: Screen2()));

我想将动画应用到 Screen1 和 Screen2 而不仅仅是 Screen2

example

【问题讨论】:

【参考方案1】:

实现这一点的正确方法是使用在 PageRouteBuilder 对象的 transitionBuilder 中给出的 secondAnimation 参数。

这里你可以在flutter sdk的flutter/lib/src/widgets/routes.dart文件中阅读更多关于secondaryAnimation参数的信息:

///
/// When the [Navigator] pushes a route on the top of its stack, the
/// [secondaryAnimation] can be used to define how the route that was on
/// the top of the stack leaves the screen. Similarly when the topmost route
/// is popped, the secondaryAnimation can be used to define how the route
/// below it reappears on the screen. When the Navigator pushes a new route
/// on the top of its stack, the old topmost route's secondaryAnimation
/// runs from 0.0 to 1.0. When the Navigator pops the topmost route, the
/// secondaryAnimation for the route below it runs from 1.0 to 0.0.
///
/// The example below adds a transition that's driven by the
/// [secondaryAnimation]. When this route disappears because a new route has
/// been pushed on top of it, it translates in the opposite direction of
/// the new route. Likewise when the route is exposed because the topmost
/// route has been popped off.
///
/// ```dart
///   transitionsBuilder: (
///       BuildContext context,
///       Animation<double> animation,
///       Animation<double> secondaryAnimation,
///       Widget child,
///   ) 
///     return SlideTransition(
///       position: AlignmentTween(
///         begin: const Offset(0.0, 1.0),
///         end: Offset.zero,
///       ).animate(animation),
///       child: SlideTransition(
///         position: TweenOffset(
///           begin: Offset.zero,
///           end: const Offset(0.0, 1.0),
///         ).animate(secondaryAnimation),
///         child: child,
///       ),
///     );
///   
/// ```

这是一个使用 secondaryAnimation 参数的工作示例:

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      initialRoute: '/',
      onGenerateRoute: (RouteSettings settings) 
        if (settings.name == '/') 
          return PageRouteBuilder<dynamic>(
            pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => Page1(),
            transitionsBuilder: (
              BuildContext context,
              Animation<double> animation,
              Animation<double> secondaryAnimation,
              Widget child,
            ) 
              final Tween<Offset> offsetTween = Tween<Offset>(begin: Offset(0.0, 0.0), end: Offset(-1.0, 0.0));
              final Animation<Offset> slideOutLeftAnimation = offsetTween.animate(secondaryAnimation);
              return SlideTransition(position: slideOutLeftAnimation, child: child);
            ,
          );
         else 
          // handle other routes here
          return null;
        
      ,
    );
  


class Page1 extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(title: Text("Page 1")),
      backgroundColor: Colors.blue,
      body: Center(
        child: RaisedButton(
          onPressed: () => Navigator.push(
            context,
            PageRouteBuilder<dynamic>(
              pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) => Page2(),
              transitionsBuilder: (
                BuildContext context,
                Animation<double> animation,
                Animation<double> secondaryAnimation,
                Widget child,
              ) 
                final Tween<Offset> offsetTween = Tween<Offset>(begin: Offset(1.0, 0.0), end: Offset(0.0, 0.0));
                final Animation<Offset> slideInFromTheRightAnimation = offsetTween.animate(animation);
                return SlideTransition(position: slideInFromTheRightAnimation, child: child);
              ,
            ),
          ),
          child: Text("Go to Page 2"),
        ),
      ),
    );
  


class Page2 extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(title: Text("Page 2"), backgroundColor: Colors.green),
      backgroundColor: Colors.green,
      body: Center(child: RaisedButton(onPressed: () => Navigator.pop(context), child: Text("Back to Page 1"))),
    );
  

结果:

【讨论】:

能否请您添加第三条额外路线,并显示您是否仍然可以重现辅助动画....当我推送命名路线时,它的偏移量是 (-1, 0) 而不是 (0, 0)【参考方案2】:

截图(Null Safe):


我使用了不同的方式,但diegodeveloper提供的类似逻辑

完整代码:

void main() => runApp(MaterialApp(home: Page1()));

class Page1 extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      backgroundColor: Colors.grey,
      appBar: AppBar(title: Text('Page 1')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.push(
            context,
            MyCustomPageRoute(
              parent: this,
              builder: (context) => Page2(),
            ),
          ),
          child: Text('2nd Page'),
        ),
      ),
    );
  


class Page2 extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      backgroundColor: Colors.blueGrey,
      appBar: AppBar(title: Text('Page 2')),
      body: Center(
        child: ElevatedButton(
          onPressed: () => Navigator.pop(context),
          child: Text('Back'),
        ),
      ),
    );
  


class MyCustomPageRoute<T> extends MaterialPageRoute<T> 
  final Widget parent;

  MyCustomPageRoute(
    required this.parent,
    required WidgetBuilder builder,
    RouteSettings? settings,
  ) : super(builder: builder, settings: settings);

  @override
  Widget buildTransitions(_, Animation<double> animation, __, Widget child) 
    var anim1 = Tween<Offset>(begin: Offset.zero, end: Offset(-1.0, 0.0)).animate(animation);
    var anim2 = Tween<Offset>(begin: Offset(1.0, 0.0), end: Offset.zero).animate(animation);
    return Stack(
      children: <Widget>[
        SlideTransition(position: anim1, child: parent),
        SlideTransition(position: anim2, child: child),
      ],
    );
  

【讨论】:

似乎使用这种方法或@diegodeveloper 方法,小部件在转换开始时调用 initState() 。使用 MaterialPageRoute 它不会发生。 我已经用 MaterialPageRoute 尝试过这段代码,但是 initState() 还是被调用了......它会是什么? 你是指第二页的initState() 吗? 我的意思是 oldWidget 的 initState @CopsOnRoad 如何在列表项上执行此动画?你能指导我吗?【参考方案3】:

好问题,PageRouteBuilder 默认使用AnimationController 来处理动画转换,因此,当您关闭视图时,它只需从animationController 调用“reverse”方法,您将看到您正在使用的相同动画但反过来。

如果您想在关闭视图时更改动画,您可以检查当前动画的状态并与AnimationStatus.reverse进行比较

这是您的代码,当它反向时带有Fade 动画。

  class SlideLeftRoute extends PageRouteBuilder 
    final Widget enterWidget;
    SlideLeftRoute(this.enterWidget)
        : super(
            pageBuilder: (BuildContext context, Animation<double> animation,
                Animation<double> secondaryAnimation) 
              return enterWidget;
            ,
            transitionsBuilder: (BuildContext context,
                Animation<double> animation,
                Animation<double> secondaryAnimation,
                Widget child) 
              if (animation.status == AnimationStatus.reverse) 
                //do your dismiss animation here
                return FadeTransition(
                  opacity: animation,
                  child: child,
                );
               else 
                return SlideTransition(
                    position: new Tween<Offset>(
                      begin: const Offset(1.0, 0.0),
                      end: Offset.zero,
                    ).animate(animation),
                    child: child);
              
            ,
          );
  

解决方法

    class SlideLeftRoute extends PageRouteBuilder 
      final Widget enterWidget;
      final Widget oldWidget;

      SlideLeftRoute(this.enterWidget, this.oldWidget)
          : super(
                transitionDuration: Duration(milliseconds: 600),
                pageBuilder: (BuildContext context, Animation<double> animation,
                    Animation<double> secondaryAnimation) 
                  return enterWidget;
                ,
                transitionsBuilder: (BuildContext context,
                    Animation<double> animation,
                    Animation<double> secondaryAnimation,
                    Widget child) 
                  return Stack(
                    children: <Widget>[
                      SlideTransition(
                          position: new Tween<Offset>(
                            begin: const Offset(0.0, 0.0),
                            end: const Offset(-1.0, 0.0),
                          ).animate(animation),
                          child: oldWidget),
                      SlideTransition(
                          position: new Tween<Offset>(
                            begin: const Offset(1.0, 0.0),
                            end: Offset.zero,
                          ).animate(animation),
                          child: enterWidget)
                    ],
                  );
                );
    

用法:

 Navigator.of(context)
              .push(SlideLeftRoute(enterWidget: Page2(), oldWidget: this));

【讨论】:

你是不是想说,如果我们正在运行 FadeTransition 来切换到一个新的路由,那么之前的路由也会运行这个转换,但是顺序相反? @CopsOnRoad ,您可以尝试删除 slidetransition 和 animation.status 条件 其实你的代码看起来不错,虽然我还没试过。正如您在第一段中所写的那样,先前的路线(屏幕)将自动以相反的顺序运行转换(默认情况下)。但是当我这样做时,旧路线仍然存在,只有新路线变得活跃。 @CopsOnRoad 完全正确.. 我想说的是,当我推动新屏幕时,前一个屏幕不会做任何动画。只有新屏幕有过渡。 您可以在 SlideLeftRoute 中添加另一个参数并传递旧的孩子 :)【参考方案4】:

还有另一种方法可以做到这一点。 initState() 在 oldWidget 中被调用的问题将不再存在。

void main() => runApp(MaterialApp(theme: ThemeData.dark(), home: HomePage()));

class HomePage extends StatefulWidget 
  @override
  _HomePageState createState() => _HomePageState();


class _HomePageState extends State<HomePage> 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(title: Text("Page 1")),
      body: RaisedButton(
        child: Text("Next"),
        onPressed: () 
          Navigator.push(
            context,
            PageRouteBuilder(
              pageBuilder: (c, a1, a2) => Page2(),
              transitionsBuilder: (context, anim1, anim2, child) 
                return SlideTransition(
                  position: Tween<Offset>(end: Offset(0, 0), begin: Offset(1, 0)).animate(anim1),
                  child: Page2(),
                );
              ,
            ),
          );
        ,
      ),
    );
  


class Page2 extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(title: Text("Page 2")),
      body: RaisedButton(
        child: Text("Back"),
        onPressed: () => Navigator.pop(context),
      ),
    );
  

【讨论】:

以上是关于Flutter 过渡出口的主要内容,如果未能解决你的问题,请参考以下文章

Flutter 初尝:从 Java 无缝过渡

Flutter 页面路由过渡动画

Flutter:同时进行英雄过渡 + 小部件动画?

Flutter 在主题中改变页面过渡速度/持续时间

可以使用 Flutter 复制 iOS App Store 过渡吗?

Flutter Navigator 2.0 页面过渡