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
,并在MaterialApp
的navigatorObservers
中添加新的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:如何在页面转换时为背景颜色设置动画的主要内容,如果未能解决你的问题,请参考以下文章