Add an AnimatedIcon class and vitool (vector icon tool) to generate data for it (#13530)
@ -166,6 +166,7 @@ Future<Null> _runTests() async {
|
||||
|
||||
await _runAllDartTests(path.join(flutterRoot, 'dev', 'devicelab'));
|
||||
await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests'));
|
||||
await _runFlutterTest(path.join(flutterRoot, 'dev', 'tools', 'vitool'));
|
||||
await _runFlutterTest(path.join(flutterRoot, 'examples', 'hello_world'));
|
||||
await _runFlutterTest(path.join(flutterRoot, 'examples', 'layers'));
|
||||
await _runFlutterTest(path.join(flutterRoot, 'examples', 'stocks'));
|
||||
|
9
dev/tools/vitool/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
# Files and directories created by pub
|
||||
.packages
|
||||
.pub/
|
||||
build/
|
||||
# Remove the following pattern if you wish to check in your lock file
|
||||
pubspec.lock
|
||||
|
||||
# Directory created by dartdoc
|
||||
doc/api/
|
13
dev/tools/vitool/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
# vitool
|
||||
|
||||
This tool generates Dart files from frames described in SVG files that follow
|
||||
the small subset of SVG described below.
|
||||
This tool was crafted specifically to handle the assets for certain Material
|
||||
design animations as created by the Google Material Design team, and is not
|
||||
intended to be a general-purpose tool.
|
||||
|
||||
## Supported SVG features
|
||||
- groups
|
||||
- group transforms
|
||||
- group opacities
|
||||
- paths (strokes are not supported, only fills, eliptical arc curve commands are not supported)
|
97
dev/tools/vitool/bin/main.dart
Normal file
@ -0,0 +1,97 @@
|
||||
// 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.
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:args/args.dart';
|
||||
import 'package:vitool/vitool.dart';
|
||||
|
||||
const String kCodegenComment =
|
||||
'// AUTOGENERATED FILE DO NOT EDIT!\n'
|
||||
'// This file was generated by vitool.\n';
|
||||
|
||||
void main(List<String> args) {
|
||||
final ArgParser parser = new ArgParser();
|
||||
|
||||
parser.addFlag(
|
||||
'help',
|
||||
abbr: 'h',
|
||||
negatable: false,
|
||||
help: 'Display the tool\'s usage instructions and quit.'
|
||||
);
|
||||
|
||||
parser.addOption(
|
||||
'output',
|
||||
abbr: 'o',
|
||||
help: 'Target path to write the generated Dart file to.'
|
||||
);
|
||||
|
||||
parser.addOption(
|
||||
'asset-name',
|
||||
abbr: 'n',
|
||||
help: 'Name to be used for the generated constant.'
|
||||
);
|
||||
|
||||
parser.addOption(
|
||||
'part-of',
|
||||
abbr: 'p',
|
||||
help: 'Library name to add a dart \'part of\' clause for.'
|
||||
);
|
||||
|
||||
parser.addOption(
|
||||
'header',
|
||||
abbr: 'd',
|
||||
help: 'File whose contents are to be prepended to the beginning of '
|
||||
'the generated Dart file; this can be used for a license comment.'
|
||||
);
|
||||
|
||||
parser.addFlag(
|
||||
'codegen_comment',
|
||||
abbr: 'c',
|
||||
defaultsTo: true,
|
||||
help: 'Whether to include the following comment after the header:\n'
|
||||
'$kCodegenComment'
|
||||
);
|
||||
|
||||
final ArgResults argResults = parser.parse(args);
|
||||
|
||||
if (argResults['help'] ||
|
||||
!argResults.wasParsed('output') ||
|
||||
!argResults.wasParsed('asset-name') ||
|
||||
argResults.rest.isEmpty) {
|
||||
printUsage(parser);
|
||||
return;
|
||||
}
|
||||
|
||||
final List<FrameData> frames = <FrameData>[];
|
||||
for (String filePath in argResults.rest) {
|
||||
final FrameData data = interpretSvg(filePath);
|
||||
frames.add(data);
|
||||
}
|
||||
|
||||
final StringBuffer generatedSb = new StringBuffer();
|
||||
|
||||
if (argResults.wasParsed('header')) {
|
||||
generatedSb.write(new File(argResults['header']).readAsStringSync());
|
||||
generatedSb.write('\n');
|
||||
}
|
||||
|
||||
if (argResults['codegen_comment'])
|
||||
generatedSb.write(kCodegenComment);
|
||||
|
||||
if (argResults.wasParsed('part-of'))
|
||||
generatedSb.write('part of ${argResults['part-of']};\n');
|
||||
|
||||
final Animation animation = new Animation.fromFrameData(frames);
|
||||
generatedSb.write(animation.toDart('_AnimatedIconData', argResults['asset-name']));
|
||||
|
||||
final File outFile = new File(argResults['output']);
|
||||
outFile.writeAsStringSync(generatedSb.toString());
|
||||
}
|
||||
|
||||
void printUsage(ArgParser parser) {
|
||||
print('Usage: vitool --asset-name=<asset_name> --output=<output_path> <frames_list>');
|
||||
print('\nExample: vitool --asset-name=_\$menu_arrow --output=lib/data/menu_arrow.g.dart assets/svg/menu_arrow/*.svg\n');
|
||||
print(parser.usage);
|
||||
}
|
560
dev/tools/vitool/lib/vitool.dart
Normal file
@ -0,0 +1,560 @@
|
||||
// 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.
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
import 'package:xml/xml.dart' as xml show parse;
|
||||
import 'package:xml/xml.dart' hide parse;
|
||||
|
||||
// String to use for a single indentation.
|
||||
const String kIndent = ' ';
|
||||
|
||||
/// Represents an animation, and provides logic to generate dart code for it.
|
||||
class Animation {
|
||||
const Animation(this.size, this.paths);
|
||||
|
||||
factory Animation.fromFrameData(List<FrameData> frames) {
|
||||
_validateFramesData(frames);
|
||||
final Point<double> size = frames[0].size;
|
||||
final List<PathAnimation> paths = <PathAnimation>[];
|
||||
for (int i = 0; i < frames[0].paths.length; i += 1) {
|
||||
paths.add(new PathAnimation.fromFrameData(frames, i));
|
||||
}
|
||||
return new Animation(size, paths);
|
||||
}
|
||||
|
||||
/// The size of the animation (width, height) in pixels.
|
||||
final Point<double> size;
|
||||
|
||||
/// List of paths in the animation.
|
||||
final List<PathAnimation> paths;
|
||||
|
||||
static void _validateFramesData(List<FrameData> frames) {
|
||||
final Point<double> size = frames[0].size;
|
||||
final int numPaths = frames[0].paths.length;
|
||||
for (int i = 0; i < frames.length; i += 1) {
|
||||
final FrameData frame = frames[i];
|
||||
if (size != frame.size)
|
||||
throw new Exception(
|
||||
'All animation frames must have the same size,\n'
|
||||
'first frame size was: (${size.x}, ${size.y})\n'
|
||||
'frame $i size was: (${frame.size.x}, ${frame.size.y})'
|
||||
);
|
||||
if (numPaths != frame.paths.length)
|
||||
throw new Exception(
|
||||
'All animation frames must have the same number of paths,\n'
|
||||
'first frame has $numPaths paths\n'
|
||||
'frame $i has ${frame.paths.length} paths'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String toDart(String className, String varName) {
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
sb.write('const $className $varName = const $className(\n');
|
||||
sb.write('${kIndent}const Size(${size.x}, ${size.y}),\n');
|
||||
sb.write('${kIndent}const <_PathFrames>[\n');
|
||||
for (PathAnimation path in paths)
|
||||
sb.write(path.toDart());
|
||||
sb.write('$kIndent],\n');
|
||||
sb.write(');');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the animation of a single path.
|
||||
class PathAnimation {
|
||||
const PathAnimation(this.commands, {@required this.opacities});
|
||||
|
||||
factory PathAnimation.fromFrameData(List<FrameData> frames, int pathIdx) {
|
||||
if (frames.isEmpty)
|
||||
return const PathAnimation(const <PathCommandAnimation>[], opacities: const <double>[]);
|
||||
|
||||
final List<PathCommandAnimation> commands = <PathCommandAnimation>[];
|
||||
for (int commandIdx = 0; commandIdx < frames[0].paths[pathIdx].commands.length; commandIdx += 1) {
|
||||
final int numPointsInCommand = frames[0].paths[pathIdx].commands[commandIdx].points.length;
|
||||
final List<List<Point<double>>> points = new List<List<Point<double>>>(numPointsInCommand);
|
||||
for (int j = 0; j < numPointsInCommand; j += 1)
|
||||
points[j] = <Point<double>>[];
|
||||
final String commandType = frames[0].paths[pathIdx].commands[commandIdx].type;
|
||||
for (int i = 0; i < frames.length; i += 1) {
|
||||
final FrameData frame = frames[i];
|
||||
final String currentCommandType = frame.paths[pathIdx].commands[commandIdx].type;
|
||||
if (commandType != currentCommandType)
|
||||
throw new Exception(
|
||||
'Paths must be built from the same commands in all frames'
|
||||
'command $commandIdx at frame 0 was of type \'$commandType\''
|
||||
'command $commandIdx at frame $i was of type \'$currentCommandType\''
|
||||
);
|
||||
for (int j = 0; j < numPointsInCommand; j += 1)
|
||||
points[j].add(frame.paths[pathIdx].commands[commandIdx].points[j]);
|
||||
}
|
||||
commands.add(new PathCommandAnimation(commandType, points));
|
||||
}
|
||||
|
||||
final List<double> opacities =
|
||||
frames.map<double>((FrameData d) => d.paths[pathIdx].opacity).toList();
|
||||
|
||||
return new PathAnimation(commands, opacities: opacities);
|
||||
}
|
||||
|
||||
/// List of commands for drawing the path.
|
||||
final List<PathCommandAnimation> commands;
|
||||
/// The path opacity for each animation frame.
|
||||
final List<double> opacities;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PathAnimation(commands: $commands, opacities: $opacities)';
|
||||
}
|
||||
|
||||
String toDart() {
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
sb.write('${kIndent * 2}const _PathFrames(\n');
|
||||
sb.write('${kIndent * 3}opacities: const <double>[\n');
|
||||
for (double opacity in opacities)
|
||||
sb.write('${kIndent * 4}$opacity,\n');
|
||||
sb.write('${kIndent * 3}],\n');
|
||||
sb.write('${kIndent * 3}commands: const <_PathCommand>[\n');
|
||||
for (PathCommandAnimation command in commands)
|
||||
sb.write(command.toDart());
|
||||
sb.write('${kIndent * 3}],\n');
|
||||
sb.write('${kIndent * 2}),\n');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the animation of a single path command.
|
||||
class PathCommandAnimation {
|
||||
const PathCommandAnimation(this.type, this.points);
|
||||
|
||||
/// The command type.
|
||||
final String type;
|
||||
|
||||
/// A matrix with the command's points in different frames.
|
||||
///
|
||||
/// points[i][j] is the i-th point of the command at frame j.
|
||||
final List<List<Point<double>>> points;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PathCommandAnimation(type: $type, points: $points)';
|
||||
}
|
||||
|
||||
String toDart() {
|
||||
String dartCommandClass;
|
||||
switch (type) {
|
||||
case 'M':
|
||||
dartCommandClass = '_PathMoveTo';
|
||||
break;
|
||||
case 'C':
|
||||
dartCommandClass = '_PathCubicTo';
|
||||
break;
|
||||
case 'L':
|
||||
dartCommandClass = '_PathLineTo';
|
||||
break;
|
||||
case 'Z':
|
||||
dartCommandClass = '_PathClose';
|
||||
break;
|
||||
default:
|
||||
throw new Exception('unsupported path command: $type');
|
||||
}
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
sb.write('${kIndent * 4}const $dartCommandClass(\n');
|
||||
for (List<Point<double>> pointFrames in points) {
|
||||
sb.write('${kIndent * 5}const <Offset>[\n');
|
||||
for (Point<double> point in pointFrames)
|
||||
sb.write('${kIndent * 6}const Offset(${point.x}, ${point.y}),\n');
|
||||
sb.write('${kIndent * 5}],\n');
|
||||
}
|
||||
sb.write('${kIndent * 4}),\n');
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Interprets some subset of an SVG file.
|
||||
///
|
||||
/// Recursively goes over the SVG tree, applying transforms and opacities,
|
||||
/// and build a FrameData which is a flat representation of the paths in the SVG
|
||||
/// file, after applying transformations and converting relative coordinates to
|
||||
/// absolute.
|
||||
///
|
||||
/// This does not support the SVG specification, but is just built to
|
||||
/// support SVG files exported by a specific tool the motion design team is
|
||||
/// using.
|
||||
FrameData interpretSvg(String svgFilePath) {
|
||||
final File file = new File(svgFilePath);
|
||||
final String fileData = file.readAsStringSync();
|
||||
final XmlElement svgElement = _extractSvgElement(xml.parse(fileData));
|
||||
final double width = parsePixels(_extractAttr(svgElement, 'width')).toDouble();
|
||||
final double height = parsePixels(_extractAttr(svgElement, 'height')).toDouble();
|
||||
|
||||
final List<SvgPath> paths =
|
||||
_interpretSvgGroup(svgElement.children, new _Transform());
|
||||
return new FrameData(new Point<double>(width, height), paths);
|
||||
}
|
||||
|
||||
List<SvgPath> _interpretSvgGroup(List<XmlNode> children, _Transform transform) {
|
||||
final List<SvgPath> paths = <SvgPath>[];
|
||||
for (XmlNode node in children) {
|
||||
if (node.nodeType != XmlNodeType.ELEMENT)
|
||||
continue;
|
||||
final XmlElement element = node;
|
||||
|
||||
if (element.name.local == 'path') {
|
||||
paths.add(SvgPath.fromElement(element).applyTransform(transform));
|
||||
}
|
||||
|
||||
if (element.name.local == 'g') {
|
||||
double opacity = transform.opacity;
|
||||
if (_hasAttr(element, 'opacity'))
|
||||
opacity *= double.parse(_extractAttr(element, 'opacity'));
|
||||
|
||||
Matrix3 transformMatrix = transform.transformMatrix;
|
||||
if (_hasAttr(element, 'transform'))
|
||||
transformMatrix = transformMatrix.multiplied(
|
||||
_parseSvgTransform(_extractAttr(element, 'transform')));
|
||||
|
||||
final _Transform subtreeTransform = new _Transform(
|
||||
transformMatrix: transformMatrix,
|
||||
opacity: opacity
|
||||
);
|
||||
paths.addAll(_interpretSvgGroup(element.children, subtreeTransform));
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
// Given a points list in the form e.g: "25.0, 1.0 12.0, 12.0 23.0, 9.0" matches
|
||||
// the coordinated of the first point and the rest of the string, for the
|
||||
// example above:
|
||||
// group 1 will match "25.0"
|
||||
// group 2 will match "1.0"
|
||||
// group 3 will match "12.0, 12.0 23.0, 9.0"
|
||||
//
|
||||
// Commas are optional.
|
||||
final RegExp _pointMatcher = new RegExp(r'^ *([\-\.0-9]+) *,? *([\-\.0-9]+)(.*)');
|
||||
|
||||
/// Parse a string with a list of points, e.g:
|
||||
/// '25.0, 1.0 12.0, 12.0 23.0, 9.0' will be parsed to:
|
||||
/// [Point(25.0, 1.0), Point(12.0, 12.0), Point(23.0, 9.0)].
|
||||
///
|
||||
/// Commas are optional.
|
||||
List<Point<double>> parsePoints(String points) {
|
||||
String unParsed = points;
|
||||
final List<Point<double>> result = <Point<double>>[];
|
||||
while (unParsed.isNotEmpty && _pointMatcher.hasMatch(unParsed)) {
|
||||
final Match m = _pointMatcher.firstMatch(unParsed);
|
||||
result.add(new Point<double>(
|
||||
double.parse(m.group(1)),
|
||||
double.parse(m.group(2))
|
||||
));
|
||||
unParsed = m.group(3);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Data for a single animation frame.
|
||||
class FrameData {
|
||||
const FrameData(this.size, this.paths);
|
||||
|
||||
final Point<double> size;
|
||||
final List<SvgPath> paths;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other){
|
||||
if (runtimeType != other.runtimeType)
|
||||
return false;
|
||||
final FrameData typedOther = other;
|
||||
return size == typedOther.size
|
||||
&& const ListEquality<SvgPath>().equals(paths, typedOther.paths);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => size.hashCode ^ paths.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'FrameData(size: $size, paths: $paths)';
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an SVG path element.
|
||||
class SvgPath {
|
||||
const SvgPath(this.id, this.commands, {this.opacity = 1.0});
|
||||
|
||||
final String id;
|
||||
final List<SvgPathCommand> commands;
|
||||
final double opacity;
|
||||
|
||||
static final String _pathCommandAtom = ' *([a-zA-Z]) *([\-\.0-9 ,]*)';
|
||||
static final RegExp _pathCommandValidator = new RegExp('^($_pathCommandAtom)*\$');
|
||||
static final RegExp _pathCommandMatcher = new RegExp(_pathCommandAtom);
|
||||
|
||||
static SvgPath fromElement(XmlElement pathElement) {
|
||||
assert(pathElement.name.local == 'path');
|
||||
final String id = _extractAttr(pathElement, 'id');
|
||||
final String dAttr = _extractAttr(pathElement, 'd');
|
||||
final List<SvgPathCommand> commands = <SvgPathCommand>[];
|
||||
final SvgPathCommandBuilder commandsBuilder = new SvgPathCommandBuilder();
|
||||
if (!_pathCommandValidator.hasMatch(dAttr))
|
||||
throw new Exception('illegal or unsupported path d expression: $dAttr');
|
||||
for (Match match in _pathCommandMatcher.allMatches(dAttr)) {
|
||||
final String commandType = match.group(1);
|
||||
final String pointStr = match.group(2);
|
||||
commands.add(commandsBuilder.build(commandType, parsePoints(pointStr)));
|
||||
}
|
||||
return new SvgPath(id, commands);
|
||||
}
|
||||
|
||||
SvgPath applyTransform(_Transform transform) {
|
||||
final List<SvgPathCommand> transformedCommands =
|
||||
commands.map((SvgPathCommand c) => c.applyTransform(transform)).toList();
|
||||
return new SvgPath(id, transformedCommands, opacity: opacity * transform.opacity);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (runtimeType != other.runtimeType)
|
||||
return false;
|
||||
final SvgPath typedOther = other;
|
||||
return id == typedOther.id
|
||||
&& opacity == typedOther.opacity
|
||||
&& const ListEquality<SvgPathCommand>().equals(commands, typedOther.commands);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode ^ commands.hashCode ^ opacity.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SvgPath(id: $id, opacity: $opacity, commands: $commands)';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Represents a single SVG path command from an SVG d element.
|
||||
///
|
||||
/// This class normalizes all the 'd' commands into a single type, that has
|
||||
/// a command type and a list of points.
|
||||
///
|
||||
/// Some examples of how d commands translated to SvgPathCommand:
|
||||
/// * "M 0.0, 1.0" => SvgPathCommand('M', [Point(0.0, 1.0)])
|
||||
/// * "Z" => SvgPathCommand('Z', [])
|
||||
/// * "C 1.0, 1.0 2.0, 2.0 3.0, 3.0" SvgPathCommand('C', [Point(1.0, 1.0),
|
||||
/// Point(2.0, 2.0), Point(3.0, 3.0)])
|
||||
class SvgPathCommand {
|
||||
const SvgPathCommand(this.type, this.points);
|
||||
|
||||
/// The command type.
|
||||
final String type;
|
||||
|
||||
/// List of points used by this command.
|
||||
final List<Point<double>> points;
|
||||
|
||||
SvgPathCommand applyTransform(_Transform transform) {
|
||||
final List<Point<double>> transformedPoints =
|
||||
_vector3ArrayToPoints(
|
||||
transform.transformMatrix.applyToVector3Array(
|
||||
_pointsToVector3Array(points)
|
||||
)
|
||||
);
|
||||
return new SvgPathCommand(type, transformedPoints);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (runtimeType != other.runtimeType)
|
||||
return false;
|
||||
final SvgPathCommand typedOther = other;
|
||||
return type == typedOther.type
|
||||
&& const ListEquality<Point<double>>().equals(points, typedOther.points);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => type.hashCode ^ points.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SvgPathCommand(type: $type, points: $points)';
|
||||
}
|
||||
}
|
||||
|
||||
class SvgPathCommandBuilder {
|
||||
static const Map<String, Null> kRelativeCommands = const <String, Null> {
|
||||
'c': null,
|
||||
'l': null,
|
||||
'm': null,
|
||||
't': null,
|
||||
's': null,
|
||||
};
|
||||
|
||||
Point<double> lastPoint = const Point<double>(0.0, 0.0);
|
||||
Point<double> subPathStartPoint = const Point<double>(0.0, 0.0);
|
||||
|
||||
SvgPathCommand build(String type, List<Point<double>> points) {
|
||||
List<Point<double>> absPoints = points;
|
||||
if (_isRelativeCommand(type)) {
|
||||
absPoints = points.map((Point<double> p) => p + lastPoint).toList();
|
||||
}
|
||||
|
||||
if (type == 'M' || type == 'm')
|
||||
subPathStartPoint = absPoints.last;
|
||||
|
||||
if (type == 'Z' || type == 'z')
|
||||
lastPoint = subPathStartPoint;
|
||||
else
|
||||
lastPoint = absPoints.last;
|
||||
|
||||
return new SvgPathCommand(type.toUpperCase(), absPoints);
|
||||
}
|
||||
|
||||
static bool _isRelativeCommand(String type) {
|
||||
return kRelativeCommands.containsKey(type);
|
||||
}
|
||||
}
|
||||
|
||||
List<double> _pointsToVector3Array(List<Point<double>> points) {
|
||||
final List<double> result = new List<double>(points.length * 3);
|
||||
for (int i = 0; i < points.length; i += 1) {
|
||||
result[i * 3] = points[i].x;
|
||||
result[i * 3 + 1] = points[i].y;
|
||||
result[i * 3 + 2] = 1.0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
List<Point<double>> _vector3ArrayToPoints(List<double> vector) {
|
||||
final int numPoints = (vector.length / 3).floor();
|
||||
final List<Point<double>> points = new List<Point<double>>(numPoints);
|
||||
for (int i = 0; i < numPoints; i += 1) {
|
||||
points[i] = new Point<double>(vector[i*3], vector[i*3 + 1]);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
/// Represents a transformation to apply on an SVG subtree.
|
||||
///
|
||||
/// This includes more transforms than the ones described by the SVG transform
|
||||
/// attribute, e.g opacity.
|
||||
class _Transform {
|
||||
|
||||
/// Constructs a new _Transform, default arguments create a no-op transform.
|
||||
_Transform({Matrix3 transformMatrix, this.opacity = 1.0}) :
|
||||
this.transformMatrix = transformMatrix ?? new Matrix3.identity();
|
||||
|
||||
final Matrix3 transformMatrix;
|
||||
final double opacity;
|
||||
|
||||
_Transform applyTransform(_Transform transform) {
|
||||
return new _Transform(
|
||||
transformMatrix: transform.transformMatrix.multiplied(transformMatrix),
|
||||
opacity: transform.opacity * opacity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final String _transformCommandAtom = ' *([^(]+)\\(([^)]*)\\)';
|
||||
final RegExp _transformValidator = new RegExp('^($_transformCommandAtom)*\$');
|
||||
final RegExp _transformCommand = new RegExp(_transformCommandAtom);
|
||||
|
||||
Matrix3 _parseSvgTransform(String transform){
|
||||
if (!_transformValidator.hasMatch(transform))
|
||||
throw new Exception('illegal or unsupported transform: $transform');
|
||||
final Iterable<Match> matches =_transformCommand.allMatches(transform).toList().reversed;
|
||||
Matrix3 result = new Matrix3.identity();
|
||||
for (Match m in matches) {
|
||||
final String command = m.group(1);
|
||||
final String params = m.group(2);
|
||||
if (command == 'translate') {
|
||||
result = _parseSvgTranslate(params).multiplied(result);
|
||||
continue;
|
||||
}
|
||||
if (command == 'scale') {
|
||||
result = _parseSvgScale(params).multiplied(result);
|
||||
continue;
|
||||
}
|
||||
if (command == 'rotate') {
|
||||
result = _parseSvgRotate(params).multiplied(result);
|
||||
continue;
|
||||
}
|
||||
throw new Exception('unimplemented transform: $command');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
final RegExp _valueSeparator = new RegExp('( *, *| +)');
|
||||
|
||||
Matrix3 _parseSvgTranslate(String paramsStr) {
|
||||
final List<String> params = paramsStr.split(_valueSeparator);
|
||||
assert(params.isNotEmpty);
|
||||
assert(params.length <= 2);
|
||||
final double x = double.parse(params[0]);
|
||||
final double y = params.length < 2 ? 0 : double.parse(params[1]);
|
||||
return _matrix(1.0, 0.0, 0.0, 1.0, x, y);
|
||||
}
|
||||
|
||||
Matrix3 _parseSvgScale(String paramsStr) {
|
||||
final List<String> params = paramsStr.split(_valueSeparator);
|
||||
assert(params.isNotEmpty);
|
||||
assert(params.length <= 2);
|
||||
final double x = double.parse(params[0]);
|
||||
final double y = params.length < 2 ? 0 : double.parse(params[1]);
|
||||
return _matrix(x, 0.0, 0.0, y, 0.0, 0.0);
|
||||
}
|
||||
|
||||
Matrix3 _parseSvgRotate(String paramsStr) {
|
||||
final List<String> params = paramsStr.split(_valueSeparator);
|
||||
assert(params.length == 1);
|
||||
final double a = radians(double.parse(params[0]));
|
||||
return _matrix(cos(a), sin(a), -sin(a), cos(a), 0.0, 0.0);
|
||||
}
|
||||
|
||||
Matrix3 _matrix(double a, double b, double c, double d, double e, double f) {
|
||||
return new Matrix3(a, b, 0.0, c, d, 0.0, e, f, 1.0);
|
||||
}
|
||||
|
||||
// Matches a pixels expression e.g "14px".
|
||||
// First group is just the number.
|
||||
final RegExp _pixelsExp = new RegExp('^([0-9]+)px\$');
|
||||
|
||||
/// Parses a pixel expression, e.g "14px", and returns the number.
|
||||
/// Throws an [ArgumentError] if the given string doesn't match the pattern.
|
||||
int parsePixels(String pixels) {
|
||||
if (!_pixelsExp.hasMatch(pixels))
|
||||
throw new ArgumentError(
|
||||
'illegal pixels expression: \'$pixels\''
|
||||
' (the tool currently only support pixel units).');
|
||||
return int.parse(_pixelsExp.firstMatch(pixels).group(1));
|
||||
}
|
||||
|
||||
String _extractAttr(XmlElement element, String name) {
|
||||
try {
|
||||
return element.attributes.singleWhere((XmlAttribute x) => x.name.local == name)
|
||||
.value;
|
||||
} catch (e) {
|
||||
throw new ArgumentError(
|
||||
'Can\'t find a single \'$name\' attributes in ${element.name}, '
|
||||
'attributes were: ${element.attributes}'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasAttr(XmlElement element, String name) {
|
||||
return element.attributes.where((XmlAttribute a) => a.name.local == name).isNotEmpty;
|
||||
}
|
||||
|
||||
XmlElement _extractSvgElement(XmlDocument document) {
|
||||
return document.children.singleWhere(
|
||||
(XmlNode node) => node.nodeType == XmlNodeType.ELEMENT &&
|
||||
_asElement(node).name.local == 'svg'
|
||||
);
|
||||
}
|
||||
|
||||
XmlElement _asElement(XmlNode node) => node;
|
54
dev/tools/vitool/pubspec.yaml
Normal file
@ -0,0 +1,54 @@
|
||||
name: vitool
|
||||
description: A tool for generating Dart vector animation code from SVG sequences.
|
||||
version: 0.0.1
|
||||
homepage: https://flutter.io
|
||||
author: Flutter Authors <flutter-dev@googlegroups.com>
|
||||
|
||||
environment:
|
||||
sdk: '>=1.20.1 <2.0.0'
|
||||
|
||||
dependencies:
|
||||
args: 0.13.7
|
||||
vector_math: 2.0.5
|
||||
xml: 2.6.0
|
||||
|
||||
dev_dependencies:
|
||||
test: 0.12.26
|
||||
|
||||
async: 1.13.3 # TRANSITIVE DEPENDENCY
|
||||
barback: 0.15.2+13 # TRANSITIVE DEPENDENCY
|
||||
boolean_selector: 1.0.2 # TRANSITIVE DEPENDENCY
|
||||
charcode: 1.1.1 # TRANSITIVE DEPENDENCY
|
||||
collection: 1.14.3 # TRANSITIVE DEPENDENCY
|
||||
convert: 2.0.1 # TRANSITIVE DEPENDENCY
|
||||
crypto: 2.0.2+1 # TRANSITIVE DEPENDENCY
|
||||
glob: 1.1.5 # TRANSITIVE DEPENDENCY
|
||||
http: 0.11.3+14 # TRANSITIVE DEPENDENCY
|
||||
http_multi_server: 2.0.4 # TRANSITIVE DEPENDENCY
|
||||
http_parser: 3.1.1 # TRANSITIVE DEPENDENCY
|
||||
io: 0.3.1 # TRANSITIVE DEPENDENCY
|
||||
js: 0.6.1 # TRANSITIVE DEPENDENCY
|
||||
matcher: 0.12.1+4 # TRANSITIVE DEPENDENCY
|
||||
meta: 1.1.1 # TRANSITIVE DEPENDENCY
|
||||
mime: 0.9.5 # TRANSITIVE DEPENDENCY
|
||||
node_preamble: 1.4.0 # TRANSITIVE DEPENDENCY
|
||||
package_config: 1.0.3 # TRANSITIVE DEPENDENCY
|
||||
package_resolver: 1.0.2 # TRANSITIVE DEPENDENCY
|
||||
path: 1.5.1 # TRANSITIVE DEPENDENCY
|
||||
petitparser: 1.6.1 # TRANSITIVE DEPENDENCY
|
||||
pool: 1.3.3 # TRANSITIVE DEPENDENCY
|
||||
pub_semver: 1.3.2 # TRANSITIVE DEPENDENCY
|
||||
shelf: 0.7.1 # TRANSITIVE DEPENDENCY
|
||||
shelf_packages_handler: 1.0.3 # TRANSITIVE DEPENDENCY
|
||||
shelf_static: 0.2.6 # TRANSITIVE DEPENDENCY
|
||||
shelf_web_socket: 0.2.2 # TRANSITIVE DEPENDENCY
|
||||
source_map_stack_trace: 1.1.4 # TRANSITIVE DEPENDENCY
|
||||
source_maps: 0.10.4 # TRANSITIVE DEPENDENCY
|
||||
source_span: 1.4.0 # TRANSITIVE DEPENDENCY
|
||||
stack_trace: 1.9.1 # TRANSITIVE DEPENDENCY
|
||||
stream_channel: 1.6.2 # TRANSITIVE DEPENDENCY
|
||||
string_scanner: 1.0.2 # TRANSITIVE DEPENDENCY
|
||||
term_glyph: 1.0.0 # TRANSITIVE DEPENDENCY
|
||||
typed_data: 1.1.4 # TRANSITIVE DEPENDENCY
|
||||
web_socket_channel: 1.0.6 # TRANSITIVE DEPENDENCY
|
||||
yaml: 2.1.13 # TRANSITIVE DEPENDENCY
|
698
dev/tools/vitool/test/vitool_test.dart
Normal file
@ -0,0 +1,698 @@
|
||||
// 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.
|
||||
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:vitool/vitool.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
const String kPackagePath = '..';
|
||||
|
||||
void main() {
|
||||
|
||||
test('parsePixels', () {
|
||||
expect(parsePixels('23px'), 23);
|
||||
expect(parsePixels('9px'), 9);
|
||||
expect(() { parsePixels('9pt'); }, throwsA(const isInstanceOf<ArgumentError>()));
|
||||
});
|
||||
|
||||
test('parsePoints', () {
|
||||
expect(parsePoints('1.0, 2.0'),
|
||||
const <Point<double>>[const Point<double>(1.0, 2.0)]
|
||||
);
|
||||
expect(parsePoints('12.0, 34.0 5.0, 6.6'),
|
||||
const <Point<double>>[
|
||||
const Point<double>(12.0, 34.0),
|
||||
const Point<double>(5.0, 6.6),
|
||||
]
|
||||
);
|
||||
expect(parsePoints('12.0 34.0 5.0 6.6'),
|
||||
const <Point<double>>[
|
||||
const Point<double>(12.0, 34.0),
|
||||
const Point<double>(5.0, 6.6),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
group('parseSvg', () {
|
||||
test('empty SVGs', () {
|
||||
interpretSvg(testAsset('empty_svg_1_48x48.svg'));
|
||||
interpretSvg(testAsset('empty_svg_2_100x50.svg'));
|
||||
});
|
||||
|
||||
test('illegal SVGs', () {
|
||||
expect(
|
||||
() { interpretSvg(testAsset('illegal_svg_multiple_roots.svg')); },
|
||||
throwsA(anything)
|
||||
);
|
||||
});
|
||||
|
||||
test('SVG size', () {
|
||||
expect(
|
||||
interpretSvg(testAsset('empty_svg_1_48x48.svg')).size,
|
||||
const Point<double>(48.0, 48.0)
|
||||
);
|
||||
|
||||
expect(
|
||||
interpretSvg(testAsset('empty_svg_2_100x50.svg')).size,
|
||||
const Point<double>(100.0, 50.0)
|
||||
);
|
||||
});
|
||||
|
||||
test('horizontal bar', () {
|
||||
final FrameData frameData = interpretSvg(testAsset('horizontal_bar.svg'));
|
||||
expect(frameData.paths, <SvgPath>[
|
||||
const SvgPath('path_1', const<SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 19.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 19.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 29.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(0.0, 29.0)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
test('leading space path command', () {
|
||||
interpretSvg(testAsset('leading_space_path_command.svg'));
|
||||
});
|
||||
|
||||
test('SVG illegal path', () {
|
||||
expect(
|
||||
() { interpretSvg(testAsset('illegal_path.svg')); },
|
||||
throwsA(anything)
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
test('SVG group', () {
|
||||
final FrameData frameData = interpretSvg(testAsset('bars_group.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
const SvgPath('path_1', const<SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 19.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 19.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 29.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(0.0, 29.0)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
]),
|
||||
const SvgPath('path_2', const<SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 34.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 34.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 44.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(0.0, 44.0)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
test('SVG group translate', () {
|
||||
final FrameData frameData = interpretSvg(testAsset('bar_group_translate.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
const SvgPath('path_1', const<SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 34.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 34.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 44.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(0.0, 44.0)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
test('SVG group scale', () {
|
||||
final FrameData frameData = interpretSvg(testAsset('bar_group_scale.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
const SvgPath(
|
||||
'path_1', const<SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 9.5)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(24.0, 9.5)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(24.0, 14.5)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(0.0, 14.5)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
test('SVG group rotate scale', () {
|
||||
final FrameData frameData = interpretSvg(testAsset('bar_group_rotate_scale.svg'));
|
||||
expect(frameData.paths, const <PathMatcher>[
|
||||
const PathMatcher(
|
||||
const SvgPath(
|
||||
'path_1', const<SvgPathCommand>[
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(29.0, 0.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(29.0, 48.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(19.0, 48.0)]),
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(19.0, 0.0)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
]),
|
||||
margin: 0.000000001
|
||||
)
|
||||
]);
|
||||
});
|
||||
|
||||
test('SVG illegal transform', () {
|
||||
expect(
|
||||
() { interpretSvg(testAsset('illegal_transform.svg')); },
|
||||
throwsA(anything)
|
||||
);
|
||||
});
|
||||
|
||||
test('SVG group opacity', () {
|
||||
final FrameData frameData = interpretSvg(testAsset('bar_group_opacity.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
const SvgPath(
|
||||
'path_1',
|
||||
const<SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 19.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 19.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 29.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(0.0, 29.0)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
],
|
||||
opacity: 0.5,
|
||||
),
|
||||
]);
|
||||
});
|
||||
|
||||
test('horizontal bar relative', () {
|
||||
// This asset uses the relative 'l' command instead of 'L'.
|
||||
final FrameData frameData = interpretSvg(testAsset('horizontal_bar_relative.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
const SvgPath(
|
||||
'path_1', const<SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 19.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 19.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(48.0, 29.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(0.0, 29.0)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
|
||||
test('close in middle of path', () {
|
||||
// This asset uses the relative 'l' command instead of 'L'.
|
||||
final FrameData frameData = interpretSvg(testAsset('close_path_in_middle.svg'));
|
||||
expect(frameData.paths, const <SvgPath>[
|
||||
const SvgPath(
|
||||
'path_1', const<SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(50.0, 50.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(60.0, 50.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(60.0, 60.0)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(50.0, 40.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(40.0, 40.0)]),
|
||||
const SvgPathCommand('Z', const <Point<double>>[]),
|
||||
]),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
group('create PathAnimation', () {
|
||||
test('single path', () {
|
||||
final List<FrameData> frameData = const <FrameData>[
|
||||
const FrameData(
|
||||
const Point<double>(10.0, 10.0),
|
||||
const <SvgPath>[
|
||||
const SvgPath(
|
||||
'path_1',
|
||||
const <SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 0.0)]),
|
||||
const SvgPathCommand('L', const <Point<double>>[const Point<double>(10.0, 10.0)]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
expect(new PathAnimation.fromFrameData(frameData, 0),
|
||||
const PathAnimationMatcher(const PathAnimation(
|
||||
const <PathCommandAnimation>[
|
||||
const PathCommandAnimation('M', const <List<Point<double>>>[
|
||||
const <Point<double>>[const Point<double>(0.0, 0.0)],
|
||||
]),
|
||||
const PathCommandAnimation('L', const <List<Point<double>>>[
|
||||
const <Point<double>>[const Point<double>(10.0, 10.0)],
|
||||
]),
|
||||
],
|
||||
opacities: const <double>[1.0]
|
||||
))
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple paths', () {
|
||||
final List<FrameData> frameData = const <FrameData>[
|
||||
const FrameData(
|
||||
const Point<double>(10.0, 10.0),
|
||||
const <SvgPath>[
|
||||
const SvgPath(
|
||||
'path_1',
|
||||
const <SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 0.0)]),
|
||||
],
|
||||
),
|
||||
const SvgPath(
|
||||
'path_2',
|
||||
const <SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(5.0, 6.0)]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
expect(new PathAnimation.fromFrameData(frameData, 0),
|
||||
const PathAnimationMatcher(const PathAnimation(
|
||||
const <PathCommandAnimation>[
|
||||
const PathCommandAnimation('M', const <List<Point<double>>>[
|
||||
const <Point<double>>[const Point<double>(0.0, 0.0)],
|
||||
])
|
||||
],
|
||||
opacities: const <double>[1.0]
|
||||
))
|
||||
);
|
||||
|
||||
expect(new PathAnimation.fromFrameData(frameData, 1),
|
||||
const PathAnimationMatcher(const PathAnimation(
|
||||
const <PathCommandAnimation>[
|
||||
const PathCommandAnimation('M', const <List<Point<double>>>[
|
||||
const <Point<double>>[const Point<double>(5.0, 6.0)],
|
||||
])
|
||||
],
|
||||
opacities: const <double>[1.0]
|
||||
))
|
||||
);
|
||||
});
|
||||
|
||||
test('multiple frames', () {
|
||||
final List<FrameData> frameData = const <FrameData>[
|
||||
const FrameData(
|
||||
const Point<double>(10.0, 10.0),
|
||||
const <SvgPath>[
|
||||
const SvgPath(
|
||||
'path_1',
|
||||
const <SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 0.0)])
|
||||
],
|
||||
opacity: 0.5,
|
||||
),
|
||||
],
|
||||
),
|
||||
const FrameData(
|
||||
const Point<double>(10.0, 10.0),
|
||||
const <SvgPath>[
|
||||
const SvgPath(
|
||||
'path_1',
|
||||
const <SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(10.0, 10.0)])
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
expect(new PathAnimation.fromFrameData(frameData, 0),
|
||||
const PathAnimationMatcher(const PathAnimation(
|
||||
const <PathCommandAnimation>[
|
||||
const PathCommandAnimation('M', const <List<Point<double>>>[
|
||||
const <Point<double>>[
|
||||
const Point<double>(0.0, 0.0),
|
||||
const Point<double>(10.0, 10.0),
|
||||
],
|
||||
]),
|
||||
],
|
||||
opacities: const <double>[0.5, 1.0]
|
||||
))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('create Animation', () {
|
||||
test('multiple paths', () {
|
||||
final List<FrameData> frameData = const <FrameData>[
|
||||
const FrameData(
|
||||
const Point<double>(10.0, 10.0),
|
||||
const <SvgPath>[
|
||||
const SvgPath(
|
||||
'path_1',
|
||||
const <SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(0.0, 0.0)]),
|
||||
],
|
||||
),
|
||||
const SvgPath(
|
||||
'path_1',
|
||||
const <SvgPathCommand>[
|
||||
const SvgPathCommand('M', const <Point<double>>[const Point<double>(5.0, 6.0)]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
final Animation animation = new Animation.fromFrameData(frameData);
|
||||
expect(animation.paths[0],
|
||||
const PathAnimationMatcher(const PathAnimation(
|
||||
const <PathCommandAnimation>[
|
||||
const PathCommandAnimation('M', const <List<Point<double>>>[
|
||||
const <Point<double>>[const Point<double>(0.0, 0.0)],
|
||||
])
|
||||
],
|
||||
opacities: const <double>[1.0]
|
||||
))
|
||||
);
|
||||
|
||||
expect(animation.paths[1],
|
||||
const PathAnimationMatcher(const PathAnimation(
|
||||
const <PathCommandAnimation>[
|
||||
const PathCommandAnimation('M', const <List<Point<double>>>[
|
||||
const <Point<double>>[const Point<double>(5.0, 6.0)],
|
||||
])
|
||||
],
|
||||
opacities: const <double>[1.0]
|
||||
))
|
||||
);
|
||||
|
||||
expect(animation.size, const Point<double>(10.0, 10.0));
|
||||
});
|
||||
});
|
||||
|
||||
group('toDart', () {
|
||||
test('_PathMoveTo', () {
|
||||
final PathCommandAnimation command = const PathCommandAnimation(
|
||||
'M',
|
||||
const <List<Point<double>>>[
|
||||
const <Point<double>>[
|
||||
const Point<double>(1.0, 2.0),
|
||||
const Point<double>(3.0, 4.0),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
expect(command.toDart(),
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(1.0, 2.0),\n'
|
||||
' const Offset(3.0, 4.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
test('_PathLineTo', () {
|
||||
final PathCommandAnimation command = const PathCommandAnimation(
|
||||
'L',
|
||||
const <List<Point<double>>>[
|
||||
const <Point<double>>[
|
||||
const Point<double>(1.0, 2.0),
|
||||
const Point<double>(3.0, 4.0),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
expect(command.toDart(),
|
||||
' const _PathLineTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(1.0, 2.0),\n'
|
||||
' const Offset(3.0, 4.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
test('_PathCubicTo', () {
|
||||
final PathCommandAnimation command = const PathCommandAnimation(
|
||||
'C',
|
||||
const <List<Point<double>>>[
|
||||
const <Point<double>>[
|
||||
const Point<double>(16.0, 24.0),
|
||||
const Point<double>(16.0, 10.0),
|
||||
],
|
||||
const <Point<double>>[
|
||||
const Point<double>(16.0, 25.0),
|
||||
const Point<double>(16.0, 11.0),
|
||||
],
|
||||
const <Point<double>>[
|
||||
const Point<double>(40.0, 40.0),
|
||||
const Point<double>(40.0, 40.0),
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
expect(command.toDart(),
|
||||
' const _PathCubicTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(16.0, 24.0),\n'
|
||||
' const Offset(16.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(16.0, 25.0),\n'
|
||||
' const Offset(16.0, 11.0),\n'
|
||||
' ],\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(40.0, 40.0),\n'
|
||||
' const Offset(40.0, 40.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
test('_PathClose', () {
|
||||
final PathCommandAnimation command = const PathCommandAnimation(
|
||||
'Z',
|
||||
const <List<Point<double>>>[],
|
||||
);
|
||||
|
||||
expect(command.toDart(),
|
||||
' const _PathClose(\n'
|
||||
' ),\n'
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
test('Unsupported path command', () {
|
||||
final PathCommandAnimation command = const PathCommandAnimation(
|
||||
'h',
|
||||
const <List<Point<double>>>[],
|
||||
);
|
||||
|
||||
expect(
|
||||
() { command.toDart(); },
|
||||
throwsA(anything)
|
||||
);
|
||||
});
|
||||
|
||||
test('_PathFrames', () {
|
||||
final PathAnimation pathAnimation = const PathAnimation(
|
||||
const <PathCommandAnimation>[
|
||||
const PathCommandAnimation('M', const <List<Point<double>>>[
|
||||
const <Point<double>>[
|
||||
const Point<double>(0.0, 0.0),
|
||||
const Point<double>(10.0, 10.0),
|
||||
],
|
||||
]),
|
||||
const PathCommandAnimation('L', const <List<Point<double>>>[
|
||||
const <Point<double>>[
|
||||
const Point<double>(48.0, 10.0),
|
||||
const Point<double>(0.0, 0.0),
|
||||
],
|
||||
]),
|
||||
],
|
||||
opacities: const <double>[0.5, 1.0]
|
||||
);
|
||||
|
||||
expect(pathAnimation.toDart(),
|
||||
' const _PathFrames(\n'
|
||||
' opacities: const <double>[\n'
|
||||
' 0.5,\n'
|
||||
' 1.0,\n'
|
||||
' ],\n'
|
||||
' commands: const <_PathCommand>[\n'
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' const Offset(10.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' const _PathLineTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(48.0, 10.0),\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
);
|
||||
});
|
||||
|
||||
test('Animation', () {
|
||||
final Animation animation = const Animation(
|
||||
const Point<double>(48.0, 48.0),
|
||||
const <PathAnimation>[
|
||||
const PathAnimation(
|
||||
const <PathCommandAnimation>[
|
||||
const PathCommandAnimation('M', const <List<Point<double>>>[
|
||||
const <Point<double>>[
|
||||
const Point<double>(0.0, 0.0),
|
||||
const Point<double>(10.0, 10.0),
|
||||
],
|
||||
]),
|
||||
const PathCommandAnimation('L', const <List<Point<double>>>[
|
||||
const <Point<double>>[
|
||||
const Point<double>(48.0, 10.0),
|
||||
const Point<double>(0.0, 0.0),
|
||||
],
|
||||
]),
|
||||
],
|
||||
opacities: const <double>[0.5, 1.0]
|
||||
),
|
||||
|
||||
const PathAnimation(
|
||||
const <PathCommandAnimation>[
|
||||
const PathCommandAnimation('M', const <List<Point<double>>>[
|
||||
const <Point<double>>[
|
||||
const Point<double>(0.0, 0.0),
|
||||
const Point<double>(10.0, 10.0),
|
||||
],
|
||||
]),
|
||||
],
|
||||
opacities: const <double>[0.5, 1.0]
|
||||
),
|
||||
]);
|
||||
|
||||
expect(animation.toDart('_AnimatedIconData', '_\$data1'),
|
||||
'const _AnimatedIconData _\$data1 = const _AnimatedIconData(\n'
|
||||
' const Size(48.0, 48.0),\n'
|
||||
' const <_PathFrames>[\n'
|
||||
' const _PathFrames(\n'
|
||||
' opacities: const <double>[\n'
|
||||
' 0.5,\n'
|
||||
' 1.0,\n'
|
||||
' ],\n'
|
||||
' commands: const <_PathCommand>[\n'
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' const Offset(10.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' const _PathLineTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(48.0, 10.0),\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' const _PathFrames(\n'
|
||||
' opacities: const <double>[\n'
|
||||
' 0.5,\n'
|
||||
' 1.0,\n'
|
||||
' ],\n'
|
||||
' commands: const <_PathCommand>[\n'
|
||||
' const _PathMoveTo(\n'
|
||||
' const <Offset>[\n'
|
||||
' const Offset(0.0, 0.0),\n'
|
||||
' const Offset(10.0, 10.0),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
' ),\n'
|
||||
' ],\n'
|
||||
');'
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Matches all path commands' points within an error margin.
|
||||
class PathMatcher extends Matcher {
|
||||
const PathMatcher(this.actual, {this.margin = 0.0});
|
||||
|
||||
final SvgPath actual;
|
||||
final double margin;
|
||||
|
||||
@override
|
||||
Description describe(Description description) => description.add('$actual (±$margin)');
|
||||
|
||||
@override
|
||||
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
||||
if (item == null || actual == null)
|
||||
return item == actual;
|
||||
|
||||
if (item.runtimeType != actual.runtimeType)
|
||||
return false;
|
||||
|
||||
final SvgPath other = item;
|
||||
if (other.id != actual.id || other.opacity != actual.opacity)
|
||||
return false;
|
||||
|
||||
if (other.commands.length != actual.commands.length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < other.commands.length; i += 1) {
|
||||
if (!commandsMatch(actual.commands[i], other.commands[i]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool commandsMatch(SvgPathCommand actual, SvgPathCommand other) {
|
||||
if (other.points.length != actual.points.length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < other.points.length; i += 1) {
|
||||
if ((other.points[i].x - actual.points[i].x).abs() > margin)
|
||||
return false;
|
||||
if ((other.points[i].y - actual.points[i].y).abs() > margin)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class PathAnimationMatcher extends Matcher {
|
||||
const PathAnimationMatcher(this.expected);
|
||||
|
||||
final PathAnimation expected;
|
||||
|
||||
@override
|
||||
Description describe(Description description) => description.add('$expected');
|
||||
|
||||
@override
|
||||
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
||||
if (item == null || expected == null)
|
||||
return item == expected;
|
||||
|
||||
if (item.runtimeType != expected.runtimeType)
|
||||
return false;
|
||||
|
||||
final PathAnimation other = item;
|
||||
|
||||
if (!const ListEquality<double>().equals(other.opacities, expected.opacities))
|
||||
return false;
|
||||
|
||||
if (other.commands.length != expected.commands.length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < other.commands.length; i += 1) {
|
||||
if (!commandsMatch(expected.commands[i], other.commands[i]))
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool commandsMatch(PathCommandAnimation expected, PathCommandAnimation other) {
|
||||
if (other.points.length != expected.points.length)
|
||||
return false;
|
||||
|
||||
for (int i = 0; i < other.points.length; i += 1)
|
||||
if (!const ListEquality<Point<double>>().equals(other.points[i], expected.points[i]))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
String testAsset(String name) {
|
||||
return path.join(kPackagePath, 'test_assets', name);
|
||||
}
|
||||
|
5
dev/tools/vitool/test_assets/bar_group_opacity.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<g if="group_2" opacity="0.5" >
|
||||
<path id="path_1" d="M 0,19.0 L 48.0, 19.0 L 48.0, 29.0 L 0, 29.0 Z " fill="#000000" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 227 B |
5
dev/tools/vitool/test_assets/bar_group_rotate_scale.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<g if="group_2" transform="translate(48.0, 0) rotate(90.0)">
|
||||
<path id="path_1" d="M 0,19.0 L 48.0, 19.0 L 48.0, 29.0 L 0, 29.0 Z " fill="#000000" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 256 B |
5
dev/tools/vitool/test_assets/bar_group_scale.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<g if="group_2" transform="scale(0.5,0.5)">
|
||||
<path id="path_1" d="M 0,19.0 L 48.0, 19.0 L 48.0, 29.0 L 0, 29.0 Z " fill="#000000" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 239 B |
5
dev/tools/vitool/test_assets/bar_group_translate.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<g if="group_2" transform="translate(0,15.0)">
|
||||
<path id="path_1" d="M 0,19.0 L 48.0, 19.0 L 48.0, 29.0 L 0, 29.0 Z " fill="#000000" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 242 B |
6
dev/tools/vitool/test_assets/bars_group.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<g if="group_1">
|
||||
<path id="path_1" d="M 0,19.0 L 48.0, 19.0 L 48.0, 29.0 L 0, 29.0 Z " fill="#000000" />
|
||||
<path id="path_2" d="M 0,34.0 L 48.0, 34.0 L 48.0, 44.0 L 0, 44.0 Z " fill="#000000" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 304 B |
3
dev/tools/vitool/test_assets/close_path_in_middle.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="100px" height="100px" >
|
||||
<path id="path_1" d="M 50.0 50.0 l 10.0 0 l 0.0 10.0 z l 0.0 -10.0 l -10.0 0.0 z " fill="#000000" />
|
||||
</svg>
|
After Width: | Height: | Size: 197 B |
2
dev/tools/vitool/test_assets/empty_svg_1_48x48.svg
Normal file
@ -0,0 +1,2 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
</svg>
|
After Width: | Height: | Size: 94 B |
8
dev/tools/vitool/test_assets/empty_svg_2_100x50.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Empty SVG file -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
id="empty_svg"
|
||||
width="100px"
|
||||
height="50px">
|
||||
</svg>
|
After Width: | Height: | Size: 182 B |
3
dev/tools/vitool/test_assets/horizontal_bar.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<path id="path_1" d="M 0,19.0 L 48.0, 19.0 L 48.0, 29.0 L 0, 29.0 Z " fill="#000000" />
|
||||
</svg>
|
After Width: | Height: | Size: 182 B |
3
dev/tools/vitool/test_assets/horizontal_bar_relative.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<path id="path_1" d="M 0,19.0 l 48.0, 0.0 l 0.0, 10.0 l -48.0, 0.0 Z " fill="#000000" />
|
||||
</svg>
|
After Width: | Height: | Size: 183 B |
3
dev/tools/vitool/test_assets/illegal_path.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<path id="path_1" d="M 1.0 !!!" fill="#000000" />
|
||||
</svg>
|
After Width: | Height: | Size: 144 B |
@ -0,0 +1,3 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
</svg>
|
||||
<svg/>
|
After Width: | Height: | Size: 101 B |
5
dev/tools/vitool/test_assets/illegal_transform.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<g if="group_2" transform="translate(0,15.0) !!">
|
||||
<path id="path_1" d="M 0,19.0 L 48.0, 19.0 L 48.0, 29.0 L 0, 29.0 Z " fill="#000000" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 245 B |
@ -0,0 +1,3 @@
|
||||
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
|
||||
<path id="path_1" d=" z" fill="#000000" />
|
||||
</svg>
|
After Width: | Height: | Size: 137 B |
28
packages/flutter/lib/material_animated_icons.dart
Normal file
@ -0,0 +1,28 @@
|
||||
// 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.
|
||||
|
||||
/// Flutter widgets implementing Material Design animated icons.
|
||||
///
|
||||
/// To use, import `package:flutter/material_animated_icons.dart`.
|
||||
library material_animated_icons;
|
||||
|
||||
import 'dart:math' as math show pi;
|
||||
import 'dart:ui' as ui show Paint, Path, Canvas;
|
||||
import 'dart:ui' show lerpDouble;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
// This package is split into multiple parts to enable a private API that is
|
||||
// testable.
|
||||
|
||||
// Public API.
|
||||
part 'src/material_animated_icons/animated_icons.dart';
|
||||
// Provides a public interface for referring to the private icon
|
||||
// implementations.
|
||||
part 'src/material_animated_icons/animated_icons_data.dart';
|
||||
|
||||
// Animated icons data files.
|
||||
part 'src/material_animated_icons/data/arrow_menu.g.dart';
|
||||
part 'src/material_animated_icons/data/menu_arrow.g.dart';
|
@ -0,0 +1,299 @@
|
||||
// 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.
|
||||
///
|
||||
/// If the text diection is [TextDirection.rtl], the icon will be mirrored
|
||||
/// 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...
|
||||
///
|
||||
/// _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(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.
|
||||
//
|
||||
// [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 T _Interpolator<T>(T a, T b, double progress);
|
@ -0,0 +1,54 @@
|
||||
// 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.
|
||||
|
||||
// This file serves as the interface between the public and private APIs for
|
||||
// animated icons.
|
||||
// The AnimatedIcons class is public and is used to specify available icons,
|
||||
// while the _AnimatedIconData interface which used to deliver the icon data is
|
||||
// kept private.
|
||||
|
||||
part of material_animated_icons;
|
||||
|
||||
/// Identifier for the supported material design animated icons.
|
||||
///
|
||||
/// Use with [AnimatedIcon] class to show specific animated icons.
|
||||
abstract class AnimatedIcons {
|
||||
/// The material design arrow to menu icon animation.
|
||||
static const AnimatedIconData arrow_menu = _$arrow_menu;
|
||||
|
||||
/// The material design menu to arrow icon animation.
|
||||
static const AnimatedIconData menu_arrow = _$menu_arrow;
|
||||
}
|
||||
|
||||
/// Vector graphics data for icons used by [AnimatedIcon].
|
||||
///
|
||||
/// Instances of this class are currently opaque because we have not committed to a specific
|
||||
/// animated vector graphics format.
|
||||
///
|
||||
/// See also:
|
||||
/// * [AnimatedIcons], a class that contains constants that implement this interface.
|
||||
abstract class AnimatedIconData {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
const AnimatedIconData();
|
||||
|
||||
/// Whether this icon should be mirrored horizontally when text direction is
|
||||
/// right-to-left.
|
||||
///
|
||||
/// See also:
|
||||
/// * [TextDirection], which discusses concerns regarding reading direction
|
||||
/// in Flutter.
|
||||
/// * [Directionality], a widget which determines the ambient directionality.
|
||||
bool get matchTextDirection;
|
||||
}
|
||||
|
||||
class _AnimatedIconData extends AnimatedIconData {
|
||||
const _AnimatedIconData(this.size, this.paths, {this.matchTextDirection = false});
|
||||
|
||||
final Size size;
|
||||
final List<_PathFrames> paths;
|
||||
|
||||
@override
|
||||
final bool matchTextDirection;
|
||||
}
|
@ -66,11 +66,13 @@ class Icon extends StatelessWidget {
|
||||
/// The given color will be adjusted by the opacity of the current
|
||||
/// [IconTheme], if any.
|
||||
///
|
||||
/// If no [IconTheme]s are specified, icons will default to black.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
@ -86,7 +88,7 @@ class Icon extends StatelessWidget {
|
||||
|
||||
/// Semantic label for the icon.
|
||||
///
|
||||
/// This would be read out in accessibility modes (e.g TalkBack/VoiceOver).
|
||||
/// Announced in accessibility modes (e.g TalkBack/VoiceOver).
|
||||
/// This label does not show in the UI.
|
||||
///
|
||||
/// See also:
|
||||
|
@ -0,0 +1,408 @@
|
||||
// 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.
|
||||
|
||||
// This is the test for the private implementation of animated icons.
|
||||
// To make the private API accessible from the test we do not import the
|
||||
// material material_animated_icons library, but instead, this test file is an
|
||||
// implementation of that library, using some of the parts of the real
|
||||
// material_animated_icons, this give the test access to the private APIs.
|
||||
library material_animated_icons;
|
||||
|
||||
import 'dart:math' as math show pi;
|
||||
import 'dart:ui' show lerpDouble;
|
||||
import 'dart:ui' as ui show Paint, Path, Canvas;
|
||||
|
||||
import 'package:flutter/animation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
part '../../lib/src/material_animated_icons/animated_icons.dart';
|
||||
part '../../lib/src/material_animated_icons/animated_icons_data.dart';
|
||||
part '../../lib/src/material_animated_icons/data/menu_arrow.g.dart';
|
||||
|
||||
class MockCanvas extends Mock implements ui.Canvas {}
|
||||
class MockPath extends Mock implements ui.Path {}
|
||||
|
||||
void main () {
|
||||
group('Interpolate points', () {
|
||||
test('- single point', () {
|
||||
final List<Offset> points = const <Offset>[
|
||||
const Offset(25.0, 1.0),
|
||||
];
|
||||
expect(_interpolate(points, 0.0, Offset.lerp), const Offset(25.0, 1.0));
|
||||
expect(_interpolate(points, 0.5, Offset.lerp), const Offset(25.0, 1.0));
|
||||
expect(_interpolate(points, 1.0, Offset.lerp), const Offset(25.0, 1.0));
|
||||
});
|
||||
|
||||
test('- two points', () {
|
||||
final List<Offset> points = const <Offset>[
|
||||
const Offset(25.0, 1.0),
|
||||
const Offset(12.0, 12.0),
|
||||
];
|
||||
expect(_interpolate(points, 0.0, Offset.lerp), const Offset(25.0, 1.0));
|
||||
expect(_interpolate(points, 0.5, Offset.lerp), const Offset(18.5, 6.5));
|
||||
expect(_interpolate(points, 1.0, Offset.lerp), const Offset(12.0, 12.0));
|
||||
});
|
||||
|
||||
test('- three points', () {
|
||||
final List<Offset> points = const <Offset>[
|
||||
const Offset(25.0, 1.0),
|
||||
const Offset(12.0, 12.0),
|
||||
const Offset(23.0, 9.0),
|
||||
];
|
||||
expect(_interpolate(points, 0.0, Offset.lerp), const Offset(25.0, 1.0));
|
||||
expect(_interpolate(points, 0.25, Offset.lerp), const Offset(18.5, 6.5));
|
||||
expect(_interpolate(points, 0.5, Offset.lerp), const Offset(12.0, 12.0));
|
||||
expect(_interpolate(points, 0.75, Offset.lerp), const Offset(17.5, 10.5));
|
||||
expect(_interpolate(points, 1.0, Offset.lerp), const Offset(23.0, 9.0));
|
||||
});
|
||||
});
|
||||
|
||||
group('_AnimatedIconPainter', () {
|
||||
final Size size = const Size(48.0, 48.0);
|
||||
final MockCanvas mockCanvas = new MockCanvas();
|
||||
List<MockPath> generatedPaths;
|
||||
final _UiPathFactory pathFactory = () {
|
||||
final MockPath path = new MockPath();
|
||||
generatedPaths.add(path);
|
||||
return path;
|
||||
};
|
||||
|
||||
setUp(() {
|
||||
generatedPaths = <MockPath> [];
|
||||
});
|
||||
|
||||
test('progress 0', () {
|
||||
final _AnimatedIconPainter painter = new _AnimatedIconPainter(
|
||||
paths: movingBar.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
painter.paint(mockCanvas, size);
|
||||
expect(generatedPaths.length, 1);
|
||||
|
||||
verifyInOrder(<dynamic>[
|
||||
generatedPaths[0].moveTo(0.0, 0.0),
|
||||
generatedPaths[0].lineTo(48.0, 0.0),
|
||||
generatedPaths[0].lineTo(48.0, 10.0),
|
||||
generatedPaths[0].lineTo(0.0, 10.0),
|
||||
generatedPaths[0].lineTo(0.0, 0.0),
|
||||
generatedPaths[0].close(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('progress 1', () {
|
||||
final _AnimatedIconPainter painter = new _AnimatedIconPainter(
|
||||
paths: movingBar.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(1.0),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
painter.paint(mockCanvas, size);
|
||||
expect(generatedPaths.length, 1);
|
||||
|
||||
verifyInOrder(<dynamic>[
|
||||
generatedPaths[0].moveTo(0.0, 38.0),
|
||||
generatedPaths[0].lineTo(48.0, 38.0),
|
||||
generatedPaths[0].lineTo(48.0, 48.0),
|
||||
generatedPaths[0].lineTo(0.0, 48.0),
|
||||
generatedPaths[0].lineTo(0.0, 38.0),
|
||||
generatedPaths[0].close(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('clamped progress', () {
|
||||
final _AnimatedIconPainter painter = new _AnimatedIconPainter(
|
||||
paths: movingBar.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(1.5),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
painter.paint(mockCanvas, size);
|
||||
expect(generatedPaths.length, 1);
|
||||
|
||||
verifyInOrder(<dynamic>[
|
||||
generatedPaths[0].moveTo(0.0, 38.0),
|
||||
generatedPaths[0].lineTo(48.0, 38.0),
|
||||
generatedPaths[0].lineTo(48.0, 48.0),
|
||||
generatedPaths[0].lineTo(0.0, 48.0),
|
||||
generatedPaths[0].lineTo(0.0, 38.0),
|
||||
generatedPaths[0].close(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('scale', () {
|
||||
final _AnimatedIconPainter painter = new _AnimatedIconPainter(
|
||||
paths: movingBar.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 0.5,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
painter.paint(mockCanvas, size);
|
||||
verify(mockCanvas.scale(0.5, 0.5));
|
||||
});
|
||||
|
||||
test('mirror', () {
|
||||
final _AnimatedIconPainter painter = new _AnimatedIconPainter(
|
||||
paths: movingBar.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: true,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
painter.paint(mockCanvas, size);
|
||||
verifyInOrder(<dynamic>[
|
||||
mockCanvas.rotate(math.pi),
|
||||
mockCanvas.translate(-48.0, -48.0)
|
||||
]);
|
||||
});
|
||||
|
||||
test('interpolated frame', () {
|
||||
final _AnimatedIconPainter painter = new _AnimatedIconPainter(
|
||||
paths: movingBar.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.5),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
painter.paint(mockCanvas, size);
|
||||
expect(generatedPaths.length, 1);
|
||||
|
||||
verifyInOrder(<dynamic>[
|
||||
generatedPaths[0].moveTo(0.0, 19.0),
|
||||
generatedPaths[0].lineTo(48.0, 19.0),
|
||||
generatedPaths[0].lineTo(48.0, 29.0),
|
||||
generatedPaths[0].lineTo(0.0, 29.0),
|
||||
generatedPaths[0].lineTo(0.0, 19.0),
|
||||
generatedPaths[0].close(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('curved frame', () {
|
||||
final _AnimatedIconPainter painter = new _AnimatedIconPainter(
|
||||
paths: bow.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(1.0),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
painter.paint(mockCanvas, size);
|
||||
expect(generatedPaths.length, 1);
|
||||
|
||||
verifyInOrder(<dynamic>[
|
||||
generatedPaths[0].moveTo(0.0, 24.0),
|
||||
generatedPaths[0].cubicTo(16.0, 48.0, 32.0, 48.0, 48.0, 24.0),
|
||||
generatedPaths[0].lineTo(0.0, 24.0),
|
||||
generatedPaths[0].close(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('interpolated curved frame', () {
|
||||
final _AnimatedIconPainter painter = new _AnimatedIconPainter(
|
||||
paths: bow.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.25),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
painter.paint(mockCanvas, size);
|
||||
expect(generatedPaths.length, 1);
|
||||
|
||||
verifyInOrder(<dynamic>[
|
||||
generatedPaths[0].moveTo(0.0, 24.0),
|
||||
generatedPaths[0].cubicTo(16.0, 17.0, 32.0, 17.0, 48.0, 24.0),
|
||||
generatedPaths[0].lineTo(0.0, 24.0),
|
||||
generatedPaths[0].close(),
|
||||
]);
|
||||
});
|
||||
|
||||
test('should not repaint same values', () {
|
||||
final _AnimatedIconPainter painter1 = new _AnimatedIconPainter(
|
||||
paths: bow.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
|
||||
final _AnimatedIconPainter painter2 = new _AnimatedIconPainter(
|
||||
paths: bow.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
|
||||
expect(painter1.shouldRepaint(painter2), false);
|
||||
});
|
||||
|
||||
test('should repaint on progress change', () {
|
||||
final _AnimatedIconPainter painter1 = new _AnimatedIconPainter(
|
||||
paths: bow.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
|
||||
final _AnimatedIconPainter painter2 = new _AnimatedIconPainter(
|
||||
paths: bow.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.1),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
|
||||
expect(painter1.shouldRepaint(painter2), true);
|
||||
});
|
||||
|
||||
test('should repaint on color change', () {
|
||||
final _AnimatedIconPainter painter1 = new _AnimatedIconPainter(
|
||||
paths: bow.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFF00FF00),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
|
||||
final _AnimatedIconPainter painter2 = new _AnimatedIconPainter(
|
||||
paths: bow.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFFFF0000),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
|
||||
expect(painter1.shouldRepaint(painter2), true);
|
||||
});
|
||||
|
||||
test('should repaint on paths change', () {
|
||||
final _AnimatedIconPainter painter1 = new _AnimatedIconPainter(
|
||||
paths: bow.paths,
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFF0000FF),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
|
||||
final _AnimatedIconPainter painter2 = new _AnimatedIconPainter(
|
||||
paths: const <_PathFrames> [],
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
color: const Color(0xFF0000FF),
|
||||
scale: 1.0,
|
||||
shouldMirror: false,
|
||||
uiPathFactory: pathFactory
|
||||
);
|
||||
|
||||
expect(painter1.shouldRepaint(painter2), true);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
const _AnimatedIconData movingBar = const _AnimatedIconData(
|
||||
const Size(48.0, 48.0),
|
||||
const <_PathFrames> [
|
||||
const _PathFrames(
|
||||
opacities: const <double> [1.0, 0.2],
|
||||
commands: const <_PathCommand> [
|
||||
const _PathMoveTo(
|
||||
const <Offset> [
|
||||
const Offset(0.0, 0.0),
|
||||
const Offset(0.0, 38.0),
|
||||
],
|
||||
),
|
||||
const _PathLineTo(
|
||||
const <Offset> [
|
||||
const Offset(48.0, 0.0),
|
||||
const Offset(48.0, 38.0),
|
||||
],
|
||||
),
|
||||
const _PathLineTo(
|
||||
const <Offset> [
|
||||
const Offset(48.0, 10.0),
|
||||
const Offset(48.0, 48.0),
|
||||
],
|
||||
),
|
||||
const _PathLineTo(
|
||||
const <Offset> [
|
||||
const Offset(0.0, 10.0),
|
||||
const Offset(0.0, 48.0),
|
||||
],
|
||||
),
|
||||
const _PathLineTo(
|
||||
const <Offset> [
|
||||
const Offset(0.0, 0.0),
|
||||
const Offset(0.0, 38.0),
|
||||
],
|
||||
),
|
||||
const _PathClose(),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
const _AnimatedIconData bow = const _AnimatedIconData(
|
||||
const Size(48.0, 48.0),
|
||||
const <_PathFrames> [
|
||||
const _PathFrames(
|
||||
opacities: const <double> [1.0, 1.0],
|
||||
commands: const <_PathCommand> [
|
||||
const _PathMoveTo(
|
||||
const <Offset> [
|
||||
const Offset(0.0, 24.0),
|
||||
const Offset(0.0, 24.0),
|
||||
const Offset(0.0, 24.0),
|
||||
],
|
||||
),
|
||||
const _PathCubicTo(
|
||||
const <Offset> [
|
||||
const Offset(16.0, 24.0),
|
||||
const Offset(16.0, 10.0),
|
||||
const Offset(16.0, 48.0),
|
||||
],
|
||||
const <Offset> [
|
||||
const Offset(32.0, 24.0),
|
||||
const Offset(32.0, 10.0),
|
||||
const Offset(32.0, 48.0),
|
||||
],
|
||||
const <Offset> [
|
||||
const Offset(48.0, 24.0),
|
||||
const Offset(48.0, 24.0),
|
||||
const Offset(48.0, 24.0),
|
||||
],
|
||||
),
|
||||
const _PathLineTo(
|
||||
const <Offset> [
|
||||
const Offset(0.0, 24.0),
|
||||
const Offset(0.0, 24.0),
|
||||
const Offset(0.0, 24.0),
|
||||
],
|
||||
),
|
||||
const _PathClose(),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
@ -0,0 +1,237 @@
|
||||
// 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.
|
||||
|
||||
import 'dart:math' as math show pi;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/material_animated_icons.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import '../widgets/semantics_tester.dart';
|
||||
|
||||
class MockCanvas extends Mock implements Canvas {}
|
||||
|
||||
void main() {
|
||||
testWidgets('IconTheme color', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: const IconTheme(
|
||||
data: const IconThemeData(
|
||||
color: const Color(0xFF666666),
|
||||
),
|
||||
child: const AnimatedIcon(
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
icon: AnimatedIcons.arrow_menu,
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
final CustomPaint customPaint = tester.widget(find.byType(CustomPaint));
|
||||
final MockCanvas canvas = new MockCanvas();
|
||||
customPaint.painter.paint(canvas, const Size(48.0, 48.0));
|
||||
verify(canvas.drawPath(any, paintColorMatcher(0xFF666666)));
|
||||
});
|
||||
|
||||
testWidgets('IconTheme opacity', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: const IconTheme(
|
||||
data: const IconThemeData(
|
||||
color: const Color(0xFF666666),
|
||||
opacity: 0.5,
|
||||
),
|
||||
child: const AnimatedIcon(
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
icon: AnimatedIcons.arrow_menu,
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
final CustomPaint customPaint = tester.widget(find.byType(CustomPaint));
|
||||
final MockCanvas canvas = new MockCanvas();
|
||||
customPaint.painter.paint(canvas, const Size(48.0, 48.0));
|
||||
verify(canvas.drawPath(any, paintColorMatcher(0x80666666)));
|
||||
});
|
||||
|
||||
testWidgets('color overrides IconTheme color', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: const IconTheme(
|
||||
data: const IconThemeData(
|
||||
color: const Color(0xFF666666),
|
||||
),
|
||||
child: const AnimatedIcon(
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
icon: AnimatedIcons.arrow_menu,
|
||||
color: const Color(0xFF0000FF),
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
final CustomPaint customPaint = tester.widget(find.byType(CustomPaint));
|
||||
final MockCanvas canvas = new MockCanvas();
|
||||
customPaint.painter.paint(canvas, const Size(48.0, 48.0));
|
||||
verify(canvas.drawPath(any, paintColorMatcher(0xFF0000FF)));
|
||||
});
|
||||
|
||||
testWidgets('IconTheme size', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: const IconTheme(
|
||||
data: const IconThemeData(
|
||||
color: const Color(0xFF666666),
|
||||
size: 12.0,
|
||||
),
|
||||
child: const AnimatedIcon(
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
icon: AnimatedIcons.arrow_menu,
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
final CustomPaint customPaint = tester.widget(find.byType(CustomPaint));
|
||||
final MockCanvas canvas = new MockCanvas();
|
||||
customPaint.painter.paint(canvas, const Size(12.0, 12.0));
|
||||
// arrow_menu default size is 48x48 so we expect it to be scaled by 0.25.
|
||||
verify(canvas.scale(0.25, 0.25));
|
||||
});
|
||||
|
||||
testWidgets('size overridesIconTheme size', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: const IconTheme(
|
||||
data: const IconThemeData(
|
||||
color: const Color(0xFF666666),
|
||||
size: 12.0,
|
||||
),
|
||||
child: const AnimatedIcon(
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
icon: AnimatedIcons.arrow_menu,
|
||||
size: 96.0,
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
final CustomPaint customPaint = tester.widget(find.byType(CustomPaint));
|
||||
final MockCanvas canvas = new MockCanvas();
|
||||
customPaint.painter.paint(canvas, const Size(12.0, 12.0));
|
||||
// arrow_menu default size is 48x48 so we expect it to be scaled by 2.
|
||||
verify(canvas.scale(2.0, 2.0));
|
||||
});
|
||||
|
||||
testWidgets('Semantic label', (WidgetTester tester) async {
|
||||
final SemanticsTester semantics = new SemanticsTester(tester);
|
||||
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: const AnimatedIcon(
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
icon: AnimatedIcons.arrow_menu,
|
||||
size: 96.0,
|
||||
semanticLabel: 'a label',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(semantics, includesNodeWith(label: 'a label'));
|
||||
});
|
||||
|
||||
testWidgets('Inherited text direction rtl', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.rtl,
|
||||
child: const IconTheme(
|
||||
data: const IconThemeData(
|
||||
color: const Color(0xFF666666),
|
||||
),
|
||||
child: const AnimatedIcon(
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
icon: AnimatedIcons.arrow_menu,
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
final CustomPaint customPaint = tester.widget(find.byType(CustomPaint));
|
||||
final MockCanvas canvas = new MockCanvas();
|
||||
customPaint.painter.paint(canvas, const Size(48.0, 48.0));
|
||||
verifyInOrder(<dynamic>[
|
||||
canvas.rotate(math.pi),
|
||||
canvas.translate(-48.0, -48.0)
|
||||
]);
|
||||
});
|
||||
|
||||
testWidgets('Inherited text direction ltr', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: const IconTheme(
|
||||
data: const IconThemeData(
|
||||
color: const Color(0xFF666666),
|
||||
),
|
||||
child: const AnimatedIcon(
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
icon: AnimatedIcons.arrow_menu,
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
final CustomPaint customPaint = tester.widget(find.byType(CustomPaint));
|
||||
final MockCanvas canvas = new MockCanvas();
|
||||
customPaint.painter.paint(canvas, const Size(48.0, 48.0));
|
||||
verifyNever(canvas.rotate(any));
|
||||
verifyNever(canvas.translate(any, any));
|
||||
});
|
||||
|
||||
testWidgets('Inherited text direction overridden', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: const IconTheme(
|
||||
data: const IconThemeData(
|
||||
color: const Color(0xFF666666),
|
||||
),
|
||||
child: const AnimatedIcon(
|
||||
progress: const AlwaysStoppedAnimation<double>(0.0),
|
||||
icon: AnimatedIcons.arrow_menu,
|
||||
textDirection: TextDirection.rtl,
|
||||
)
|
||||
),
|
||||
),
|
||||
);
|
||||
final CustomPaint customPaint = tester.widget(find.byType(CustomPaint));
|
||||
final MockCanvas canvas = new MockCanvas();
|
||||
customPaint.painter.paint(canvas, const Size(48.0, 48.0));
|
||||
verifyInOrder(<dynamic>[
|
||||
canvas.rotate(math.pi),
|
||||
canvas.translate(-48.0, -48.0)
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
dynamic paintColorMatcher(int color) {
|
||||
return new PaintColorMatcher(color);
|
||||
}
|
||||
|
||||
class PaintColorMatcher extends Matcher {
|
||||
const PaintColorMatcher(this.expectedColor);
|
||||
|
||||
final int expectedColor;
|
||||
|
||||
@override
|
||||
Description describe(Description description) =>
|
||||
description.add('color was not $expectedColor');
|
||||
|
||||
@override
|
||||
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
|
||||
final Paint actualPaint = item;
|
||||
return actualPaint.color == new Color(expectedColor);
|
||||
}
|
||||
}
|