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

4

所以,我在使用Flutter时遇到了一个与特定动画有关的问题。

基本上,我想要做的是同时运行路由变化的hero transition和相邻widget的自定义动画。

简单来说,我在我的根部拥有一个自定义的InheritedWidget,它从一个StatefulWidget父类中获取应用程序状态。在我的InheritedWidget中嵌套了一个WidgetsApp和一个相邻的自定义选项卡导航。树的结构看起来像这样:

Root Widget (Stateful)
        |
        |__InheritedWidget
                   |
                   |__WidgetsApp (Handles routing)
                   |
                   |__Navigation Bar (Overlay)

当我在我的WidgetsApp上执行使用Hero转换的路由更改时,出现了问题。在此期间,我试图通过动画将导航栏显示或隐藏,具体取决于用户所在的视图。但是,由于我在应用程序状态中使用布尔变量来通过动画显示或隐藏导航栏,因此在此过程中SetState调用“覆盖”了hero转换,因为树在重建过程中被重建(这是我想的)。
我的初步想法是InheritedWidget会捕获应用程序状态更改,并通过updateShouldNotify仅重建导航栏,但遗憾的是我没有看到期望的效果:(
因此,有人尝试过类似的事情吗?或者有什么好的解决方案呢? :)
1个回答

4
我已经做过类似的事情,但不幸的是我的代码还包含了许多其他内容,而且这样做相对复杂,因此我必须分离出一些东西来使示例更具代表性,但我现在无法做到。 我将解释我所做的事情的一般概念,虽然可能有更好的方法。
您想编写一个带有State的StatefulWidget,该State还扩展了NavigatorObserver(您可能可以使用无状态小部件,但我认为不行)。 我个人将其置于树中导航器之上(即在其构建函数中构建导航器),但您很可能也可以将其“放在”导航器旁边。
覆盖NavigatorObserver的didPush,didRemove,didPop等方法。 在每个方法内,调用setState并保存动画和其他参数,例如:
class NavigationFaderState extends State<NavigationFader> with NavigatorObserver {

  Animation _animation;
  // whatever else you need, maybe starting/finishing opacity or position etc.

  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
    setState(() {
      _animation = route.animation;
    }
    route.animation.addStatusListener((status) {
      if (status = AnimationStatus.completed) {
        setState(() {
          _animation = null;
        });
      }
    });
  }

  ....
}

在您的构建函数中,您需要检查_animation并根据其是否存在以及您可能想设置的任何其他参数进行动画处理(例如,一个标志用于动画处理,以及向前或向后是否有利 - 我认为'pop'动画可能从0开始,并像推送动画一样到1,但我可能错了)。 然后,您可以将此动画连接到您想要动画处理导航栏的方式,可能使用AnimatedBuilder或直接连接动画,或者其他方式。如果有关于所有这些工作原理的任何具体问题,请发表评论,我将添加一些注释等。 希望能有所帮助=) 编辑:附带完整代码示例。对于记录,我不提议这段代码非常好,或者这是您应该做的事情。但这是解决问题的一种方法。在真正的应用程序中使用它之前,值得测试它并可能添加一些断言以检查状态等。 import 'package:flutter/material.dart';
void main() => runApp(new MyApp());

class MyApp extends StatelessWidget {
  PushListener listener = new PushListener();

  @override
  Widget build(BuildContext context) {
    return new WidgetsApp(
      locale: new Locale("en"),
      navigatorObservers: [listener],
      builder: (context, child) {
        // this is here rather than outside the WidgetsApp so that it
        // gets access to directionality, text styles, etc
        return new Scaffold(
          body: child,
          bottomNavigationBar:
              new ColorChangingNavigationBar(key: listener.navBarKey),
        );
      },
      onGenerateRoute: (settings) {
        switch (settings.name) {
          case '/':
            return new MaterialPageRoute(
              settings: settings,
              builder: (context) => Column(
                    children: <Widget>[
                      new Text(
                          "I have a green nav bar when you open me and blue when you come back"),
                      new RaisedButton(
                        onPressed: () {
                          Navigator.pushNamed(context, "/red");
                        },
                        child: new Text("Next"),
                      ),
                    ],
                  ),
            );
          case '/red':
            return new MaterialPageRoute(
              settings: settings,
              builder: (context) => Column(
                    children: <Widget>[
                      new Text("I have a red nav bar"),
                      new RaisedButton(
                        onPressed: () {
                          Navigator.pop(context);
                        },
                      )
                    ],
                  ),
            );
        }
      },
      color: Colors.blue,
    );
  }
}

class PushListener extends NavigatorObserver {
  GlobalKey<ColorChangingNavigationBarState> navBarKey = new GlobalKey();

  @override
  void didPop(Route route, Route previousRoute) {
    if (route is ModalRoute && navBarKey.currentState != null) {
      var name = route.settings.name;
      var color = name == "/" ? Colors.red.shade500 : Colors.blue.shade500;
      var animation = new ReverseAnimation(route.animation);
      print("Popping & changing color to: ${name == "/" ? "red" : "blue"}");

      navBarKey.currentState.setAnimating(animation, color);
    }
  }

  @override
  void didPush(Route route, Route previousRoute) {
    if (route is ModalRoute && navBarKey.currentState != null) {
      var name = route.settings.name;
      var color = name == "/" ? Colors.blue.shade500 : Colors.red.shade500;
      print("Pushing & changing color to: ${name == "/" ? "red" : "blue"}");
      var animation = route.animation;
      navBarKey.currentState.setAnimating(animation, color);
    }
  }

  @override
  void didRemove(Route route, Route previousRoute) {
    // probably don't need
  }

  @override
  void didStartUserGesture() {
    // might want to do if gestures are supported with whichever type of
    // route you're using.
  }

  @override
  void didStopUserGesture() {
    // if you implement didStartUserGesture
  }
}

class ColorChangingNavigationBar extends StatefulWidget {
  final Color startColor;

  ColorChangingNavigationBar(
      {Key key, this.startColor = const Color.fromRGBO(0, 255, 0, 1.0)})
      : super(key: key);

  @override
  State<StatefulWidget> createState() => new ColorChangingNavigationBarState();
}

class _ColorAnimationInfo {
  final Animation animation;
  final Tween<Color> colorTween;
  final AnimationStatusListener statusListener;

  _ColorAnimationInfo(this.animation, this.colorTween, this.statusListener);
}

class ColorChangingNavigationBarState
    extends State<ColorChangingNavigationBar> {
  @override
  void initState() {
    _toColor = widget.startColor;
    super.initState();
  }

  Color _toColor;
  _ColorAnimationInfo _colorAnimationInfo;

  void setAnimating(Animation animation, Color to) {
    var fromColor;
    if (_colorAnimationInfo != null) {
      fromColor = _colorAnimationInfo.colorTween
          .lerp(_colorAnimationInfo.animation.value);
      _colorAnimationInfo.animation
          .removeStatusListener(_colorAnimationInfo.statusListener);
    } else {
      fromColor = _toColor;
    }

    var statusListener = (state) {
      if (state == AnimationStatus.completed ||
          state == AnimationStatus.dismissed) {
        setState(() {
          _colorAnimationInfo = null;
        });
      }
    };

    animation.addStatusListener(statusListener);

    setState(() {
      _toColor = to;
      Tween<Color> colorTween = new ColorTween(begin: fromColor, end: to);

      _colorAnimationInfo =
          new _ColorAnimationInfo(animation, colorTween, statusListener);
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_colorAnimationInfo != null) {
      return new AnimatedBuilder(
          animation: _colorAnimationInfo.animation,
          builder: (context, child) {
            return new Container(
              color: _colorAnimationInfo.colorTween
                  .lerp(_colorAnimationInfo.animation.value),
              height: 30.0,
            );
          });
    } else {
      return new Container(
        color: _toColor,
        height: 30.0,
      );
    }
  }

  @override
  void dispose() {
    if (_colorAnimationInfo != null) {
      _colorAnimationInfo.animation.removeStatusListener(_colorAnimationInfo.statusListener);
    }
    _colorAnimationInfo = null;
    super.dispose();
  }
}

非常感谢您抽出时间来回答! :) 我基本了解了您的概念,但我对Flutter仍然很陌生,如果您有时间发布一些示例代码,我将非常高兴看到它 :) - JacobHK
我会看看是否有时间! - rmtmckenzie
@JacobHK 我已经添加了一个代码示例。希望它足够清楚地说明它在做什么。代码肯定需要进行一些清理,但这应该可以传达要点 =)。哦,顺便说一句,就记录而言,这可能不是Flutter人员建议完全按照此方式执行的方法。 - rmtmckenzie
非常感谢您详细的回复!我一定会深入研究您解决方案的基础并进行测试! :) - JacobHK

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接