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
95
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
209
210
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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
// 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:
// late 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:
///
/// * [SemanticsProperties.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 ui.Path _pathFactory() => ui.Path();
@override
Widget build(BuildContext context) {
assert(debugCheckHasDirectionality(context));
final _AnimatedIconData iconData = icon as _AnimatedIconData;
final IconThemeData iconTheme = IconTheme.of(context);
assert(iconTheme.isConcrete);
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);
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);