animated_icons.dart 9.56 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
    super.key,
39 40
    required this.icon,
    required this.progress,
41 42 43 44 45
    this.color,
    this.size,
    this.semanticLabel,
    this.textDirection,
  }) : assert(progress != null),
46
       assert(icon != null);
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

  /// 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.
70
  final Color? color;
71 72 73 74 75 76

  /// 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.
77
  final double? size;
78 79 80 81 82 83 84 85

  /// 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.
86 87 88 89 90
  ///
  /// See also:
  ///
  ///  * [SemanticsProperties.label], which is set to [semanticLabel] in the
  ///    underlying [Semantics] widget.
91
  final String? semanticLabel;
92 93 94 95 96

  /// 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
97
  /// If the text direction is [TextDirection.rtl], the icon will be mirrored
98
  /// horizontally (e.g back arrow will point right).
99
  final TextDirection? textDirection;
100

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

  @override
  Widget build(BuildContext context) {
105
    assert(debugCheckHasDirectionality(context));
106
    final _AnimatedIconData iconData = icon as _AnimatedIconData;
107
    final IconThemeData iconTheme = IconTheme.of(context);
108 109
    assert(iconTheme.isConcrete);
    final double iconSize = size ?? iconTheme.size!;
110
    final TextDirection textDirection = this.textDirection ?? Directionality.of(context);
111 112
    final double iconOpacity = iconTheme.opacity!;
    Color iconColor = color ?? iconTheme.color!;
113
    if (iconOpacity != 1.0) {
114
      iconColor = iconColor.withOpacity(iconColor.opacity * iconOpacity);
115
    }
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
  }) : 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.
    if (shouldMirror) {
      canvas.rotate(math.pi);
      canvas.translate(-size.width, -size.height);
    }
163
    canvas.scale(scale, scale);
164

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


  @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
184
  bool? hitTest(Offset position) => null;
185 186 187 188 189

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

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

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

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

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

/// Paths are being built by a set of commands e.g moveTo, lineTo, etc...
216 217
///
/// _PathCommand instances represents such a command, and can apply it to
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
/// 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) {
236
    final Offset offset = _interpolate<Offset?>(points, progress, Offset.lerp)!;
237 238 239 240 241 242 243 244 245 246 247 248 249
    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) {
250 251 252
    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)!;
253 254 255
    path.cubicTo(
      controlPoint1.dx, controlPoint1.dy,
      controlPoint2.dx, controlPoint2.dy,
256
      targetPoint.dx, targetPoint.dy,
257 258 259 260 261 262 263 264 265 266 267 268
    );
  }
}

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

  final List<Offset> points;

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

class _PathClose extends _PathCommand {
  const _PathClose();

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

283 284 285 286 287 288 289 290 291 292 293
/// 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.
294 295 296
T _interpolate<T>(List<T> values, double progress, _Interpolator<T> interpolator) {
  assert(progress <= 1.0);
  assert(progress >= 0.0);
297
  if (values.length == 1) {
298
    return values[0];
299
  }
300
  final double targetIdx = lerpDouble(0, values.length -1, progress)!;
301 302 303 304 305 306
  final int lowIdx = targetIdx.floor();
  final int highIdx = targetIdx.ceil();
  final double t = targetIdx - lowIdx;
  return interpolator(values[lowIdx], values[highIdx], t);
}

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