Flutter 使用内部动画为两条路线设置动画

Posted

技术标签:

【中文标题】Flutter 使用内部动画为两条路线设置动画【英文标题】:Flutter animating two routes with inner animations 【发布时间】:2020-07-17 16:22:40 【问题描述】:

我完全被它困住了。我想创建一个自定义页面路由,它将为 IN 和 OUT 页​​面设置动画。路线本身的动画是一项简单的任务,我是这样做的:

    import 'package:flutter/material.dart';

    class FromMenuRoute extends PageRouteBuilder 

      final Widget nextPage;
      final Widget prevPage;

      FromMenuRoute(this.prevPage, this.nextPage) : super(
        transitionDuration: Duration(milliseconds: 500),
        pageBuilder: (
          BuildContext context,
          Animation<double> animation,
          Animation<double> secondaryAnimation,
        ) 
          return nextPage;
        ,
        maintainState: false,
        transitionsBuilder: (
          BuildContext context,
          Animation<double> animation,
          Animation<double> secondaryAnimation,
          Widget child,
        ) 
          var colorTheme = CustomThemeData.of(context).colorTheme;
          return Material(
            child: Stack(
              overflow: Overflow.visible,
              children: <Widget>[
                SlideTransition(
                  position: Tween<Offset>(
                    begin: const Offset(0.0, 0.0),
                    end: const Offset(-0.3, 0.0),
                  ).animate(animation),
                  child: prevPage,
                ),
                SlideTransition(
                  position: Tween<Offset>(
                    begin: const Offset(1.0, 0.0),
                    end: Offset.zero,
                  ).animate(animation),
                  child: AnimatedBuilder(
                    animation: animation,
                    builder: (c, w) 
                      return Material(
                        shadowColor: colorTheme.textColor,
                        elevation: 30.0 * animation.value,
                        child: nextPage
                      );
                    ,
                  ),
                )
              ],
            ),
          );
        
      );
    

但问题是我还想在这些页面中运行一些动画,而不仅仅是为页面小部件本身设置动画。而且我不知道该怎么做。

我正在使用这样的路线

Navigator.of(context).push(
    FromMenuRoute(prevPage: widget, nextPage: nextPageWidget)
);

我的第一个想法是在 OUT 页​​面中启动一个动画控制器,然后像这样推送路由:

_animationController.forward();
Navigator.of(context).push(
    FromMenuRoute(prevPage: widget, nextPage: nextPageWidget)
);

它奏效了。页面动画与页面转换一起运行。但是当我需要弹出路由时,我无法设法反转控制器。 我的意思是,我当然可以,例如在 didWidgetUpdate 方法中,但它没有任何效果,因为小部件(在本示例中称为 nextPage)丢失了其上下文,并且动画控制器为小部件的另一个副本设置了动画,该副本在弹出过渡结束时才显示。 在弹出过渡运行时,它显示了“nextPage”的旧副本

第二个想法是使用 GlobalKey 来保持小部件的状态,该小部件也因错误而失败 使用 2 个重复的全局键。

理想的选择是在我的 FromMenuRoute 中是这样的

SlideTransition(
   position: Tween<Offset>(
    begin: const Offset(0.0, 0.0),
    end: const Offset(-0.3, 0.0),
  ).animate(animation),
  child: prevPage.copyWith(animation: animation), // but I understand this can't be done
),

所以我已经没有好主意了。我不想以某种非常棘手的方式做到这一点。也许有一些我不知道的更好的解决方案。请分享一下,如果你知道如何解决这个问题


这是我想要实现的详细示例

import 'package:flutter/material.dart';

class Page1 extends StatefulWidget 
  @override
  _Page1State createState() => _Page1State();


class _Page1State extends State<Page1> with SingleTickerProviderStateMixin 


  AnimationController _animationController;

  @override
  void initState() 
    _animationController = AnimationController(
      vsync: this,
      lowerBound: 0.0,
      upperBound: 1.0,
      duration: Duration(milliseconds: 3000)
    );
    super.initState();
  
  @override
  void dispose() 
    _animationController.dispose();
    super.dispose();
  


  @override
  Widget build(BuildContext context) 
    return Container(
      color: Colors.red,
      width: double.infinity,
      height: double.infinity,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Container(
            width: double.infinity,
            height: 60.0,
            color: Colors.amber,
            child: Stack(
              children: <Widget>[
                SlideTransition(
                  position: Tween<Offset>(
                    begin: const Offset(0.0, 0.0),
                    end: Offset(3.0, 0.0),
                  ).animate(_animationController),
                  child: Container(
                    height: 60.0,
                    width: 60.0,
                    color: Colors.blue,
                  ),
                ),
              ],
            ),
          ),
          SizedBox(height: 100.0,),
          RaisedButton(
            child: Text('Add new route'),
            onPressed: () 
              /// calling here doe not affect animation because
              /// a new widget is created on top of this
              /// but is I call it from initState() I won't have access to the 
              /// controller later, when I need to revert the animation
              _animationController.forward();
              Navigator.of(context).push(
                FromMenuRoute(prevPage: widget, nextPage: Page2())
              );
            ,
          ),
        ],
      ),
    );
  



class Page2 extends StatefulWidget 
  @override
  _Page2State createState() => _Page2State();


class _Page2State extends State<Page2> 
  @override
  Widget build(BuildContext context) 
    return Container(
      color: Colors.green,
      width: double.infinity,
      height: double.infinity,
    );
  



class FromMenuRoute extends PageRouteBuilder 

  final Widget nextPage;
  final Widget prevPage;

  FromMenuRoute(this.prevPage, this.nextPage) : super(
    transitionDuration: Duration(milliseconds: 3000),
    pageBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
    ) 
      return nextPage;
    ,
    maintainState: true,
    transitionsBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child,
    ) 
      return Material(
        child: Stack(
          overflow: Overflow.visible,
          children: <Widget>[
            SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(0.0, 0.0),
                end: const Offset(-0.3, 0.0),
              ).animate(animation),
              child: prevPage,
            ),

            SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(1.0, 0.0),
                end: Offset.zero,
              ).animate(animation),
              child: AnimatedBuilder(
                animation: animation,
                builder: (c, w) 
                  return Opacity(
                    opacity: .8,
                    child: Material(
                      shadowColor: Colors.black,
                      elevation: 30.0 * animation.value,
                      child: nextPage
                    ),
                  );
                ,
              ),
            )
          ],
        ),
      );
    
  );

我需要在页面转换开始时开始移动一个蓝色方块,并在路线弹出时恢复该方块的动画

【问题讨论】:

@pskink 你读过这个问题吗? 是的。路线本身的动画不是我需要的。当路线动画开始时,我需要在小部件内运行动画。动画和secondaryAnimation 都无济于事。我通过 nextPage 参数传递了一个小部件,然后它用于推送和弹出路由,但问题是它在推送路由结束后丢失了它的上下文,所以我无法再访问它以在其中运行一些动画。而是在后台创建一个新的小部件,并将上下文附加到它。这就是我已经苦苦挣扎了 2 天的问题 如果我能像你的例子那样做,那将是一件容易的事。您将动画传递给小部件的构造函数。我不能这样做,因为我使用了一个之前已经构建的小部件,因此无法将动画传递给该小部件 我可能误解了这个问题,或者你误解了它。这是数据示例paste.ubuntu.com/p/d6vGMMG9Tj 我需要在过渡开始时为蓝色方块设置动画并在路线弹出时恢复其动画,这就是我不能做的,因为在背景中创建了一个具有相同状态的新小部件跨度> 是的,我做到了。这不是我所要求的,因为它在路由内构造页面内容,但在我的情况下,路由无法访问页面的内容。看看我提供的答案。我找到了解决方案。路线动画可以通过 ModalRoute.of(context).animation 访问。这样就解决了问题 【参考方案1】:

我刚刚找到了解决方案。我可以通过

访问动画
ModalRoute.of(context).animation;

这是工作示例

import 'package:flutter/material.dart';

class Page1 extends StatefulWidget 
  @override
  _Page1State createState() => _Page1State();


class _Page1State extends State<Page1> with SingleTickerProviderStateMixin 

  @override
  Widget build(BuildContext context) 
    var modalRoute = ModalRoute.of(context);

    return Container(
      color: Colors.red,
      width: double.infinity,
      height: double.infinity,
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          Container(
            width: double.infinity,
            height: 60.0,
            color: Colors.amber,
            child: Stack(
              children: <Widget>[
                AnimatedBuilder(
                  animation: modalRoute.animation,
                  builder: (c, w) 
                    var isCompleted = modalRoute.animation.status == AnimationStatus.completed;
                    var posX = 150.0 * modalRoute.animation.value;
                    if (isCompleted) 
                      posX = 0;
                    
                    return Transform(
                      transform: Matrix4.translationValues(posX, 0, 0),
                      child: Container(
                        height: 60.0,
                        width: 60.0,
                        color: Colors.blue,
                      ),
                    );
                  ,
                ),
              ],
            ),
          ),
          SizedBox(height: 100.0,),
          RaisedButton(
            child: Text('Add new route'),
            onPressed: () 
              Navigator.of(context).push(
                FromMenuRoute(prevPage: widget, nextPage: Page2())
              );
            ,
          ),
        ],
      ),
    );
  



class Page2 extends StatefulWidget 
  @override
  _Page2State createState() => _Page2State();


class _Page2State extends State<Page2> 
  @override
  Widget build(BuildContext context) 
    return Container(
      color: Colors.green,
      width: double.infinity,
      height: double.infinity,
    );
  



class FromMenuRoute extends PageRouteBuilder 

  final Widget nextPage;
  final Widget prevPage;

  FromMenuRoute(this.prevPage, this.nextPage) : super(
    transitionDuration: Duration(milliseconds: 3000),
    pageBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
    ) 
      return nextPage;
    ,
    maintainState: true,
    transitionsBuilder: (
      BuildContext context,
      Animation<double> animation,
      Animation<double> secondaryAnimation,
      Widget child,
    ) 
      return Material(
        child: Stack(
          overflow: Overflow.visible,
          children: <Widget>[
            SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(0.0, 0.0),
                end: const Offset(-0.3, 0.0),
              ).animate(animation),
              child: prevPage,
            ),

            SlideTransition(
              position: Tween<Offset>(
                begin: const Offset(1.0, 0.0),
                end: Offset.zero,
              ).animate(animation),
              child: AnimatedBuilder(
                animation: animation,
                builder: (c, w) 
                  return Material(
                    shadowColor: Colors.black,
                    elevation: 30.0 * animation.value,
                    child: nextPage
                  );
                ,
              ),
            )
          ],
        ),
      );
    
  );

【讨论】:

以上是关于Flutter 使用内部动画为两条路线设置动画的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Flutter 中弹出没有动画的屏幕

flutter 实现弹出窗 点击下拉栏 微信右上角弹出窗

如何在jquery中同时为两件事制作动画

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

禁用 Flutter Hero 反向动画

Flutter:使用底部导航栏为应用栏颜色设置动画