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