animated_icons.dart 9.26 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
// Copyright 2017 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.

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:
// AnimationController controller;

/// Shows an animated icon at a given animation [progress].
///
/// The available icons are specified in [AnimatedIcons].
///
/// ### Sample code
///
/// ```dart
/// new AnimatedIcon(
///   icon: AnimatedIcons.menu_arrow,
///   progress: controller,
///   semanticLabel: 'Show menu',
/// )
/// ```
///
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({
    Key key,
    @required this.icon,
    @required this.progress,
    this.color,
    this.size,
    this.semanticLabel,
    this.textDirection,
  }) : assert(progress != null),
       assert(icon != null);

  /// 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.
  final Color color;

  /// 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.
  final double size;

  /// 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.
  ///
  /// See also:
  ///
  ///  * [Semantics.label], which is set to [semanticLabel] in the underlying
  ///    [Semantics] widget.
  final String semanticLabel;

  /// 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
95
  /// If the text direction is [TextDirection.rtl], the icon will be mirrored
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 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 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
  /// horizontally (e.g back arrow will point right).
  final TextDirection textDirection;

  static final _UiPathFactory _pathFactory = () => new ui.Path();

  @override
  Widget build(BuildContext context) {
    final _AnimatedIconData iconData = icon;
    final IconThemeData iconTheme = IconTheme.of(context);
    final double iconSize = size ?? iconTheme.size;
    final TextDirection textDirection = this.textDirection ?? Directionality.of(context);
    final double iconOpacity = iconTheme.opacity;
    Color iconColor = color ?? iconTheme.color;
    if (iconOpacity != 1.0)
      iconColor = iconColor.withOpacity(iconColor.opacity * iconOpacity);
    return new Semantics(
      label: semanticLabel,
      child: new CustomPaint(
        size: new Size(iconSize, iconSize),
        painter: new _AnimatedIconPainter(
          paths: iconData.paths,
          progress: progress,
          color: iconColor,
          scale: iconSize / iconData.size.width,
          shouldMirror: textDirection == TextDirection.rtl && iconData.matchTextDirection,
          uiPathFactory: _pathFactory,
        ),
      ),
    );
  }
}

typedef ui.Path _UiPathFactory();

class _AnimatedIconPainter extends CustomPainter {
  _AnimatedIconPainter({
    @required this.paths,
    @required this.progress,
    @required this.color,
    @required this.scale,
    @required this.shouldMirror,
    @required this.uiPathFactory,
  }) : 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);
    }

    final double clampedProgress = progress.value.clamp(0.0, 1.0);
    for (_PathFrames path in paths)
      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
  bool hitTest(Offset position) => null;

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

  @override
  SemanticsBuilderCallback get semanticsBuilder => null;
}

class _PathFrames {
  const _PathFrames({
    @required this.commands,
    @required this.opacities
  });

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

  void paint(ui.Canvas canvas, Color color, _UiPathFactory uiPathFactory, double progress) {
    final double opacity = _interpolate(opacities, progress, lerpDouble);
    final ui.Paint paint = new ui.Paint()
      ..style = PaintingStyle.fill
      ..color = color.withOpacity(color.opacity * opacity);
    final ui.Path path = uiPathFactory();
    for (_PathCommand command in commands)
      command.apply(path, progress);
    canvas.drawPath(path, paint);
  }
}

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

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

  final List<Offset> points;

  @override
  void apply(Path path, double progress) {
    final Offset point = _interpolate(points, progress, Offset.lerp);
    path.lineTo(point.dx, point.dy);
  }
}

class _PathClose extends _PathCommand {
  const _PathClose();

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

// Interpolates a value given a set of values equally spaced in time.
//
278
// [interpolator] is the interpolation function used to interpolate between 2
279 280
// points of type T.
//
281
// This is currently done with linear interpolation between every 2 consecutive
282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299
// 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.
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];
  final double targetIdx = lerpDouble(0, values.length -1, progress);
  final int lowIdx = targetIdx.floor();
  final int highIdx = targetIdx.ceil();
  final double t = targetIdx - lowIdx;
  return interpolator(values[lowIdx], values[highIdx], t);
}

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