Flutter:如何在页面转换时为背景颜色设置动画

Posted

技术标签:

【中文标题】Flutter:如何在页面转换时为背景颜色设置动画【英文标题】:Flutter: How to animate the background color while page transition 【发布时间】:2021-02-27 19:40:19 【问题描述】:

我想在两个页面的 背景色 之间设置动画。我说的是页面转换,但不是转换整个页面,我只想更改新页面的背景颜色(从前一页的 bg 颜色到新颜色),而其余的前景内容淡入(或任何其他类型的过渡)。

如果您需要更多说明,我需要类似 Hero 的过渡,但要使用页面的背景颜色。

背景色不必只是某个容器的颜色属性。

编辑:为了轻松回答,假设我的页面可以通过为其构造函数指定颜色来调用。所以页面是这样的,

class MyWidget extends StatelessWidget 
  final Color bgColor;

  const MyWidget(this.bgColor);

  @override
  Widget build(BuildContext context) 
    return Stack(
      fit: StackFit.expand,
      children: [
        Container(color: bgColor),
        // foreground widgets...
      ],
    );
  

我的方法应该是什么?我应该创建类似自定义过渡的东西吗?在这种情况下,我该如何为背景颜色设置动画?

【问题讨论】:

【参考方案1】:

ColoredBox修改Hero,并在MaterialAppnavigatorObservers中添加新的NavigatorObserver

import 'package:flutter/foundation.dart';

import 'package:flutter/material.dart';

void main() 
  runApp(App());


class App extends StatelessWidget 
  const App(Key key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return MaterialApp(
      navigatorObservers: [ColorHeroController()],
      home: Screen1(),
    );
  


class Screen1 extends StatelessWidget 
  Widget build(BuildContext context) 
    return Scaffold(
      body: Center(
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('This is color hero'),
            Padding(
              padding: EdgeInsets.only(bottom: 50, left: 50),
              child: ColorHero(
                color: Colors.blue,
                tag: 'tag',
                child: Container(
                  height: 50,
                  width: 100,
                ),
              ),
            ),
            RaisedButton(
              onPressed: () 
                Navigator.of(context).push(
                  MaterialPageRoute(
                    builder: (context) => Screen2(),
                  ),
                );
              ,
              child: Text('go'),
            ),
          ],
        ),
      ),
    );
  


class Screen2 extends StatelessWidget 
  const Screen2(Key key) : super(key: key);

  @override
  Widget build(BuildContext context) 
    return Scaffold(
      appBar: AppBar(title: Text('page2')),
      body: Center(
        child: Row(
          children: [
            Text('Page2'),
            ColorHero(
              tag: 'tag',
              color: Colors.red,
              child: Container(
                height: 100,
                width: 100,
              ),
            ),
          ],
        ),
      ),
    );
  


typedef CreateRectTween = Tween<Rect> Function(Rect begin, Rect end);

typedef HeroPlaceholderBuilder = Widget Function(
  BuildContext context,
  Size heroSize,
  Widget child,
);

typedef HeroFlightShuttleBuilder = Widget Function(
  BuildContext flightContext,
  Animation<double> animation,
  HeroFlightDirection flightDirection,
  BuildContext fromHeroContext,
  BuildContext toHeroContext,
);

typedef _OnFlightEnded = void Function(_HeroFlight flight);

enum HeroFlightDirection  push, pop 

Rect _boundingBoxFor(BuildContext context, [BuildContext ancestorContext]) 
  final RenderBox box = context.findRenderObject() as RenderBox;
  assert(box != null && box.hasSize);
  return MatrixUtils.transformRect(
    box.getTransformTo(ancestorContext?.findRenderObject()),
    Offset.zero & box.size,
  );


class ColorHero extends StatefulWidget 
  const ColorHero(
    @required this.color,
    Key key,
    @required this.tag,
    this.createRectTween,
    this.flightShuttleBuilder,
    this.placeholderBuilder,
    this.transitionOnUserGestures = false,
    @required this.child,
  )  : assert(tag != null),
        assert(transitionOnUserGestures != null),
        assert(child != null),
        super(key: key);

  final Object tag;

  final Color color;

  final CreateRectTween createRectTween;

  final Widget child;

  final HeroFlightShuttleBuilder flightShuttleBuilder;

  final HeroPlaceholderBuilder placeholderBuilder;

  final bool transitionOnUserGestures;

  static Map<Object, _ColorHeroState> _allHeroesFor(
    BuildContext context,
    bool isUserGestureTransition,
    NavigatorState navigator,
  ) 
    assert(context != null);
    assert(isUserGestureTransition != null);
    assert(navigator != null);
    final Map<Object, _ColorHeroState> result = <Object, _ColorHeroState>;

    void inviteHero(StatefulElement hero, Object tag) 
      assert(() 
        if (result.containsKey(tag)) 
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary(
                'There are multiple heroes that share the same tag within a subtree.'),
            ErrorDescription(
                'Within each subtree for which heroes are to be animated (i.e. a PageRoute subtree), '
                'each Hero must have a unique non-null tag.\n'
                'In this case, multiple heroes had the following tag: $tag\n'),
            DiagnosticsProperty<StatefulElement>(
                'Here is the subtree for one of the offending heroes', hero,
                linePrefix: '# ', style: DiagnosticsTreeStyle.dense),
          ]);
        
        return true;
      ());
      final ColorHero heroWidget = hero.widget as ColorHero;
      final _ColorHeroState heroState = hero.state as _ColorHeroState;
      if (!isUserGestureTransition || heroWidget.transitionOnUserGestures) 
        result[tag] = heroState;
       else 
        heroState.ensurePlaceholderIsHidden();
      
    

    void visitor(Element element) 
      final Widget widget = element.widget;
      if (widget is ColorHero) 
        final StatefulElement hero = element as StatefulElement;
        final Object tag = widget.tag;
        assert(tag != null);
        if (Navigator.of(hero) == navigator) 
          inviteHero(hero, tag);
         else 
          final ModalRoute<dynamic> heroRoute = ModalRoute.of(hero);
          if (heroRoute != null &&
              heroRoute is PageRoute &&
              heroRoute.isCurrent) 
            inviteHero(hero, tag);
          
        
      
      element.visitChildren(visitor);
    

    context.visitChildElements(visitor);
    return result;
  

  @override
  _ColorHeroState createState() => _ColorHeroState();

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) 
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<Object>('tag', tag));
  


class _ColorHeroState extends State<ColorHero> 
  final GlobalKey _key = GlobalKey();
  Size _placeholderSize;

  bool _shouldIncludeChild = true;

  void startFlight(bool shouldIncludedChildInPlaceholder = false) 
    _shouldIncludeChild = shouldIncludedChildInPlaceholder;
    assert(mounted);
    final RenderBox box = context.findRenderObject() as RenderBox;
    assert(box != null && box.hasSize);
    setState(() 
      _placeholderSize = box.size;
    );
  

  void ensurePlaceholderIsHidden() 
    if (mounted) 
      setState(() 
        _placeholderSize = null;
      );
    
  

  void endFlight(bool keepPlaceholder = false) 
    if (!keepPlaceholder) 
      ensurePlaceholderIsHidden();
    
  

  @override
  Widget build(BuildContext context) 
    assert(context.findAncestorWidgetOfExactType<ColorHero>() == null,
        'A Hero widget cannot be the descendant of another Hero widget.');

    final bool showPlaceholder = _placeholderSize != null;

    if (showPlaceholder && widget.placeholderBuilder != null) 
      return widget.placeholderBuilder(context, _placeholderSize, widget.child);
    

    if (showPlaceholder && !_shouldIncludeChild) 
      return SizedBox(
        width: _placeholderSize.width,
        height: _placeholderSize.height,
      );
    

    return SizedBox(
      width: _placeholderSize?.width,
      height: _placeholderSize?.height,
      child: Offstage(
        offstage: showPlaceholder,
        child: TickerMode(
          enabled: !showPlaceholder,
          child: KeyedSubtree(
            key: _key,
            child: ColoredBox(
              color: widget.color,
              child: widget.child,
            ),
          ),
        ),
      ),
    );
  


class _HeroFlightManifest 
  _HeroFlightManifest(
    @required this.type,
    @required this.overlay,
    @required this.navigatorRect,
    @required this.fromRoute,
    @required this.toRoute,
    @required this.fromHero,
    @required this.toHero,
    @required this.createRectTween,
    @required this.shuttleBuilder,
    @required this.isUserGestureTransition,
    @required this.isDiverted,
  ) : assert(fromHero.widget.tag == toHero.widget.tag);

  final HeroFlightDirection type;
  final OverlayState overlay;
  final Rect navigatorRect;
  final PageRoute<dynamic> fromRoute;
  final PageRoute<dynamic> toRoute;
  final _ColorHeroState fromHero;
  final _ColorHeroState toHero;
  final CreateRectTween createRectTween;
  final HeroFlightShuttleBuilder shuttleBuilder;
  final bool isUserGestureTransition;
  final bool isDiverted;

  Object get tag => fromHero.widget.tag;

  Animation<double> get animation 
    return CurvedAnimation(
      parent: (type == HeroFlightDirection.push)
          ? toRoute.animation
          : fromRoute.animation,
      curve: Curves.fastOutSlowIn,
      reverseCurve: isDiverted ? null : Curves.fastOutSlowIn.flipped,
    );
  

  @override
  String toString() 
    return '_HeroFlightManifest($type tag: $tag from route: $fromRoute.settings '
        'to route: $toRoute.settings with hero: $fromHero to $toHero)';
  


class _HeroFlight 
  _HeroFlight(this.onFlightEnded) 
    _proxyAnimation = ProxyAnimation()
      ..addStatusListener(_handleAnimationUpdate);
  

  final _OnFlightEnded onFlightEnded;

  Tween<Rect> heroRectTween;
  Tween<Color> colorTween;

  Widget shuttle;

  Animation<double> _heroOpacity = kAlwaysCompleteAnimation;
  ProxyAnimation _proxyAnimation;

  _HeroFlightManifest manifest;
  OverlayEntry overlayEntry;
  bool _aborted = false;

  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);
  

  static final Animatable<double> _reverseTween =
      Tween<double>(begin: 1.0, end: 0.0);

  Widget _buildOverlay(BuildContext context) 
    assert(manifest != null);
    shuttle ??= manifest.shuttleBuilder(
      context,
      manifest.animation,
      manifest.type,
      manifest.fromHero.context,
      manifest.toHero.context,
    );
    assert(shuttle != null);

    return AnimatedBuilder(
      animation: _proxyAnimation,
      child: shuttle,
      builder: (BuildContext context, Widget child) 
        final RenderBox toHeroBox =
            manifest.toHero.context?.findRenderObject() as RenderBox;
        if (_aborted || toHeroBox == null || !toHeroBox.attached) 
          if (_heroOpacity.isCompleted) 
            _heroOpacity = _proxyAnimation.drive(
              _reverseTween.chain(
                  CurveTween(curve: Interval(_proxyAnimation.value, 1.0))),
            );
          
         else if (toHeroBox.hasSize) 
          final RenderBox finalRouteBox =
              manifest.toRoute.subtreeContext?.findRenderObject() as RenderBox;
          final Offset toHeroOrigin =
              toHeroBox.localToGlobal(Offset.zero, ancestor: finalRouteBox);
          if (toHeroOrigin != heroRectTween.end.topLeft) 
            final Rect heroRectEnd = toHeroOrigin & heroRectTween.end.size;
            heroRectTween =
                _doCreateRectTween(heroRectTween.begin, heroRectEnd);
          
        

        final Rect rect = heroRectTween.evaluate(_proxyAnimation);
        final Size size = manifest.navigatorRect.size;
        final RelativeRect offsets = RelativeRect.fromSize(rect, size);

        final color = ColorTween(
          begin: manifest.fromHero.widget.color,
          end: manifest.toHero.widget.color,
        ).evaluate(_proxyAnimation);

        return Positioned(
          top: offsets.top,
          right: offsets.right,
          bottom: offsets.bottom,
          left: offsets.left,
          child: IgnorePointer(
            child: RepaintBoundary(
              child: Opacity(
                opacity: _heroOpacity.value,
                child: ColoredBox(
                  color: color,
                  child: child,
                ),
              ),
            ),
          ),
        );
      ,
    );
  

  void _handleAnimationUpdate(AnimationStatus status) 
    if (manifest.fromRoute?.navigator?.userGestureInProgress == true) return;
    if (status == AnimationStatus.completed ||
        status == AnimationStatus.dismissed) 
      _proxyAnimation.parent = null;

      assert(overlayEntry != null);
      overlayEntry.remove();
      overlayEntry = null;

      manifest.fromHero
          .endFlight(keepPlaceholder: status == AnimationStatus.completed);
      manifest.toHero
          .endFlight(keepPlaceholder: status == AnimationStatus.dismissed);
      onFlightEnded(this);
    
  

  void start(_HeroFlightManifest initialManifest) 
    assert(!_aborted);
    assert(() 
      final Animation<double> initial = initialManifest.animation;
      assert(initial != null);
      final HeroFlightDirection type = initialManifest.type;
      assert(type != null);
      switch (type) 
        case HeroFlightDirection.pop:
          return initial.value == 1.0 && initialManifest.isUserGestureTransition
              ? initial.status == AnimationStatus.completed
              : initial.status == AnimationStatus.reverse;
        case HeroFlightDirection.push:
          return initial.value == 0.0 &&
              initial.status == AnimationStatus.forward;
      
      return null;
    ());

    manifest = initialManifest;

    if (manifest.type == HeroFlightDirection.pop)
      _proxyAnimation.parent = ReverseAnimation(manifest.animation);
    else
      _proxyAnimation.parent = manifest.animation;

    manifest.fromHero.startFlight(
        shouldIncludedChildInPlaceholder:
            manifest.type == HeroFlightDirection.push);
    manifest.toHero.startFlight();

    heroRectTween = _doCreateRectTween(
      _boundingBoxFor(
          manifest.fromHero.context, manifest.fromRoute.subtreeContext),
      _boundingBoxFor(manifest.toHero.context, manifest.toRoute.subtreeContext),
    );

    overlayEntry = OverlayEntry(builder: _buildOverlay);
    manifest.overlay.insert(overlayEntry);
  

  void divert(_HeroFlightManifest newManifest) 
    assert(manifest.tag == newManifest.tag);
    if (manifest.type == HeroFlightDirection.push &&
        newManifest.type == HeroFlightDirection.pop) 
      assert(newManifest.animation.status == AnimationStatus.reverse);
      assert(manifest.fromHero == newManifest.toHero);
      assert(manifest.toHero == newManifest.fromHero);
      assert(manifest.fromRoute == newManifest.toRoute);
      assert(manifest.toRoute == newManifest.fromRoute);

      _proxyAnimation.parent = ReverseAnimation(newManifest.animation);
      heroRectTween = ReverseTween<Rect>(heroRectTween);
     else if (manifest.type == HeroFlightDirection.pop &&
        newManifest.type == HeroFlightDirection.push) 
      assert(newManifest.animation.status == AnimationStatus.forward);
      assert(manifest.toHero == newManifest.fromHero);
      assert(manifest.toRoute == newManifest.fromRoute);

      _proxyAnimation.parent = newManifest.animation.drive(
        Tween<double>(
          begin: manifest.animation.value,
          end: 1.0,
        ),
      );
      if (manifest.fromHero != newManifest.toHero) 
        manifest.fromHero.endFlight(keepPlaceholder: true);
        newManifest.toHero.startFlight();
        heroRectTween = _doCreateRectTween(
          heroRectTween.end,
          _boundingBoxFor(
              newManifest.toHero.context, newManifest.toRoute.subtreeContext),
        );
       else 
        heroRectTween =
            _doCreateRectTween(heroRectTween.end, heroRectTween.begin);
      
     else 
      assert(manifest.fromHero != newManifest.fromHero);
      assert(manifest.toHero != newManifest.toHero);

      heroRectTween = _doCreateRectTween(
        heroRectTween.evaluate(_proxyAnimation),
        _boundingBoxFor(
            newManifest.toHero.context, newManifest.toRoute.subtreeContext),
      );
      shuttle = null;

      if (newManifest.type == HeroFlightDirection.pop)
        _proxyAnimation.parent = ReverseAnimation(newManifest.animation);
      else
        _proxyAnimation.parent = newManifest.animation;

      manifest.fromHero.endFlight(keepPlaceholder: true);
      manifest.toHero.endFlight(keepPlaceholder: true);

      newManifest.fromHero.startFlight(
          shouldIncludedChildInPlaceholder:
              newManifest.type == HeroFlightDirection.push);
      newManifest.toHero.startFlight();

      overlayEntry.markNeedsBuild();
    

    _aborted = false;
    manifest = newManifest;
  

  void abort() 
    _aborted = true;
  

  @override
  String toString() 
    final RouteSettings from = manifest.fromRoute.settings;
    final RouteSettings to = manifest.toRoute.settings;
    final Object tag = manifest.tag;
    return 'HeroFlight(for: $tag, from: $from, to: $to $_proxyAnimation.parent)';
  


class ColorHeroController extends NavigatorObserver 
  ColorHeroController(this.createRectTween);

  final CreateRectTween createRectTween;

  final Map<Object, _HeroFlight> _flights = <Object, _HeroFlight>;

  @override
  void didPush(Route<dynamic> route, Route<dynamic> previousRoute) 
    assert(navigator != null);
    assert(route != null);
    _maybeStartHeroTransition(
        previousRoute, route, HeroFlightDirection.push, false);
  

  @override
  void didPop(Route<dynamic> route, Route<dynamic> previousRoute) 
    assert(navigator != null);
    assert(route != null);

    if (!navigator.userGestureInProgress)
      _maybeStartHeroTransition(
          route, previousRoute, HeroFlightDirection.pop, false);
  

  @override
  void didReplace(Route<dynamic> newRoute, Route<dynamic> oldRoute) 
    assert(navigator != null);
    if (newRoute?.isCurrent == true) 
      _maybeStartHeroTransition(
          oldRoute, newRoute, HeroFlightDirection.push, false);
    
  

  @override
  void didStartUserGesture(Route<dynamic> route, Route<dynamic> previousRoute) 
    assert(navigator != null);
    assert(route != null);
    _maybeStartHeroTransition(
        route, previousRoute, HeroFlightDirection.pop, true);
  

  @override
  void didStopUserGesture() 
    if (navigator.userGestureInProgress) return;

    bool isInvalidFlight(_HeroFlight flight) 
      return flight.manifest.isUserGestureTransition &&
          flight.manifest.type == HeroFlightDirection.pop &&
          flight._proxyAnimation.isDismissed;
    

    final List<_HeroFlight> invalidFlights =
        _flights.values.where(isInvalidFlight).toList(growable: false);

    for (final _HeroFlight flight in invalidFlights) 
      flight._handleAnimationUpdate(AnimationStatus.dismissed);
    
  

  void _maybeStartHeroTransition(
    Route<dynamic> fromRoute,
    Route<dynamic> toRoute,
    HeroFlightDirection flightType,
    bool isUserGestureTransition,
  ) 
    if (toRoute != fromRoute &&
        toRoute is PageRoute<dynamic> &&
        fromRoute is PageRoute<dynamic>) 
      final PageRoute<dynamic> from = fromRoute;
      final PageRoute<dynamic> to = toRoute;
      final Animation<double> animation =
          (flightType == HeroFlightDirection.push)
              ? to.animation
              : from.animation;

      switch (flightType) 
        case HeroFlightDirection.pop:
          if (animation.value == 0.0) 
            return;
          
          break;
        case HeroFlightDirection.push:
          if (animation.value == 1.0) 
            return;
          
          break;
      

      if (isUserGestureTransition &&
          flightType == HeroFlightDirection.pop &&
          to.maintainState) 
        _startHeroTransition(
            from, to, animation, flightType, isUserGestureTransition);
       else 
        to.offstage = to.animation.value == 0.0;

        WidgetsBinding.instance.addPostFrameCallback((Duration value) 
          _startHeroTransition(
              from, to, animation, flightType, isUserGestureTransition);
        );
      
    
  

  void _startHeroTransition(
    PageRoute<dynamic> from,
    PageRoute<dynamic> to,
    Animation<double> animation,
    HeroFlightDirection flightType,
    bool isUserGestureTransition,
  ) 
    if (navigator == null ||
        from.subtreeContext == null ||
        to.subtreeContext == null) 
      to.offstage = false;
      return;
    

    final Rect navigatorRect = _boundingBoxFor(navigator.context);

    final Map<Object, _ColorHeroState> fromHeroes = ColorHero._allHeroesFor(
        from.subtreeContext, isUserGestureTransition, navigator);
    final Map<Object, _ColorHeroState> toHeroes = ColorHero._allHeroesFor(
        to.subtreeContext, isUserGestureTransition, navigator);

    to.offstage = false;

    for (final Object tag in fromHeroes.keys) 
      if (toHeroes[tag] != null) 
        final HeroFlightShuttleBuilder fromShuttleBuilder =
            fromHeroes[tag].widget.flightShuttleBuilder;
        final HeroFlightShuttleBuilder toShuttleBuilder =
            toHeroes[tag].widget.flightShuttleBuilder;
        final bool isDiverted = _flights[tag] != null;

        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,
          isUserGestureTransition: isUserGestureTransition,
          isDiverted: isDiverted,
        );

        if (isDiverted)
          _flights[tag].divert(manifest);
        else
          _flights[tag] = _HeroFlight(_handleFlightEnded)..start(manifest);
       else if (_flights[tag] != null) 
        _flights[tag].abort();
      
    

    for (final Object tag in toHeroes.keys) 
      if (fromHeroes[tag] == null) toHeroes[tag].ensurePlaceholderIsHidden();
    
  

  void _handleFlightEnded(_HeroFlight flight) 
    _flights.remove(flight.manifest.tag);
  

  static final HeroFlightShuttleBuilder _defaultHeroFlightShuttleBuilder = (
    BuildContext flightContext,
    Animation<double> animation,
    HeroFlightDirection flightDirection,
    BuildContext fromHeroContext,
    BuildContext toHeroContext,
  ) 
    final ColorHero toHero = toHeroContext.widget as ColorHero;
    return toHero.child;
  ;


【讨论】:

这是我想要的解决方案.. 但是这么多代码!出于好奇,这些都是你写的吗?无论如何,我需要完全研究你的代码。太感谢了。无论如何都接受答案。 我在我的应用程序中使用了代码,它就像一个魅力!非常感谢您的努力。 当然..如果您有足够的评分,请随时为答案投票。

以上是关于Flutter:如何在页面转换时为背景颜色设置动画的主要内容,如果未能解决你的问题,请参考以下文章

在android中设置背景颜色时动画[重复]

Flutter:在从小部件树中删除时或在其生命周期结束时为小部件设置动画?

如何在 Flutter 的主小部件中为小部件设置背景颜色?

页面加载时自动为页面的背景颜色设置动画[重复]

在链接悬停时为 SVG 设置动画

如何在 Vue 中删除列表项时为列表项设置动画