animated_icons.dart 9.54 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
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

part of material_animated_icons;

// The code for drawing animated icons is kept in a private API, as we are not
// yet ready for exposing a public API for (partial) vector graphics support.
// See: https://github.com/flutter/flutter/issues/1831 for details regarding
// generic vector graphics support in Flutter.

// Examples can assume:
13
// late AnimationController controller;
14 15 16 17 18

/// Shows an animated icon at a given animation [progress].
///
/// The available icons are specified in [AnimatedIcons].
///
19 20
/// {@youtube 560 315 https://www.youtube.com/watch?v=pJcbh8pbvJs}
///
21
/// {@tool snippet}
22 23
///
/// ```dart
24
/// AnimatedIcon(
25 26 27 28 29
///   icon: AnimatedIcons.menu_arrow,
///   progress: controller,
///   semanticLabel: 'Show menu',
/// )
/// ```
30
/// {@end-tool}
31 32 33 34 35 36 37
///
class AnimatedIcon extends StatelessWidget {
  /// Creates an AnimatedIcon.
  ///
  /// The [progress] and [icon] arguments must not be null.
  /// The [size] and [color] default to the value given by the current [IconTheme].
  const AnimatedIcon({
38 39 40
    Key? key,
    required this.icon,
    required this.progress,
41 42 43 44 45
    this.color,
    this.size,
    this.semanticLabel,
    this.textDirection,
  }) : assert(progress != null),
46 47
       assert(icon != null),
       super(key: key);
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70

  /// The animation progress for the animated icon.
  ///
  /// The value is clamped to be between 0 and 1.
  ///
  /// This determines the actual frame that is displayed.
  final Animation<double> progress;

  /// The color to use when drawing the icon.
  ///
  /// Defaults to the current [IconTheme] color, if any.
  ///
  /// The given color will be adjusted by the opacity of the current
  /// [IconTheme], if any.
  ///
  /// In material apps, if there is a [Theme] without any [IconTheme]s
  /// specified, icon colors default to white if the theme is dark
  /// and black if the theme is light.
  ///
  /// If no [IconTheme] and no [Theme] is specified, icons will default to black.
  ///
  /// See [Theme] to set the current theme and [ThemeData.brightness]
  /// for setting the current theme's brightness.
71
  final Color? color;
72 73 74 75 76 77

  /// The size of the icon in logical pixels.
  ///
  /// Icons occupy a square with width and height equal to size.
  ///
  /// Defaults to the current [IconTheme] size.
78
  final double? size;
79 80 81 82 83 84 85 86

  /// The icon to display. Available icons are listed in [AnimatedIcons].
  final AnimatedIconData icon;

  /// Semantic label for the icon.
  ///
  /// Announced in accessibility modes (e.g TalkBack/VoiceOver).
  /// This label does not show in the UI.
87 88 89 90 91
  ///
  /// See also:
  ///
  ///  * [SemanticsProperties.label], which is set to [semanticLabel] in the
  ///    underlying [Semantics] widget.
92
  final String? semanticLabel;
93 94 95 96 97

  /// The text direction to use for rendering the icon.
  ///
  /// If this is null, the ambient [Directionality] is used instead.
  ///
Josh Soref's avatar
Josh Soref committed
98
  /// If the text direction is [TextDirection.rtl], the icon will be mirrored
99
  /// horizontally (e.g back arrow will point right).
100
  final TextDirection? textDirection;
101

102
  static ui.Path _pathFactory() => ui.Path();
103 104 105

  @override
  Widget build(BuildContext context) {
106
    assert(debugCheckHasDirectionality(context));
107
    final _AnimatedIconData iconData = icon as _AnimatedIconData;
108
    final IconThemeData iconTheme = IconTheme.of(context);
109 110
    assert(iconTheme.isConcrete);
    final double iconSize = size ?? iconTheme.size!;
111
    final TextDirection textDirection = this.textDirection ?? Directionality.of(context);
112 113
    final double iconOpacity = iconTheme.opacity!;
    Color iconColor = color ?? iconTheme.color!;
114 115
    if (iconOpacity != 1.0)
      iconColor = iconColor.withOpacity(iconColor.opacity * iconOpacity);
116
    return Semantics(
117
      label: semanticLabel,
118 119 120
      child: CustomPaint(
        size: Size(iconSize, iconSize),
        painter: _AnimatedIconPainter(
121 122 123 124 125 126 127 128 129 130 131 132
          paths: iconData.paths,
          progress: progress,
          color: iconColor,
          scale: iconSize / iconData.size.width,
          shouldMirror: textDirection == TextDirection.rtl && iconData.matchTextDirection,
          uiPathFactory: _pathFactory,
        ),
      ),
    );
  }
}

133
typedef _UiPathFactory = ui.Path Function();
134 135 136

class _AnimatedIconPainter extends CustomPainter {
  _AnimatedIconPainter({
137 138 139 140 141 142
    required this.paths,
    required this.progress,
    required this.color,
    required this.scale,
    required this.shouldMirror,
    required this.uiPathFactory,
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
  }) : super(repaint: progress);

  // This list is assumed to be immutable, changes to the contents of the list
  // will not trigger a redraw as shouldRepaint will keep returning false.
  final List<_PathFrames> paths;
  final Animation<double> progress;
  final Color color;
  final double scale;
  /// If this is true the image will be mirrored horizontally.
  final bool shouldMirror;
  final _UiPathFactory uiPathFactory;

  @override
  void paint(ui.Canvas canvas, Size size) {
    // The RenderCustomPaint render object performs canvas.save before invoking
    // this and canvas.restore after, so we don't need to do it here.
    canvas.scale(scale, scale);
    if (shouldMirror) {
      canvas.rotate(math.pi);
      canvas.translate(-size.width, -size.height);
    }

165
    final double clampedProgress = progress.value.clamp(0.0, 1.0);
166
    for (final _PathFrames path in paths)
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
      path.paint(canvas, color, uiPathFactory, clampedProgress);
  }


  @override
  bool shouldRepaint(_AnimatedIconPainter oldDelegate) {
    return oldDelegate.progress.value != progress.value
        || oldDelegate.color != color
        // We are comparing the paths list by reference, assuming the list is
        // treated as immutable to be more efficient.
        || oldDelegate.paths != paths
        || oldDelegate.scale != scale
        || oldDelegate.uiPathFactory != uiPathFactory;
  }

  @override
183
  bool? hitTest(Offset position) => null;
184 185 186 187 188

  @override
  bool shouldRebuildSemantics(CustomPainter oldDelegate) => false;

  @override
189
  SemanticsBuilderCallback? get semanticsBuilder => null;
190 191 192 193
}

class _PathFrames {
  const _PathFrames({
194 195
    required this.commands,
    required this.opacities,
196 197 198 199 200 201
  });

  final List<_PathCommand> commands;
  final List<double> opacities;

  void paint(ui.Canvas canvas, Color color, _UiPathFactory uiPathFactory, double progress) {
202
    final double opacity = _interpolate<double?>(opacities, progress, lerpDouble)!;
203
    final ui.Paint paint = ui.Paint()
204 205 206
      ..style = PaintingStyle.fill
      ..color = color.withOpacity(color.opacity * opacity);
    final ui.Path path = uiPathFactory();
207
    for (final _PathCommand command in commands)
208 209 210 211 212 213
      command.apply(path, progress);
    canvas.drawPath(path, paint);
  }
}

/// Paths are being built by a set of commands e.g moveTo, lineTo, etc...
214 215
///
/// _PathCommand instances represents such a command, and can apply it to
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
/// a given Path.
abstract class _PathCommand {
  const _PathCommand();

  /// Applies the path command to [path].
  ///
  /// For example if the object is a [_PathMoveTo] command it will invoke
  /// [Path.moveTo] on [path].
  void apply(ui.Path path, double progress);
}

class _PathMoveTo extends _PathCommand {
  const _PathMoveTo(this.points);

  final List<Offset> points;

  @override
  void apply(Path path, double progress) {
234
    final Offset offset = _interpolate<Offset?>(points, progress, Offset.lerp)!;
235 236 237 238 239 240 241 242 243 244 245 246 247
    path.moveTo(offset.dx, offset.dy);
  }
}

class _PathCubicTo extends _PathCommand {
  const _PathCubicTo(this.controlPoints1, this.controlPoints2, this.targetPoints);

  final List<Offset> controlPoints2;
  final List<Offset> controlPoints1;
  final List<Offset> targetPoints;

  @override
  void apply(Path path, double progress) {
248 249 250
    final Offset controlPoint1 = _interpolate<Offset?>(controlPoints1, progress, Offset.lerp)!;
    final Offset controlPoint2 = _interpolate<Offset?>(controlPoints2, progress, Offset.lerp)!;
    final Offset targetPoint = _interpolate<Offset?>(targetPoints, progress, Offset.lerp)!;
251 252 253
    path.cubicTo(
      controlPoint1.dx, controlPoint1.dy,
      controlPoint2.dx, controlPoint2.dy,
254
      targetPoint.dx, targetPoint.dy,
255 256 257 258 259 260 261 262 263 264 265 266
    );
  }
}

// ignore: unused_element
class _PathLineTo extends _PathCommand {
  const _PathLineTo(this.points);

  final List<Offset> points;

  @override
  void apply(Path path, double progress) {
267
    final Offset point = _interpolate<Offset?>(points, progress, Offset.lerp)!;
268 269 270 271 272 273 274 275 276 277 278 279 280
    path.lineTo(point.dx, point.dy);
  }
}

class _PathClose extends _PathCommand {
  const _PathClose();

  @override
  void apply(Path path, double progress) {
    path.close();
  }
}

281 282 283 284 285 286 287 288 289 290 291
/// Interpolates a value given a set of values equally spaced in time.
///
/// [interpolator] is the interpolation function used to interpolate between 2
/// points of type T.
///
/// This is currently done with linear interpolation between every 2 consecutive
/// points. Linear interpolation was smooth enough with the limited set of
/// animations we have tested, so we use it for simplicity. If we find this to
/// not be smooth enough we can try applying spline instead.
///
/// [progress] is expected to be between 0.0 and 1.0.
292 293 294 295 296
T _interpolate<T>(List<T> values, double progress, _Interpolator<T> interpolator) {
  assert(progress <= 1.0);
  assert(progress >= 0.0);
  if (values.length == 1)
    return values[0];
297
  final double targetIdx = lerpDouble(0, values.length -1, progress)!;
298 299 300 301 302 303
  final int lowIdx = targetIdx.floor();
  final int highIdx = targetIdx.ceil();
  final double t = targetIdx - lowIdx;
  return interpolator(values[lowIdx], values[highIdx], t);
}

304
typedef _Interpolator<T> = T Function(T a, T b, double progress);