1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'basic.dart';
6import 'framework.dart';
7
8/// Builder callback used by [DualTransitionBuilder].
9///
10/// The builder is expected to return a transition powered by the provided
11/// `animation` and wrapping the provided `child`.
12///
13/// The `animation` provided to the builder always runs forward from 0.0 to 1.0.
14typedef AnimatedTransitionBuilder =
15 Widget Function(BuildContext context, Animation<double> animation, Widget? child);
16
17/// A transition builder that animates its [child] based on the
18/// [AnimationStatus] of the provided [animation].
19///
20/// This widget can be used to specify different enter and exit transitions for
21/// a [child].
22///
23/// While the [animation] runs forward, the [child] is animated according to
24/// [forwardBuilder] and while the [animation] is running in reverse, it is
25/// animated according to [reverseBuilder].
26///
27/// Using this builder allows the widget tree to maintain its shape by nesting
28/// the enter and exit transitions. This ensures that no state information of
29/// any descendant widget is lost when the transition starts or completes.
30class DualTransitionBuilder extends StatefulWidget {
31 /// Creates a [DualTransitionBuilder].
32 const DualTransitionBuilder({
33 super.key,
34 required this.animation,
35 required this.forwardBuilder,
36 required this.reverseBuilder,
37 this.child,
38 });
39
40 /// The animation that drives the [child]'s transition.
41 ///
42 /// When this animation runs forward, the [child] transitions as specified by
43 /// [forwardBuilder]. When it runs in reverse, the child transitions according
44 /// to [reverseBuilder].
45 final Animation<double> animation;
46
47 /// A builder for the transition that makes [child] appear on screen.
48 ///
49 /// The [child] should be fully visible when the provided `animation` reaches
50 /// 1.0.
51 ///
52 /// The `animation` provided to this builder is running forward from 0.0 to
53 /// 1.0 when [animation] runs _forward_. When [animation] runs in reverse,
54 /// the given `animation` is set to [kAlwaysCompleteAnimation].
55 ///
56 /// See also:
57 ///
58 /// * [reverseBuilder], which builds the transition for making the [child]
59 /// disappear from the screen.
60 final AnimatedTransitionBuilder forwardBuilder;
61
62 /// A builder for a transition that makes [child] disappear from the screen.
63 ///
64 /// The [child] should be fully invisible when the provided `animation`
65 /// reaches 1.0.
66 ///
67 /// The `animation` provided to this builder is running forward from 0.0 to
68 /// 1.0 when [animation] runs in _reverse_. When [animation] runs forward,
69 /// the given `animation` is set to [kAlwaysDismissedAnimation].
70 ///
71 /// See also:
72 ///
73 /// * [forwardBuilder], which builds the transition for making the [child]
74 /// appear on screen.
75 final AnimatedTransitionBuilder reverseBuilder;
76
77 /// The widget below this [DualTransitionBuilder] in the tree.
78 ///
79 /// This child widget will be wrapped by the transitions built by
80 /// [forwardBuilder] and [reverseBuilder].
81 final Widget? child;
82
83 @override
84 State<DualTransitionBuilder> createState() => _DualTransitionBuilderState();
85}
86
87class _DualTransitionBuilderState extends State<DualTransitionBuilder> {
88 late AnimationStatus _effectiveAnimationStatus;
89 final ProxyAnimation _forwardAnimation = ProxyAnimation();
90 final ProxyAnimation _reverseAnimation = ProxyAnimation();
91
92 @override
93 void initState() {
94 super.initState();
95 _effectiveAnimationStatus = widget.animation.status;
96 widget.animation.addStatusListener(_animationListener);
97 _updateAnimations();
98 }
99
100 void _animationListener(AnimationStatus animationStatus) {
101 final AnimationStatus oldEffective = _effectiveAnimationStatus;
102 _effectiveAnimationStatus = _calculateEffectiveAnimationStatus(
103 lastEffective: _effectiveAnimationStatus,
104 current: animationStatus,
105 );
106 if (oldEffective != _effectiveAnimationStatus) {
107 _updateAnimations();
108 }
109 }
110
111 @override
112 void didUpdateWidget(DualTransitionBuilder oldWidget) {
113 super.didUpdateWidget(oldWidget);
114 if (oldWidget.animation != widget.animation) {
115 oldWidget.animation.removeStatusListener(_animationListener);
116 widget.animation.addStatusListener(_animationListener);
117 _animationListener(widget.animation.status);
118 }
119 }
120
121 // When a transition is interrupted midway we just want to play the ongoing
122 // animation in reverse. Switching to the actual reverse transition would
123 // yield a disjoint experience since the forward and reverse transitions are
124 // very different.
125 AnimationStatus _calculateEffectiveAnimationStatus({
126 required AnimationStatus lastEffective,
127 required AnimationStatus current,
128 }) {
129 switch (current) {
130 case AnimationStatus.dismissed:
131 case AnimationStatus.completed:
132 return current;
133 case AnimationStatus.forward:
134 switch (lastEffective) {
135 case AnimationStatus.dismissed:
136 case AnimationStatus.completed:
137 case AnimationStatus.forward:
138 return current;
139 case AnimationStatus.reverse:
140 return lastEffective;
141 }
142 case AnimationStatus.reverse:
143 switch (lastEffective) {
144 case AnimationStatus.dismissed:
145 case AnimationStatus.completed:
146 case AnimationStatus.reverse:
147 return current;
148 case AnimationStatus.forward:
149 return lastEffective;
150 }
151 }
152 }
153
154 void _updateAnimations() {
155 switch (_effectiveAnimationStatus) {
156 case AnimationStatus.dismissed:
157 case AnimationStatus.forward:
158 _forwardAnimation.parent = widget.animation;
159 _reverseAnimation.parent = kAlwaysDismissedAnimation;
160 case AnimationStatus.reverse:
161 case AnimationStatus.completed:
162 _forwardAnimation.parent = kAlwaysCompleteAnimation;
163 _reverseAnimation.parent = ReverseAnimation(widget.animation);
164 }
165 }
166
167 @override
168 void dispose() {
169 widget.animation.removeStatusListener(_animationListener);
170 super.dispose();
171 }
172
173 @override
174 Widget build(BuildContext context) {
175 return widget.forwardBuilder(
176 context,
177 _forwardAnimation,
178 widget.reverseBuilder(context, _reverseAnimation, widget.child),
179 );
180 }
181}
182