| // Copyright 2014 The Flutter 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]. |
| /// |
| /// {@youtube 560 315 https://www.youtube.com/watch?v=pJcbh8pbvJs} |
| /// |
| /// {@tool snippet} |
| /// |
| /// ```dart |
| /// AnimatedIcon( |
| /// icon: AnimatedIcons.menu_arrow, |
| /// progress: controller, |
| /// semanticLabel: 'Show menu', |
| /// ) |
| /// ``` |
| /// {@end-tool} |
| /// |
| 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), |
| super(key: key); |
| |
| /// 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. |
| /// |
| /// If the text direction is [TextDirection.rtl], the icon will be mirrored |
| /// horizontally (e.g back arrow will point right). |
| final TextDirection textDirection; |
| |
| static final _UiPathFactory _pathFactory = () => ui.Path(); |
| |
| @override |
| Widget build(BuildContext context) { |
| final _AnimatedIconData iconData = icon as _AnimatedIconData; |
| 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 Semantics( |
| label: semanticLabel, |
| child: CustomPaint( |
| size: Size(iconSize, iconSize), |
| painter: _AnimatedIconPainter( |
| paths: iconData.paths, |
| progress: progress, |
| color: iconColor, |
| scale: iconSize / iconData.size.width, |
| shouldMirror: textDirection == TextDirection.rtl && iconData.matchTextDirection, |
| uiPathFactory: _pathFactory, |
| ), |
| ), |
| ); |
| } |
| } |
| |
| typedef _UiPathFactory = ui.Path Function(); |
| |
| 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) as double; |
| for (final _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<double>(opacities, progress, lerpDouble); |
| final ui.Paint paint = ui.Paint() |
| ..style = PaintingStyle.fill |
| ..color = color.withOpacity(color.opacity * opacity); |
| final ui.Path path = uiPathFactory(); |
| for (final _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... |
| /// |
| /// _PathCommand instances represents such a command, and can apply it to |
| /// 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<Offset>(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<Offset>(controlPoints1, progress, Offset.lerp); |
| final Offset controlPoint2 = _interpolate<Offset>(controlPoints2, progress, Offset.lerp); |
| final Offset targetPoint = _interpolate<Offset>(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<Offset>(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. |
| // |
| // [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. |
| 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 _Interpolator<T> = T Function(T a, T b, double progress); |