tween_animation_builder.dart 9.49 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/animation.dart';

import 'framework.dart';
import 'implicit_animations.dart';
import 'value_listenable_builder.dart';

/// [Widget] builder that animates a property of a [Widget] to a target value
/// whenever the target value changes.
///
14 15
/// {@youtube 560 315 https://www.youtube.com/watch?v=l9uHB8VXZOg}
///
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
/// The type of the animated property ([Color], [Rect], [double], etc.) is
/// defined via the type of the provided [tween] (e.g. [ColorTween],
/// [RectTween], [Tween<double>], etc.).
///
/// The [tween] also defines the target value for the animation: When the widget
/// first builds, it animates from [Tween.begin] to [Tween.end]. A new animation
/// can be triggered anytime by providing a new [tween] with a new [Tween.end]
/// value. The new animation runs from the current animation value (which may be
/// [Tween.end] of the old [tween], if that animation completed) to [Tween.end]
/// of the new [tween].
///
/// The animation is further customized by providing a [curve] and [duration].
///
/// The current value of the animation along with the [child] is passed to
/// the [builder] callback, which is expected to build a [Widget] based on the
/// current animation value. The [builder] is called throughout the animation
/// for every animation value until [Tween.end] is reached.
///
/// A provided [onEnd] callback is called whenever an animation completes.
/// Registering an [onEnd] callback my be useful to trigger an action (like
/// another animation) at the end of the current animation.
///
/// ## Performance optimizations
///
/// 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.
///
/// ## Ownership of the [Tween]
///
/// The [TweenAnimationBuilder] takes full ownership of the provided [tween]
/// instance and it will mutate it. Once a [Tween] has been passed to a
/// [TweenAnimationBuilder], its properties should not be accessed or changed
/// anymore to avoid interference with the [TweenAnimationBuilder].
///
/// It is good practice to never store a [Tween] provided to a
/// [TweenAnimationBuilder] in an instance variable to avoid accidental
/// modifications of the [Tween].
///
/// ## Example Code
///
64
/// {@tool dartpad --template=stateful_widget_scaffold_center}
65 66 67 68 69 70 71 72 73 74
/// This example shows an [IconButton] that "zooms" in when the widget first
/// builds (its size smoothly increases from 0 to 24) and whenever the button
/// is pressed, it smoothly changes its size to the new target value of either
/// 48 or 24.
///
/// ```dart
/// double targetValue = 24.0;
///
/// @override
/// Widget build(BuildContext context) {
75
///   return TweenAnimationBuilder<double>(
76
///     tween: Tween<double>(begin: 0, end: targetValue),
77
///     duration: const Duration(seconds: 1),
78
///     builder: (BuildContext context, double size, Widget? child) {
79 80 81
///       return IconButton(
///         iconSize: size,
///         color: Colors.blue,
82
///         icon: child!,
83 84 85 86 87 88 89
///         onPressed: () {
///           setState(() {
///             targetValue = targetValue == 24.0 ? 48.0 : 24.0;
///           });
///         },
///       );
///     },
90
///     child: const Icon(Icons.aspect_ratio),
91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
///   );
/// }
/// ```
/// {@end-tool}
///
/// ## Relationship to [ImplicitlyAnimatedWidget]s and [AnimatedWidget]s
///
/// The [ImplicitlyAnimatedWidget] has many subclasses that provide animated
/// versions of regular widgets. These subclasses (like [AnimatedOpacity],
/// [AnimatedContainer], [AnimatedSize], etc.) animate changes in their
/// properties smoothly and they are easier to use than this general-purpose
/// builder. However, [TweenAnimationBuilder] (which itself is a subclass of
/// [ImplicitlyAnimatedWidget]) is handy for animating any widget property to a
/// given target value even when the framework (or third-party widget library)
/// doesn't ship with an animated version of that widget.
///
/// Those [ImplicitlyAnimatedWidget]s (including this [TweenAnimationBuilder])
/// all manage an internal [AnimationController] to drive the animation. If you
/// want more control over the animation than just setting a target value,
/// [duration], and [curve], have a look at (subclasses of) [AnimatedWidget]s.
/// For those, you have to manually manage an [AnimationController] giving you
/// full control over the animation. An example of an [AnimatedWidget] is the
/// [AnimatedBuilder], which can be used similarly to this
/// [TweenAnimationBuilder], but unlike the latter it is powered by a
/// developer-managed [AnimationController].
116
class TweenAnimationBuilder<T extends Object?> extends ImplicitlyAnimatedWidget {
117 118 119 120 121 122 123 124 125 126
  /// Creates a [TweenAnimationBuilder].
  ///
  /// The properties [tween], [duration], and [builder] are required. The values
  /// for [tween], [curve], and [builder] must not be null.
  ///
  /// The [TweenAnimationBuilder] takes full ownership of the provided [tween]
  /// instance and mutates it. Once a [Tween] has been passed to a
  /// [TweenAnimationBuilder], its properties should not be accessed or changed
  /// anymore to avoid interference with the [TweenAnimationBuilder].
  const TweenAnimationBuilder({
127 128 129
    Key? key,
    required this.tween,
    required Duration duration,
130
    Curve curve = Curves.linear,
131 132
    required this.builder,
    VoidCallback? onEnd,
133 134 135 136
    this.child,
  }) : assert(tween != null),
       assert(curve != null),
       assert(builder != null),
137
       super(key: key, duration: duration, curve: curve, onEnd: onEnd);
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188

  /// Defines the target value for the animation.
  ///
  /// When the widget first builds, the animation runs from [Tween.begin] to
  /// [Tween.end], if [Tween.begin] is non-null. A new animation can be
  /// triggered at anytime by providing a new [Tween] with a new [Tween.end]
  /// value. The new animation runs from the current animation value (which may
  /// be [Tween.end] of the old [tween], if that animation completed) to
  /// [Tween.end] of the new [tween]. The [Tween.begin] value is ignored except
  /// for the initial animation that is triggered when the widget builds for the
  /// first time.
  ///
  /// Any (subclass of) [Tween] is accepted as an argument. For example, to
  /// animate the height or width of a [Widget], use a [Tween<double>], or
  /// check out the [ColorTween] to animate the color property of a [Widget].
  ///
  /// Any [Tween] provided must have a non-null [Tween.end] value.
  ///
  /// ## Ownership
  ///
  /// The [TweenAnimationBuilder] takes full ownership of the provided [Tween]
  /// and it will mutate the [Tween]. Once a [Tween] instance has been passed
  /// to [TweenAnimationBuilder] its properties should not be accessed or
  /// changed anymore to avoid any interference with the
  /// [TweenAnimationBuilder]. If you need to change the [Tween], create a
  /// **new instance** with the new values.
  ///
  /// It is good practice to never store a [Tween] provided to a
  /// [TweenAnimationBuilder] in an instance variable to avoid accidental
  /// modifications of the [Tween].
  final Tween<T> tween;

  /// Called every time the animation value changes.
  ///
  /// The current animation value is passed to the builder along with the
  /// [child]. The builder should build a [Widget] based on the current
  /// animation value and incorporate the [child] into it, if it is non-null.
  final ValueWidgetBuilder<T> builder;

  /// 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.
  ///
  /// If the pre-built subtree is passed as the child parameter, the
  /// [TweenAnimationBuilder] will pass it back to the [builder] function so
  /// that it can be incorporated into the build.
  ///
  /// Using this pre-built child is entirely optional, but can improve
  /// performance significantly in some cases and is therefore a good practice.
189
  final Widget? child;
190 191 192 193 194 195 196

  @override
  ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() {
    return _TweenAnimationBuilderState<T>();
  }
}

197
class _TweenAnimationBuilderState<T extends Object?> extends AnimatedWidgetBaseState<TweenAnimationBuilder<T>> {
198
  Tween<T>? _currentTween;
199 200 201 202

  @override
  void initState() {
    _currentTween = widget.tween;
203
    _currentTween!.begin ??= _currentTween!.end;
204
    super.initState();
205
    if (_currentTween!.begin != _currentTween!.end) {
206
      controller.forward();
207 208 209 210 211 212 213 214 215 216 217
    }
  }

  @override
  void forEachTween(TweenVisitor<dynamic> visitor) {
    assert(
      widget.tween.end != null,
      'Tween provided to TweenAnimationBuilder must have non-null Tween.end value.',
    );
    _currentTween = visitor(_currentTween, widget.tween.end, (dynamic value) {
      assert(false);
218 219
      throw StateError('Constructor will never be called because null is never provided as current tween.');
    }) as Tween<T>?;
220 221 222 223
  }

  @override
  Widget build(BuildContext context) {
224
    return widget.builder(context, _currentTween!.evaluate(animation), widget.child);
225 226
  }
}