Unverified Commit 56061759 authored by amirh's avatar amirh Committed by GitHub

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

parent a5b27703
...@@ -166,6 +166,7 @@ Future<Null> _runTests() async { ...@@ -166,6 +166,7 @@ Future<Null> _runTests() async {
await _runAllDartTests(path.join(flutterRoot, 'dev', 'devicelab')); await _runAllDartTests(path.join(flutterRoot, 'dev', 'devicelab'));
await _runFlutterTest(path.join(flutterRoot, 'dev', 'manual_tests')); 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', 'hello_world'));
await _runFlutterTest(path.join(flutterRoot, 'examples', 'layers')); await _runFlutterTest(path.join(flutterRoot, 'examples', 'layers'));
await _runFlutterTest(path.join(flutterRoot, 'examples', 'stocks')); await _runFlutterTest(path.join(flutterRoot, 'examples', 'stocks'));
......
# 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/
# 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)
// 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);
}
This diff is collapsed.
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
This diff is collapsed.
<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>
<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>
<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>
<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>
<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>
<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>
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
</svg>
<?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>
<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>
<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>
<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>
<svg id="svg_build_00" xmlns="http://www.w3.org/2000/svg" width="48px" height="48px" >
</svg>
<svg/>
<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>
<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>
// 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';
// 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);
// 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 { ...@@ -66,11 +66,13 @@ class Icon extends StatelessWidget {
/// The given color will be adjusted by the opacity of the current /// The given color will be adjusted by the opacity of the current
/// [IconTheme], if any. /// [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 /// In material apps, if there is a [Theme] without any [IconTheme]s
/// specified, icon colors default to white if the theme is dark /// specified, icon colors default to white if the theme is dark
/// and black if the theme is light. /// 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] /// See [Theme] to set the current theme and [ThemeData.brightness]
/// for setting the current theme's brightness. /// for setting the current theme's brightness.
/// ///
...@@ -86,7 +88,7 @@ class Icon extends StatelessWidget { ...@@ -86,7 +88,7 @@ class Icon extends StatelessWidget {
/// Semantic label for the icon. /// 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. /// This label does not show in the UI.
/// ///
/// See also: /// See also:
......
// 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);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment