Add an AnimatedIcon class and vitool (vector icon tool) to generate data for it (#13530)

This commit is contained in:
amirh 2017-12-21 15:46:05 -08:00 committed by GitHub
parent a5b277030a
commit 56061759fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 4575 additions and 2 deletions

View File

@ -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
View 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/

View 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)

View 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);
}

View 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;

View 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

View 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);
}

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -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

View 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

View 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=" z" fill="#000000" />
</svg>

After

Width:  |  Height:  |  Size: 137 B

View 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';

View File

@ -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);

View File

@ -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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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:

View File

@ -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(),
],
),
],
);

View File

@ -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);
}
}