| 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 | /// @docImport 'package:flutter/material.dart'; |
| 6 | /// |
| 7 | /// @docImport 'animated_switcher.dart'; |
| 8 | /// @docImport 'implicit_animations.dart'; |
| 9 | library; |
| 10 | |
| 11 | import 'package:flutter/rendering.dart'; |
| 12 | |
| 13 | import 'animated_size.dart'; |
| 14 | import 'basic.dart'; |
| 15 | import 'focus_scope.dart'; |
| 16 | import 'framework.dart'; |
| 17 | import 'ticker_provider.dart'; |
| 18 | import 'transitions.dart'; |
| 19 | |
| 20 | // Examples can assume: |
| 21 | // bool _first = false; |
| 22 | |
| 23 | /// Specifies which of two children to show. See [AnimatedCrossFade]. |
| 24 | /// |
| 25 | /// The child that is shown will fade in, while the other will fade out. |
| 26 | enum CrossFadeState { |
| 27 | /// Show the first child ([AnimatedCrossFade.firstChild]) and hide the second |
| 28 | /// ([AnimatedCrossFade.secondChild]]). |
| 29 | showFirst, |
| 30 | |
| 31 | /// Show the second child ([AnimatedCrossFade.secondChild]) and hide the first |
| 32 | /// ([AnimatedCrossFade.firstChild]). |
| 33 | showSecond, |
| 34 | } |
| 35 | |
| 36 | /// Signature for the [AnimatedCrossFade.layoutBuilder] callback. |
| 37 | /// |
| 38 | /// The `topChild` is the child fading in, which is normally drawn on top. The |
| 39 | /// `bottomChild` is the child fading out, normally drawn on the bottom. |
| 40 | /// |
| 41 | /// For good performance, the returned widget tree should contain both the |
| 42 | /// `topChild` and the `bottomChild`; the depth of the tree, and the types of |
| 43 | /// the widgets in the tree, from the returned widget to each of the children |
| 44 | /// should be the same; and where there is a widget with multiple children, the |
| 45 | /// top child and the bottom child should be keyed using the provided |
| 46 | /// `topChildKey` and `bottomChildKey` keys respectively. |
| 47 | /// |
| 48 | /// {@tool snippet} |
| 49 | /// |
| 50 | /// ```dart |
| 51 | /// Widget defaultLayoutBuilder(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey) { |
| 52 | /// return Stack( |
| 53 | /// children: <Widget>[ |
| 54 | /// Positioned( |
| 55 | /// key: bottomChildKey, |
| 56 | /// left: 0.0, |
| 57 | /// top: 0.0, |
| 58 | /// right: 0.0, |
| 59 | /// child: bottomChild, |
| 60 | /// ), |
| 61 | /// Positioned( |
| 62 | /// key: topChildKey, |
| 63 | /// child: topChild, |
| 64 | /// ) |
| 65 | /// ], |
| 66 | /// ); |
| 67 | /// } |
| 68 | /// ``` |
| 69 | /// {@end-tool} |
| 70 | typedef AnimatedCrossFadeBuilder = |
| 71 | Widget Function(Widget topChild, Key topChildKey, Widget bottomChild, Key bottomChildKey); |
| 72 | |
| 73 | /// A widget that cross-fades between two given children and animates itself |
| 74 | /// between their sizes. |
| 75 | /// |
| 76 | /// {@youtube 560 315 https://www.youtube.com/watch?v=PGK2UUAyE54} |
| 77 | /// |
| 78 | /// The animation is controlled through the [crossFadeState] parameter. |
| 79 | /// [firstCurve] and [secondCurve] represent the opacity curves of the two |
| 80 | /// children. The [firstCurve] is inverted, i.e. it fades out when providing a |
| 81 | /// growing curve like [Curves.linear]. The [sizeCurve] is the curve used to |
| 82 | /// animate between the size of the fading-out child and the size of the |
| 83 | /// fading-in child. |
| 84 | /// |
| 85 | /// This widget is intended to be used to fade a pair of widgets with the same |
| 86 | /// width. In the case where the two children have different heights, the |
| 87 | /// animation crops overflowing children during the animation by aligning their |
| 88 | /// top edge, which means that the bottom will be clipped. |
| 89 | /// |
| 90 | /// The animation is automatically triggered when an existing |
| 91 | /// [AnimatedCrossFade] is rebuilt with a different value for the |
| 92 | /// [crossFadeState] property. |
| 93 | /// |
| 94 | /// {@tool snippet} |
| 95 | /// |
| 96 | /// This code fades between two representations of the Flutter logo. It depends |
| 97 | /// on a boolean field `_first`; when `_first` is true, the first logo is shown, |
| 98 | /// otherwise the second logo is shown. When the field changes state, the |
| 99 | /// [AnimatedCrossFade] widget cross-fades between the two forms of the logo |
| 100 | /// over three seconds. |
| 101 | /// |
| 102 | /// ```dart |
| 103 | /// AnimatedCrossFade( |
| 104 | /// duration: const Duration(seconds: 3), |
| 105 | /// firstChild: const FlutterLogo(style: FlutterLogoStyle.horizontal, size: 100.0), |
| 106 | /// secondChild: const FlutterLogo(style: FlutterLogoStyle.stacked, size: 100.0), |
| 107 | /// crossFadeState: _first ? CrossFadeState.showFirst : CrossFadeState.showSecond, |
| 108 | /// ) |
| 109 | /// ``` |
| 110 | /// {@end-tool} |
| 111 | /// |
| 112 | /// See also: |
| 113 | /// |
| 114 | /// * [AnimatedOpacity], which fades between nothing and a single child. |
| 115 | /// * [AnimatedSwitcher], which switches out a child for a new one with a |
| 116 | /// customizable transition, supporting multiple cross-fades at once. |
| 117 | /// * [AnimatedSize], the lower-level widget which [AnimatedCrossFade] uses to |
| 118 | /// automatically change size. |
| 119 | class AnimatedCrossFade extends StatefulWidget { |
| 120 | /// Creates a cross-fade animation widget. |
| 121 | /// |
| 122 | /// The [duration] of the animation is the same for all components (fade in, |
| 123 | /// fade out, and size), and you can pass [Interval]s instead of [Curve]s in |
| 124 | /// order to have finer control, e.g., creating an overlap between the fades. |
| 125 | const AnimatedCrossFade({ |
| 126 | super.key, |
| 127 | required this.firstChild, |
| 128 | required this.secondChild, |
| 129 | this.firstCurve = Curves.linear, |
| 130 | this.secondCurve = Curves.linear, |
| 131 | this.sizeCurve = Curves.linear, |
| 132 | this.alignment = Alignment.topCenter, |
| 133 | required this.crossFadeState, |
| 134 | required this.duration, |
| 135 | this.reverseDuration, |
| 136 | this.layoutBuilder = defaultLayoutBuilder, |
| 137 | this.excludeBottomFocus = true, |
| 138 | }); |
| 139 | |
| 140 | /// The child that is visible when [crossFadeState] is |
| 141 | /// [CrossFadeState.showFirst]. It fades out when transitioning |
| 142 | /// [crossFadeState] from [CrossFadeState.showFirst] to |
| 143 | /// [CrossFadeState.showSecond] and vice versa. |
| 144 | final Widget firstChild; |
| 145 | |
| 146 | /// The child that is visible when [crossFadeState] is |
| 147 | /// [CrossFadeState.showSecond]. It fades in when transitioning |
| 148 | /// [crossFadeState] from [CrossFadeState.showFirst] to |
| 149 | /// [CrossFadeState.showSecond] and vice versa. |
| 150 | final Widget secondChild; |
| 151 | |
| 152 | /// The child that will be shown when the animation has completed. |
| 153 | final CrossFadeState crossFadeState; |
| 154 | |
| 155 | /// The duration of the whole orchestrated animation. |
| 156 | final Duration duration; |
| 157 | |
| 158 | /// The duration of the whole orchestrated animation when running in reverse. |
| 159 | /// |
| 160 | /// If not supplied, this defaults to [duration]. |
| 161 | final Duration? reverseDuration; |
| 162 | |
| 163 | /// The fade curve of the first child. |
| 164 | /// |
| 165 | /// Defaults to [Curves.linear]. |
| 166 | final Curve firstCurve; |
| 167 | |
| 168 | /// The fade curve of the second child. |
| 169 | /// |
| 170 | /// Defaults to [Curves.linear]. |
| 171 | final Curve secondCurve; |
| 172 | |
| 173 | /// The curve of the animation between the two children's sizes. |
| 174 | /// |
| 175 | /// Defaults to [Curves.linear]. |
| 176 | final Curve sizeCurve; |
| 177 | |
| 178 | /// How the children should be aligned while the size is animating. |
| 179 | /// |
| 180 | /// Defaults to [Alignment.topCenter]. |
| 181 | /// |
| 182 | /// See also: |
| 183 | /// |
| 184 | /// * [Alignment], a class with convenient constants typically used to |
| 185 | /// specify an [AlignmentGeometry]. |
| 186 | /// * [AlignmentDirectional], like [Alignment] for specifying alignments |
| 187 | /// relative to text direction. |
| 188 | final AlignmentGeometry alignment; |
| 189 | |
| 190 | /// A builder that positions the [firstChild] and [secondChild] widgets. |
| 191 | /// |
| 192 | /// The widget returned by this method is wrapped in an [AnimatedSize]. |
| 193 | /// |
| 194 | /// By default, this uses [AnimatedCrossFade.defaultLayoutBuilder], which uses |
| 195 | /// a [Stack] and aligns the `bottomChild` to the top of the stack while |
| 196 | /// providing the `topChild` as the non-positioned child to fill the provided |
| 197 | /// constraints. This works well when the [AnimatedCrossFade] is in a position |
| 198 | /// to change size and when the children are not flexible. However, if the |
| 199 | /// children are less fussy about their sizes (for example a |
| 200 | /// [CircularProgressIndicator] inside a [Center]), or if the |
| 201 | /// [AnimatedCrossFade] is being forced to a particular size, then it can |
| 202 | /// result in the widgets jumping about when the cross-fade state is changed. |
| 203 | final AnimatedCrossFadeBuilder layoutBuilder; |
| 204 | |
| 205 | /// When true, this is equivalent to wrapping the bottom widget with an [ExcludeFocus] |
| 206 | /// widget while it is at the bottom of the cross-fade stack. |
| 207 | /// |
| 208 | /// Defaults to true. When it is false, the bottom widget in the cross-fade stack |
| 209 | /// can remain in focus until the top widget requests focus. This is useful for |
| 210 | /// animating between different [TextField]s so the keyboard remains open during the |
| 211 | /// cross-fade animation. |
| 212 | final bool excludeBottomFocus; |
| 213 | |
| 214 | /// The default layout algorithm used by [AnimatedCrossFade]. |
| 215 | /// |
| 216 | /// The top child is placed in a stack that sizes itself to match the top |
| 217 | /// child. The bottom child is positioned at the top of the same stack, sized |
| 218 | /// to fit its width but without forcing the height. The stack is then |
| 219 | /// clipped. |
| 220 | /// |
| 221 | /// This is the default value for [layoutBuilder]. It implements |
| 222 | /// [AnimatedCrossFadeBuilder]. |
| 223 | static Widget defaultLayoutBuilder( |
| 224 | Widget topChild, |
| 225 | Key topChildKey, |
| 226 | Widget bottomChild, |
| 227 | Key bottomChildKey, |
| 228 | ) { |
| 229 | return Stack( |
| 230 | clipBehavior: Clip.none, |
| 231 | children: <Widget>[ |
| 232 | Positioned(key: bottomChildKey, left: 0.0, top: 0.0, right: 0.0, child: bottomChild), |
| 233 | Positioned(key: topChildKey, child: topChild), |
| 234 | ], |
| 235 | ); |
| 236 | } |
| 237 | |
| 238 | @override |
| 239 | State<AnimatedCrossFade> createState() => _AnimatedCrossFadeState(); |
| 240 | |
| 241 | @override |
| 242 | void debugFillProperties(DiagnosticPropertiesBuilder properties) { |
| 243 | super.debugFillProperties(properties); |
| 244 | properties.add(EnumProperty<CrossFadeState>('crossFadeState' , crossFadeState)); |
| 245 | properties.add( |
| 246 | DiagnosticsProperty<AlignmentGeometry>( |
| 247 | 'alignment' , |
| 248 | alignment, |
| 249 | defaultValue: Alignment.topCenter, |
| 250 | ), |
| 251 | ); |
| 252 | properties.add(IntProperty('duration' , duration.inMilliseconds, unit: 'ms' )); |
| 253 | properties.add( |
| 254 | IntProperty( |
| 255 | 'reverseDuration' , |
| 256 | reverseDuration?.inMilliseconds, |
| 257 | unit: 'ms' , |
| 258 | defaultValue: null, |
| 259 | ), |
| 260 | ); |
| 261 | } |
| 262 | } |
| 263 | |
| 264 | class _AnimatedCrossFadeState extends State<AnimatedCrossFade> with TickerProviderStateMixin { |
| 265 | late AnimationController _controller; |
| 266 | late Animation<double> _firstAnimation; |
| 267 | late Animation<double> _secondAnimation; |
| 268 | |
| 269 | @override |
| 270 | void initState() { |
| 271 | super.initState(); |
| 272 | _controller = AnimationController( |
| 273 | duration: widget.duration, |
| 274 | reverseDuration: widget.reverseDuration, |
| 275 | vsync: this, |
| 276 | ); |
| 277 | if (widget.crossFadeState == CrossFadeState.showSecond) { |
| 278 | _controller.value = 1.0; |
| 279 | } |
| 280 | _firstAnimation = _initAnimation(widget.firstCurve, true); |
| 281 | _secondAnimation = _initAnimation(widget.secondCurve, false); |
| 282 | _controller.addStatusListener((AnimationStatus status) { |
| 283 | setState(() { |
| 284 | // Trigger a rebuild because it depends on _isTransitioning, which |
| 285 | // changes its value together with animation status. |
| 286 | }); |
| 287 | }); |
| 288 | } |
| 289 | |
| 290 | Animation<double> _initAnimation(Curve curve, bool inverted) { |
| 291 | Animation<double> result = _controller.drive(CurveTween(curve: curve)); |
| 292 | if (inverted) { |
| 293 | result = result.drive(Tween<double>(begin: 1.0, end: 0.0)); |
| 294 | } |
| 295 | return result; |
| 296 | } |
| 297 | |
| 298 | @override |
| 299 | void dispose() { |
| 300 | _controller.dispose(); |
| 301 | super.dispose(); |
| 302 | } |
| 303 | |
| 304 | @override |
| 305 | void didUpdateWidget(AnimatedCrossFade oldWidget) { |
| 306 | super.didUpdateWidget(oldWidget); |
| 307 | if (widget.duration != oldWidget.duration) { |
| 308 | _controller.duration = widget.duration; |
| 309 | } |
| 310 | if (widget.reverseDuration != oldWidget.reverseDuration) { |
| 311 | _controller.reverseDuration = widget.reverseDuration; |
| 312 | } |
| 313 | if (widget.firstCurve != oldWidget.firstCurve) { |
| 314 | _firstAnimation = _initAnimation(widget.firstCurve, true); |
| 315 | } |
| 316 | if (widget.secondCurve != oldWidget.secondCurve) { |
| 317 | _secondAnimation = _initAnimation(widget.secondCurve, false); |
| 318 | } |
| 319 | if (widget.crossFadeState != oldWidget.crossFadeState) { |
| 320 | switch (widget.crossFadeState) { |
| 321 | case CrossFadeState.showFirst: |
| 322 | _controller.reverse(); |
| 323 | case CrossFadeState.showSecond: |
| 324 | _controller.forward(); |
| 325 | } |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | @override |
| 330 | Widget build(BuildContext context) { |
| 331 | const Key kFirstChildKey = ValueKey<CrossFadeState>(CrossFadeState.showFirst); |
| 332 | const Key kSecondChildKey = ValueKey<CrossFadeState>(CrossFadeState.showSecond); |
| 333 | final Key topKey; |
| 334 | Widget topChild; |
| 335 | final Animation<double> topAnimation; |
| 336 | final Key bottomKey; |
| 337 | Widget bottomChild; |
| 338 | final Animation<double> bottomAnimation; |
| 339 | if (_controller.isForwardOrCompleted) { |
| 340 | topKey = kSecondChildKey; |
| 341 | topChild = widget.secondChild; |
| 342 | topAnimation = _secondAnimation; |
| 343 | bottomKey = kFirstChildKey; |
| 344 | bottomChild = widget.firstChild; |
| 345 | bottomAnimation = _firstAnimation; |
| 346 | } else { |
| 347 | topKey = kFirstChildKey; |
| 348 | topChild = widget.firstChild; |
| 349 | topAnimation = _firstAnimation; |
| 350 | bottomKey = kSecondChildKey; |
| 351 | bottomChild = widget.secondChild; |
| 352 | bottomAnimation = _secondAnimation; |
| 353 | } |
| 354 | |
| 355 | bottomChild = TickerMode( |
| 356 | key: bottomKey, |
| 357 | enabled: _controller.isAnimating, |
| 358 | child: IgnorePointer( |
| 359 | child: ExcludeSemantics( |
| 360 | // Always exclude the semantics of the widget that's fading out. |
| 361 | child: ExcludeFocus( |
| 362 | excluding: widget.excludeBottomFocus, |
| 363 | child: FadeTransition(opacity: bottomAnimation, child: bottomChild), |
| 364 | ), |
| 365 | ), |
| 366 | ), |
| 367 | ); |
| 368 | topChild = TickerMode( |
| 369 | key: topKey, |
| 370 | enabled: true, // Top widget always has its animations enabled. |
| 371 | child: IgnorePointer( |
| 372 | ignoring: false, |
| 373 | child: ExcludeSemantics( |
| 374 | excluding: false, // Always publish semantics for the widget that's fading in. |
| 375 | child: ExcludeFocus( |
| 376 | excluding: false, |
| 377 | child: FadeTransition(opacity: topAnimation, child: topChild), |
| 378 | ), |
| 379 | ), |
| 380 | ), |
| 381 | ); |
| 382 | return ClipRect( |
| 383 | child: AnimatedSize( |
| 384 | alignment: widget.alignment, |
| 385 | duration: widget.duration, |
| 386 | reverseDuration: widget.reverseDuration, |
| 387 | curve: widget.sizeCurve, |
| 388 | child: widget.layoutBuilder(topChild, topKey, bottomChild, bottomKey), |
| 389 | ), |
| 390 | ); |
| 391 | } |
| 392 | |
| 393 | @override |
| 394 | void debugFillProperties(DiagnosticPropertiesBuilder description) { |
| 395 | super.debugFillProperties(description); |
| 396 | description.add(EnumProperty<CrossFadeState>('crossFadeState' , widget.crossFadeState)); |
| 397 | description.add( |
| 398 | DiagnosticsProperty<AnimationController>('controller' , _controller, showName: false), |
| 399 | ); |
| 400 | description.add( |
| 401 | DiagnosticsProperty<AlignmentGeometry>( |
| 402 | 'alignment' , |
| 403 | widget.alignment, |
| 404 | defaultValue: Alignment.topCenter, |
| 405 | ), |
| 406 | ); |
| 407 | } |
| 408 | } |
| 409 | |