transitions.dart 19.9 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 101 102 103
///
/// The translation is expressed as a [Offset] scaled to the child's size. For
/// example, an [Offset] with a `dx` of 0.25 will result in a horizontal
/// translation of one quarter the width of the child.
Ian Hickson's avatar
Ian Hickson committed
104 105 106 107 108 109
///
/// By default, the offsets are applied in the coordinate system of the canvas
/// (so positive x offsets move the child towards the right). If a
/// [textDirection] is provided, then the offsets are applied in the reading
/// direction, so in right-to-left text, positive x offsets move towards the
/// left, and in left-to-right text, positive x offsets move towards the right.
110
class SlideTransition extends AnimatedWidget {
111 112
  /// Creates a fractional translation transition.
  ///
113
  /// The [position] argument must not be null.
114
  const SlideTransition({
115
    Key key,
116
    @required Animation<Offset> position,
117
    this.transformHitTests: true,
Ian Hickson's avatar
Ian Hickson committed
118
    this.textDirection,
119
    this.child,
120 121
  }) : assert(position != null),
       super(key: key, listenable: position);
122

123 124
  /// The animation that controls the position of the child.
  ///
125 126
  /// If the current value of the position animation is `(dx, dy)`, the child
  /// will be translated horizontally by `width * dx` and vertically by
Ian Hickson's avatar
Ian Hickson committed
127
  /// `height * dy`, after applying the [textDirection] if available.
128
  Animation<Offset> get position => listenable;
129

Ian Hickson's avatar
Ian Hickson committed
130 131 132 133 134 135 136 137 138 139 140 141 142
  /// The direction to use for the x offset described by the [position].
  ///
  /// If [textDirection] is null, the x offset is applied in the coordinate
  /// system of the canvas (so positive x offsets move the child towards the
  /// right).
  ///
  /// If [textDirection] is [TextDirection.rtl], the x offset is applied in the
  /// reading direction such that x offsets move the child towards the left.
  ///
  /// If [textDirection] is [TextDirection.ltr], the x offset is applied in the
  /// reading direction such that x offsets move the child towards the right.
  final TextDirection textDirection;

143 144 145 146 147 148
  /// 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".
149
  final bool transformHitTests;
150

151
  /// The widget below this widget in the tree.
152 153
  ///
  /// {@macro flutter.widgets.child}
154
  final Widget child;
155

156
  @override
157
  Widget build(BuildContext context) {
Ian Hickson's avatar
Ian Hickson committed
158 159 160
    Offset offset = position.value;
    if (textDirection == TextDirection.rtl)
      offset = new Offset(-offset.dx, offset.dy);
161
    return new FractionalTranslation(
Ian Hickson's avatar
Ian Hickson committed
162
      translation: offset,
163
      transformHitTests: transformHitTests,
164
      child: child,
165
    );
166 167 168
  }
}

Hans Muller's avatar
Hans Muller committed
169
/// Animates the scale of transformed widget.
170
class ScaleTransition extends AnimatedWidget {
171 172
  /// Creates a scale transition.
  ///
173
  /// The [scale] argument must not be null. The [alignment] argument defaults
174
  /// to [Alignment.center].
175
  const ScaleTransition({
176
    Key key,
177
    @required Animation<double> scale,
178
    this.alignment: Alignment.center,
179
    this.child,
180
  }) : super(key: key, listenable: scale);
181

182 183 184 185
  /// 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.
186
  Animation<double> get scale => listenable;
187

188
  /// The alignment of the origin of the coordinate system in which the scale
189 190 191
  /// takes place, relative to the size of the box.
  ///
  /// For example, to set the origin of the scale to bottom middle, you can use
192 193
  /// an alignment of (0.0, 1.0).
  final Alignment alignment;
194

195
  /// The widget below this widget in the tree.
196 197
  ///
  /// {@macro flutter.widgets.child}
198
  final Widget child;
199

200
  @override
201
  Widget build(BuildContext context) {
202
    final double scaleValue = scale.value;
203
    final Matrix4 transform = new Matrix4.identity()
204
      ..scale(scaleValue, scaleValue, 1.0);
205 206 207
    return new Transform(
      transform: transform,
      alignment: alignment,
208
      child: child,
209 210 211 212
    );
  }
}

213
/// Animates the rotation of a widget.
214
class RotationTransition extends AnimatedWidget {
215 216
  /// Creates a rotation transition.
  ///
217
  /// The [turns] argument must not be null.
218
  const RotationTransition({
Hixie's avatar
Hixie committed
219
    Key key,
220
    @required Animation<double> turns,
221
    this.child,
222
  }) : super(key: key, listenable: turns);
Hixie's avatar
Hixie committed
223

224 225 226 227
  /// 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.
228
  Animation<double> get turns => listenable;
229

230
  /// The widget below this widget in the tree.
231 232
  ///
  /// {@macro flutter.widgets.child}
233
  final Widget child;
Hixie's avatar
Hixie committed
234

235
  @override
236
  Widget build(BuildContext context) {
237 238
    final double turnsValue = turns.value;
    final Matrix4 transform = new Matrix4.rotationZ(turnsValue * math.PI * 2.0);
Hixie's avatar
Hixie committed
239 240
    return new Transform(
      transform: transform,
241
      alignment: Alignment.center,
242
      child: child,
Hixie's avatar
Hixie committed
243 244 245 246
    );
  }
}

247
/// Animates its own size and clips and aligns the child.
Ian Hickson's avatar
Ian Hickson committed
248 249 250
///
/// For a widget that automatically animates between the sizes of two children,
/// fading between them, see [AnimatedCrossFade].
251
class SizeTransition extends AnimatedWidget {
252 253
  /// Creates a size transition.
  ///
254
  /// The [sizeFactor] argument must not be null. The [axis] argument defaults
255
  /// to [Axis.vertical]. The [axisAlignment] defaults to 0.0, which centers the
256
  /// child along the main axis during the transition.
257
  const SizeTransition({
Hans Muller's avatar
Hans Muller committed
258 259
    Key key,
    this.axis: Axis.vertical,
260
    @required Animation<double> sizeFactor,
261
    this.axisAlignment: 0.0,
262
    this.child,
263 264
  }) : assert(axis != null),
       super(key: key, listenable: sizeFactor);
Hans Muller's avatar
Hans Muller committed
265 266 267 268 269 270 271

  /// [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.
272
  Animation<double> get sizeFactor => listenable;
Hans Muller's avatar
Hans Muller committed
273

274 275
  /// How to align the child along the axis that sizeFactor is modifying.
  final double axisAlignment;
Hans Muller's avatar
Hans Muller committed
276

277
  /// The widget below this widget in the tree.
278 279
  ///
  /// {@macro flutter.widgets.child}
Hans Muller's avatar
Hans Muller committed
280 281
  final Widget child;

282
  @override
Hans Muller's avatar
Hans Muller committed
283
  Widget build(BuildContext context) {
284
    AlignmentDirectional alignment;
285
    if (axis == Axis.vertical)
286
      alignment = new AlignmentDirectional(-1.0, axisAlignment);
287
    else
288
      alignment = new AlignmentDirectional(axisAlignment, -1.0);
Hans Muller's avatar
Hans Muller committed
289 290 291 292 293
    return new ClipRect(
      child: new Align(
        alignment: alignment,
        heightFactor: axis == Axis.vertical ? sizeFactor.value : null,
        widthFactor: axis == Axis.horizontal ? sizeFactor.value : null,
294
        child: child,
Hans Muller's avatar
Hans Muller committed
295 296 297 298 299
      )
    );
  }
}

300
/// Animates the opacity of a widget.
Ian Hickson's avatar
Ian Hickson committed
301 302 303
///
/// For a widget that automatically animates between the sizes of two children,
/// fading between them, see [AnimatedCrossFade].
304
class FadeTransition extends AnimatedWidget {
305 306
  /// Creates an opacity transition.
  ///
307
  /// The [opacity] argument must not be null.
308
  const FadeTransition({
309
    Key key,
310
    @required Animation<double> opacity,
311
    this.child,
312
  }) : super(key: key, listenable: opacity);
313

314 315 316 317 318 319
  /// 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.
320
  Animation<double> get opacity => listenable;
321

322
  /// The widget below this widget in the tree.
323 324
  ///
  /// {@macro flutter.widgets.child}
325 326
  final Widget child;

327
  @override
328 329 330 331 332
  Widget build(BuildContext context) {
    return new Opacity(opacity: opacity.value, child: child);
  }
}

333
/// An interpolation between two relative rects.
334
///
335
/// This class specializes the interpolation of [Tween<RelativeRect>] to
336
/// use [RelativeRect.lerp].
337 338
///
/// See [Tween] for a discussion on how to use interpolation objects.
339
class RelativeRectTween extends Tween<RelativeRect> {
340
  /// Creates a [RelativeRect] tween.
341
  ///
342 343
  /// The [begin] and [end] properties may be null; the null value
  /// is treated as [RelativeRect.fill].
344 345
  RelativeRectTween({ RelativeRect begin, RelativeRect end })
    : super(begin: begin, end: end);
346

347
  /// Returns the value this variable has at the given animation clock value.
348
  @override
349 350 351
  RelativeRect lerp(double t) => RelativeRect.lerp(begin, end, t);
}

352
/// Animated version of [Positioned] which takes a specific
353 354
/// [Animation<RelativeRect>] to transition the child's position from a start
/// position to and end position over the lifetime of the animation.
355
///
356
/// Only works if it's the child of a [Stack].
357 358 359
///
/// See also:
///
360
/// * [RelativePositionedTransition].
361
class PositionedTransition extends AnimatedWidget {
362 363
  /// Creates a transition for [Positioned].
  ///
364
  /// The [rect] argument must not be null.
365
  const PositionedTransition({
366
    Key key,
367
    @required Animation<RelativeRect> rect,
368
    @required this.child,
369
  }) : super(key: key, listenable: rect);
370

371
  /// The animation that controls the child's size and position.
372
  Animation<RelativeRect> get rect => listenable;
373

374
  /// The widget below this widget in the tree.
375 376
  ///
  /// {@macro flutter.widgets.child}
377
  final Widget child;
378

379
  @override
380
  Widget build(BuildContext context) {
381 382
    return new Positioned.fromRelativeRect(
      rect: rect.value,
383
      child: child,
384 385 386 387
    );
  }
}

388 389 390 391 392 393 394 395
/// 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:
///
396
/// * [PositionedTransition].
397
class RelativePositionedTransition extends AnimatedWidget {
398 399 400 401
  /// 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
402
  /// [size]. Both [rect] and [size] must not be null.
403
  const RelativePositionedTransition({
404 405 406
    Key key,
    @required Animation<Rect> rect,
    @required this.size,
407
    @required this.child,
408
  }) : super(key: key, listenable: rect);
409 410

  /// The animation that controls the child's size and position.
411 412
  ///
  /// See also [size].
413
  Animation<Rect> get rect => listenable;
414 415 416 417 418 419

  /// 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.
420 421
  ///
  /// {@macro flutter.widgets.child}
422 423 424 425 426 427 428 429 430 431
  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,
432
      child: child,
433 434 435 436
    );
  }
}

437 438
/// Animated version of a [DecoratedBox] that animates the different properties
/// of its [Decoration].
439
///
440 441 442
/// See also:
///
/// * [DecoratedBox], which also draws a [Decoration] but is not animated.
443
/// * [AnimatedContainer], a more full-featured container that also animates on
444 445
///   decoration using an internal animation.
class DecoratedBoxTransition extends AnimatedWidget {
446
  /// Creates an animated [DecoratedBox] whose [Decoration] animation updates
447 448
  /// the widget.
  ///
449
  /// The [decoration] and [position] must not be null.
450
  ///
451
  /// See also:
452
  ///
453
  /// * [new DecoratedBox].
454
  const DecoratedBoxTransition({
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470
    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.
471 472
  ///
  /// {@macro flutter.widgets.child}
473 474 475 476 477 478 479 480 481 482 483 484
  final Widget child;

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

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
/// Animated version of an [Align] that animates its [Align.alignment] property.
class AlignTransition extends AnimatedWidget {
  /// Creates an animated [Align] whose [AlignmentGeometry] animation updates
  /// the widget.
  ///
  /// See also:
  ///
  /// * [new Align].
  const AlignTransition({
    Key key,
    @required Animation<AlignmentGeometry> alignment,
    @required this.child,
    this.widthFactor,
    this.heightFactor,
  }) : super(key: key, listenable: alignment);

  /// The animation that controls the child's alignment.
  Animation<AlignmentGeometry> get alignment => listenable;

  /// If non-null, the child's width factor, see [Align.widthFactor].
  final double widthFactor;

  /// If non-null, the child's height factor, see [Align.heightFactor].
  final double heightFactor;

  /// The widget below this widget in the tree.
511 512
  ///
  /// {@macro flutter.widgets.child}
513 514 515 516 517 518 519 520 521 522 523 524 525
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return new Align(
      alignment: alignment.value,
      widthFactor: widthFactor,
      heightFactor: heightFactor,
      child: child,
    );
  }
}

526
/// A builder that builds a widget given a child.
527 528 529 530
///
/// The child should typically be part of the returned widget tree.
///
/// Used by [AnimatedBuilder.builder].
531 532
typedef Widget TransitionBuilder(BuildContext context, Widget child);

533 534
/// A general-purpose widget for building animations.
///
535
/// AnimatedBuilder is useful for more complex widgets that wish to include
536 537 538
/// an animation as part of a larger build function. To use AnimatedBuilder,
/// simply construct the widget and pass it a builder function.
///
539 540 541
/// For simple cases without additional state, consider using
/// [AnimatedWidget].
///
542
/// ## Performance optimizations
543
///
544 545 546 547 548 549 550 551 552 553 554
/// 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.
///
555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599
/// ## 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,
///         );
///       },
///     );
///   }
/// }
/// ```
600
class AnimatedBuilder extends AnimatedWidget {
601 602
  /// Creates an animated builder.
  ///
603
  /// The [animation] and [builder] arguments must not be null.
604
  const AnimatedBuilder({
605
    Key key,
606
    @required Listenable animation,
607
    @required this.builder,
608
    this.child,
609 610
  }) : assert(builder != null),
       super(key: key, listenable: animation);
611

612
  /// Called every time the animation changes value.
613
  final TransitionBuilder builder;
614

615 616 617 618 619
  /// The child widget to pass to the [builder].
  ///
  /// If a [builder] callback's return value 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.
620
  ///
621 622 623
  /// If the pre-built subtree is passed as the [child] parameter, the
  /// [AnimatedBuilder] will pass it back to the [builder] function so that it
  /// can be incorporated into the build.
624 625 626
  ///
  /// Using this pre-built child is entirely optional, but can improve
  /// performance significantly in some cases and is therefore a good practice.
627
  final Widget child;
628

629
  @override
630
  Widget build(BuildContext context) {
631
    return builder(context, child);
632 633
  }
}