transitions.dart 17.1 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

Hixie's avatar
Hixie committed
5 6
import 'dart:math' as math;

7
import 'package:flutter/foundation.dart';
Hixie's avatar
Hixie committed
8
import 'package:vector_math/vector_math_64.dart' show Matrix4;
9

10
import 'basic.dart';
11
import 'container.dart';
12 13
import 'framework.dart';

14
export 'package:flutter/rendering.dart' show RelativeRect;
15

16
/// A widget that rebuilds when the given [Listenable] changes value.
17
///
18
/// [AnimatedWidget] is most commonly used with [Animation] objects, which are
19 20 21 22 23 24
/// [Listenable], but it can be used with any [Listenable], including
/// [ChangeNotifier] and [ValueNotifier].
///
/// [AnimatedWidget] is most useful for widgets widgets that are otherwise
/// stateless. To use [AnimatedWidget], simply subclass it and implement the
/// build function.
25 26 27
///
/// For more complex case involving additional state, consider using
/// [AnimatedBuilder].
28 29 30 31 32 33 34 35
///
/// See also:
///
///  * [AnimatedBuilder], which is useful for more complex use cases.
///  * [Animation], which is a [Listenable] object that can be used for
///    [listenable].
///  * [ChangeNotifier], which is another [Listenable] object that can be used
///    for [listenable].
36
abstract class AnimatedWidget extends StatefulWidget {
37
  /// Creates a widget that rebuilds when the given listenable changes.
38
  ///
39
  /// The [listenable] argument is required.
40
  const AnimatedWidget({
41
    Key key,
42
    @required this.listenable
43 44
  }) : assert(listenable != null),
       super(key: key);
45

46 47 48 49
  /// The [Listenable] to which this widget is listening.
  ///
  /// Commonly an [Animation] or a [ChangeNotifier].
  final Listenable listenable;
50

51 52
  /// Override this method to build widgets that depend on the state of the
  /// listenable (e.g., the current value of the animation).
53
  @protected
54 55
  Widget build(BuildContext context);

56
  /// Subclasses typically do not override this method.
57
  @override
58
  _AnimatedState createState() => new _AnimatedState();
59

60
  @override
61
  void debugFillProperties(DiagnosticPropertiesBuilder description) {
62 63
    super.debugFillProperties(description);
    description.add(new DiagnosticsProperty<Listenable>('animation', listenable));
64 65 66
  }
}

67
class _AnimatedState extends State<AnimatedWidget> {
68
  @override
69 70
  void initState() {
    super.initState();
71
    widget.listenable.addListener(_handleChange);
72 73
  }

74
  @override
75
  void didUpdateWidget(AnimatedWidget oldWidget) {
76
    super.didUpdateWidget(oldWidget);
77 78 79
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
80 81 82
    }
  }

83
  @override
84
  void dispose() {
85
    widget.listenable.removeListener(_handleChange);
86 87 88
    super.dispose();
  }

89
  void _handleChange() {
90
    setState(() {
91
      // The listenable's state is our build state, and it changed already.
92 93 94
    });
  }

95
  @override
96
  Widget build(BuildContext context) => widget.build(context);
97 98
}

99
/// Animates the position of a widget relative to its normal position.
100
class SlideTransition extends AnimatedWidget {
101 102
  /// Creates a fractional translation transition.
  ///
103
  /// The [position] argument must not be null.
104
  const SlideTransition({
105
    Key key,
106
    @required Animation<FractionalOffsetGeometry> position,
107
    this.transformHitTests: true,
108
    this.child,
109
  }) : super(key: key, listenable: position);
110

111 112 113 114
  /// The animation that controls the position of the child.
  ///
  /// If the current value of the position animation is (dx, dy), the child will
  /// be translated horizontally by width * dx and vertically by height * dy.
115
  Animation<FractionalOffsetGeometry> get position => listenable;
116 117 118 119 120 121 122

  /// Whether hit testing should be affected by the slide animation.
  ///
  /// If false, hit testing will proceed as if the child was not translated at
  /// all. Setting this value to false is useful for fast animations where you
  /// expect the user to commonly interact with the child widget in its final
  /// location and you want the user to benefit from "muscle memory".
123
  final bool transformHitTests;
124

125
  /// The widget below this widget in the tree.
126
  final Widget child;
127

128
  @override
129 130 131 132
  Widget build(BuildContext context) {
    return new FractionalTranslation(
      translation: position.value,
      transformHitTests: transformHitTests,
133
      child: child,
134
    );
135 136 137
  }
}

Hans Muller's avatar
Hans Muller committed
138
/// Animates the scale of transformed widget.
139
class ScaleTransition extends AnimatedWidget {
140 141
  /// Creates a scale transition.
  ///
142 143
  /// The [scale] argument must not be null. The [alignment] argument defaults
  /// to [FractionalOffset.center].
144
  const ScaleTransition({
145
    Key key,
146
    @required Animation<double> scale,
147
    this.alignment: FractionalOffset.center,
148
    this.child,
149
  }) : super(key: key, listenable: scale);
150

151 152 153 154
  /// The animation that controls the scale of the child.
  ///
  /// If the current value of the scale animation is v, the child will be
  /// painted v times its normal size.
155
  Animation<double> get scale => listenable;
156 157 158 159 160 161

  /// The alignment of the origin of the coordainte system in which the scale
  /// takes place, relative to the size of the box.
  ///
  /// For example, to set the origin of the scale to bottom middle, you can use
  /// an alignment of (0.5, 1.0).
162
  final FractionalOffset alignment;
163

164
  /// The widget below this widget in the tree.
165
  final Widget child;
166

167
  @override
168
  Widget build(BuildContext context) {
169
    final double scaleValue = scale.value;
170
    final Matrix4 transform = new Matrix4.identity()
171
      ..scale(scaleValue, scaleValue, 1.0);
172 173 174
    return new Transform(
      transform: transform,
      alignment: alignment,
175
      child: child,
176 177 178 179
    );
  }
}

180
/// Animates the rotation of a widget.
181
class RotationTransition extends AnimatedWidget {
182 183
  /// Creates a rotation transition.
  ///
184
  /// The [turns] argument must not be null.
185
  const RotationTransition({
Hixie's avatar
Hixie committed
186
    Key key,
187
    @required Animation<double> turns,
188
    this.child,
189
  }) : super(key: key, listenable: turns);
Hixie's avatar
Hixie committed
190

191 192 193 194
  /// The animation that controls the rotation of the child.
  ///
  /// If the current value of the turns animation is v, the child will be
  /// rotated v * 2 * pi radians before being painted.
195
  Animation<double> get turns => listenable;
196

197
  /// The widget below this widget in the tree.
198
  final Widget child;
Hixie's avatar
Hixie committed
199

200
  @override
201
  Widget build(BuildContext context) {
202 203
    final double turnsValue = turns.value;
    final Matrix4 transform = new Matrix4.rotationZ(turnsValue * math.PI * 2.0);
Hixie's avatar
Hixie committed
204 205
    return new Transform(
      transform: transform,
206
      alignment: FractionalOffset.center,
207
      child: child,
Hixie's avatar
Hixie committed
208 209 210 211
    );
  }
}

212
/// Animates its own size and clips and aligns the child.
Ian Hickson's avatar
Ian Hickson committed
213 214 215
///
/// For a widget that automatically animates between the sizes of two children,
/// fading between them, see [AnimatedCrossFade].
216
class SizeTransition extends AnimatedWidget {
217 218
  /// Creates a size transition.
  ///
219 220
  /// The [sizeFactor] argument must not be null. The [axis] argument defaults
  /// to [Axis.vertical]. The [axisAlignment] defaults to 0.5, which centers the
221
  /// child along the main axis during the transition.
222
  const SizeTransition({
Hans Muller's avatar
Hans Muller committed
223 224
    Key key,
    this.axis: Axis.vertical,
225
    @required Animation<double> sizeFactor,
226
    this.axisAlignment: 0.5,
227
    this.child,
228 229
  }) : assert(axis != null),
       super(key: key, listenable: sizeFactor);
Hans Muller's avatar
Hans Muller committed
230 231 232 233 234 235 236

  /// [Axis.horizontal] if [sizeFactor] modifies the width, otherwise [Axis.vertical].
  final Axis axis;

  /// The animation that controls the (clipped) size of the child. If the current value
  /// of sizeFactor is v then the width or height of the widget will be its intrinsic
  /// width or height multiplied by v.
237
  Animation<double> get sizeFactor => listenable;
Hans Muller's avatar
Hans Muller committed
238

239 240
  /// How to align the child along the axis that sizeFactor is modifying.
  final double axisAlignment;
Hans Muller's avatar
Hans Muller committed
241

242
  /// The widget below this widget in the tree.
Hans Muller's avatar
Hans Muller committed
243 244
  final Widget child;

245
  @override
Hans Muller's avatar
Hans Muller committed
246
  Widget build(BuildContext context) {
247
    FractionalOffsetDirectional alignment;
248
    if (axis == Axis.vertical)
249
      alignment = new FractionalOffsetDirectional(0.0, axisAlignment);
250
    else
251
      alignment = new FractionalOffsetDirectional(axisAlignment, 0.0);
Hans Muller's avatar
Hans Muller committed
252 253 254 255 256
    return new ClipRect(
      child: new Align(
        alignment: alignment,
        heightFactor: axis == Axis.vertical ? sizeFactor.value : null,
        widthFactor: axis == Axis.horizontal ? sizeFactor.value : null,
257
        child: child,
Hans Muller's avatar
Hans Muller committed
258 259 260 261 262
      )
    );
  }
}

263
/// Animates the opacity of a widget.
Ian Hickson's avatar
Ian Hickson committed
264 265 266
///
/// For a widget that automatically animates between the sizes of two children,
/// fading between them, see [AnimatedCrossFade].
267
class FadeTransition extends AnimatedWidget {
268 269
  /// Creates an opacity transition.
  ///
270
  /// The [opacity] argument must not be null.
271
  const FadeTransition({
272
    Key key,
273
    @required Animation<double> opacity,
274
    this.child,
275
  }) : super(key: key, listenable: opacity);
276

277 278 279 280 281 282
  /// The animation that controls the opacity of the child.
  ///
  /// If the current value of the opacity animation is v, the child will be
  /// painted with an opacity of v. For example, if v is 0.5, the child will be
  /// blended 50% with its background. Similarly, if v is 0.0, the child will be
  /// completely transparent.
283
  Animation<double> get opacity => listenable;
284

285
  /// The widget below this widget in the tree.
286 287
  final Widget child;

288
  @override
289 290 291 292 293
  Widget build(BuildContext context) {
    return new Opacity(opacity: opacity.value, child: child);
  }
}

294
/// An interpolation between two relative rects.
295
///
296
/// This class specializes the interpolation of [Tween<RelativeRect>] to
297
/// use [RelativeRect.lerp].
298 299
///
/// See [Tween] for a discussion on how to use interpolation objects.
300
class RelativeRectTween extends Tween<RelativeRect> {
301
  /// Creates a [RelativeRect] tween.
302
  ///
303 304
  /// The [begin] and [end] properties may be null; the null value
  /// is treated as [RelativeRect.fill].
305 306
  RelativeRectTween({ RelativeRect begin, RelativeRect end })
    : super(begin: begin, end: end);
307

308
  /// Returns the value this variable has at the given animation clock value.
309
  @override
310 311 312
  RelativeRect lerp(double t) => RelativeRect.lerp(begin, end, t);
}

313
/// Animated version of [Positioned] which takes a specific
314 315
/// [Animation<RelativeRect>] to transition the child's position from a start
/// position to and end position over the lifetime of the animation.
316
///
317
/// Only works if it's the child of a [Stack].
318 319 320
///
/// See also:
///
321
/// * [RelativePositionedTransition].
322
class PositionedTransition extends AnimatedWidget {
323 324
  /// Creates a transition for [Positioned].
  ///
325
  /// The [rect] argument must not be null.
326
  const PositionedTransition({
327
    Key key,
328
    @required Animation<RelativeRect> rect,
329
    @required this.child,
330
  }) : super(key: key, listenable: rect);
331

332
  /// The animation that controls the child's size and position.
333
  Animation<RelativeRect> get rect => listenable;
334

335
  /// The widget below this widget in the tree.
336
  final Widget child;
337

338
  @override
339
  Widget build(BuildContext context) {
340 341
    return new Positioned.fromRelativeRect(
      rect: rect.value,
342
      child: child,
343 344 345 346
    );
  }
}

347 348 349 350 351 352 353 354
/// Animated version of [Positioned] which transitions the child's position
/// based on the value of [rect] relative to a bounding box with the
/// specified [size].
///
/// Only works if it's the child of a [Stack].
///
/// See also:
///
355
/// * [PositionedTransition].
356
class RelativePositionedTransition extends AnimatedWidget {
357 358 359 360
  /// Create an animated version of [Positioned].
  ///
  /// Each frame, the [Positioned] widget will be configured to represent the
  /// current value of the [rect] argument assuming that the stack has the given
361
  /// [size]. Both [rect] and [size] must not be null.
362
  const RelativePositionedTransition({
363 364 365
    Key key,
    @required Animation<Rect> rect,
    @required this.size,
366
    @required this.child,
367
  }) : super(key: key, listenable: rect);
368 369

  /// The animation that controls the child's size and position.
370 371
  ///
  /// See also [size].
372
  Animation<Rect> get rect => listenable;
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388

  /// The [Positioned] widget's offsets are relative to a box of this
  /// size whose origin is 0,0.
  final Size size;

  /// The widget below this widget in the tree.
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final RelativeRect offsets = new RelativeRect.fromSize(rect.value, size);
    return new Positioned(
      top: offsets.top,
      right: offsets.right,
      bottom: offsets.bottom,
      left: offsets.left,
389
      child: child,
390 391 392 393
    );
  }
}

394 395
/// Animated version of a [DecoratedBox] that animates the different properties
/// of its [Decoration].
396
///
397 398 399
/// See also:
///
/// * [DecoratedBox], which also draws a [Decoration] but is not animated.
400
/// * [AnimatedContainer], a more full-featured container that also animates on
401 402
///   decoration using an internal animation.
class DecoratedBoxTransition extends AnimatedWidget {
403
  /// Creates an animated [DecoratedBox] whose [Decoration] animation updates
404 405
  /// the widget.
  ///
406
  /// The [decoration] and [position] must not be null.
407
  ///
408
  /// See also:
409
  ///
410
  /// * [new DecoratedBox].
411
  const DecoratedBoxTransition({
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
    Key key,
    @required this.decoration,
    this.position: DecorationPosition.background,
    @required this.child,
  }) : super(key: key, listenable: decoration);

  /// Animation of the decoration to paint.
  ///
  /// Can be created using a [DecorationTween] interpolating typically between
  /// two [BoxDecoration].
  final Animation<Decoration> decoration;

  /// Whether to paint the box decoration behind or in front of the child.
  final DecorationPosition position;

  /// The widget below this widget in the tree.
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return new DecoratedBox(
      decoration: decoration.value,
      position: position,
      child: child,
    );
  }
}

440
/// A builder that builds a widget given a child.
441 442 443 444
///
/// The child should typically be part of the returned widget tree.
///
/// Used by [AnimatedBuilder.builder].
445 446
typedef Widget TransitionBuilder(BuildContext context, Widget child);

447 448
/// A general-purpose widget for building animations.
///
449
/// AnimatedBuilder is useful for more complex widgets that wish to include
450 451 452
/// an animation as part of a larger build function. To use AnimatedBuilder,
/// simply construct the widget and pass it a builder function.
///
453 454 455 456 457
/// For simple cases without additional state, consider using
/// [AnimatedWidget].
///
/// ## Performance optimisations
///
458 459 460 461 462 463 464 465 466 467 468
/// If your [builder] function contains a subtree that does not depend on the
/// animation, it's more efficient to build that subtree once instead of
/// rebuilding it on every animation tick.
///
/// If you pass the pre-built subtree as the [child] parameter, the
/// AnimatedBuilder will pass it back to your builder function so that you
/// can incorporate it into your build.
///
/// Using this pre-built child is entirely optional, but can improve
/// performance significantly in some cases and is therefore a good practice.
///
469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513
/// ## Sample code
///
/// This code defines a widget called `Spinner` that spins a green square
/// continually. It is built with an [AnimatedBuilder] and makes use of the
/// [child] feature to avoid having to rebuild the [Container] each time.
///
/// ```dart
/// class Spinner extends StatefulWidget {
///   @override
///   _SpinnerState createState() => new _SpinnerState();
/// }
///
/// class _SpinnerState extends State<Spinner> with SingleTickerProviderStateMixin {
///   AnimationController _controller;
///
///   @override
///   void initState() {
///     super.initState();
///     _controller = new AnimationController(
///       duration: const Duration(seconds: 10),
///       vsync: this,
///     )..repeat();
///   }
///
///   @override
///   void dispose() {
///     _controller.dispose();
///     super.dispose();
///   }
///
///   @override
///   Widget build(BuildContext context) {
///     return new AnimatedBuilder(
///       animation: _controller,
///       child: new Container(width: 200.0, height: 200.0, color: Colors.green),
///       builder: (BuildContext context, Widget child) {
///         return new Transform.rotate(
///           angle: _controller.value * 2.0 * math.PI,
///           child: child,
///         );
///       },
///     );
///   }
/// }
/// ```
514
class AnimatedBuilder extends AnimatedWidget {
515 516
  /// Creates an animated builder.
  ///
517
  /// The [animation] and [builder] arguments must not be null.
518
  const AnimatedBuilder({
519
    Key key,
520
    @required Listenable animation,
521
    @required this.builder,
522
    this.child,
523 524
  }) : assert(builder != null),
       super(key: key, listenable: animation);
525

526
  /// Called every time the animation changes value.
527
  final TransitionBuilder builder;
528 529 530 531 532 533 534 535 536 537 538

  /// If your builder function contains a subtree that does not depend on the
  /// animation, it's more efficient to build that subtree once instead of
  /// rebuilding it on every animation tick.
  ///
  /// If you pass the pre-built subtree as the [child] parameter, the
  /// AnimatedBuilder will pass it back to your builder function so that you
  /// can incorporate it into your build.
  ///
  /// Using this pre-built child is entirely optional, but can improve
  /// performance significantly in some cases and is therefore a good practice.
539
  final Widget child;
540

541
  @override
542
  Widget build(BuildContext context) {
543
    return builder(context, child);
544 545
  }
}