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 = 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.
33class 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
90class _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