diff --git a/examples/flutter_gallery/lib/demo/material/chip_demo.dart b/examples/flutter_gallery/lib/demo/material/chip_demo.dart index b27de343290..74f84628e1d 100644 --- a/examples/flutter_gallery/lib/demo/material/chip_demo.dart +++ b/examples/flutter_gallery/lib/demo/material/chip_demo.dart @@ -28,7 +28,7 @@ class _ChipDemoState extends State { ), const Chip( avatar: const CircleAvatar(child: const Text('B')), - label: const Text('Blueberry') + label: const Text('Blueberry'), ), ]; diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart index 61b59f892a9..031a8a9c4a8 100644 --- a/packages/flutter/lib/painting.dart +++ b/packages/flutter/lib/painting.dart @@ -37,6 +37,7 @@ export 'src/painting/images.dart'; export 'src/painting/matrix_utils.dart'; export 'src/painting/rounded_rectangle_border.dart'; export 'src/painting/shape_decoration.dart'; +export 'src/painting/stadium_border.dart'; export 'src/painting/text_painter.dart'; export 'src/painting/text_span.dart'; export 'src/painting/text_style.dart'; diff --git a/packages/flutter/lib/src/material/chip.dart b/packages/flutter/lib/src/material/chip.dart index e611fa68e46..693455b17c9 100644 --- a/packages/flutter/lib/src/material/chip.dart +++ b/packages/flutter/lib/src/material/chip.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter/painting.dart'; import 'colors.dart'; import 'debug.dart'; @@ -11,17 +12,6 @@ import 'feedback.dart'; import 'icons.dart'; import 'tooltip.dart'; -const double _kChipHeight = 32.0; -const double _kAvatarDiamater = _kChipHeight; - -const TextStyle _kLabelStyle = const TextStyle( - inherit: false, - fontSize: 13.0, - fontWeight: FontWeight.w400, - color: Colors.black87, - textBaseline: TextBaseline.alphabetic, -); - /// A material design chip. /// /// Chips represent complex entities in small blocks, such as a contact. @@ -29,7 +19,8 @@ const TextStyle _kLabelStyle = const TextStyle( /// Supplying a non-null [onDeleted] callback will cause the chip to include a /// button for deleting the chip. /// -/// Requires one of its ancestors to be a [Material] widget. +/// Requires one of its ancestors to be a [Material] widget. The [label] +/// and [border] arguments must not be null. /// /// ## Sample code /// @@ -57,11 +48,25 @@ class Chip extends StatelessWidget { this.avatar, @required this.label, this.onDeleted, - this.labelStyle, + TextStyle labelStyle, this.deleteButtonTooltipMessage, this.backgroundColor, this.deleteIconColor, - }) : super(key: key); + this.border: const StadiumBorder(), + }) : assert(label != null), + assert(border != null), + labelStyle = labelStyle ?? _defaultLabelStyle, + super(key: key); + + static const TextStyle _defaultLabelStyle = const TextStyle( + inherit: false, + fontSize: 13.0, + fontWeight: FontWeight.w400, + color: Colors.black87, + textBaseline: TextBaseline.alphabetic, + ); + + static const double _chipHeight = 32.0; /// A widget to display prior to the chip's label. /// @@ -90,6 +95,11 @@ class Chip extends StatelessWidget { /// widget's label. final Color backgroundColor; + /// The border to draw around the chip. + /// + /// Defaults to a [StadiumBorder]. + final ShapeBorder border; + /// Color for delete icon, the default being black. /// /// This has no effect when [onDelete] is null since no delete icon will be @@ -116,8 +126,8 @@ class Chip extends StatelessWidget { children.add(new ExcludeSemantics( child: new Container( margin: const EdgeInsetsDirectional.only(end: 8.0), - width: _kAvatarDiamater, - height: _kAvatarDiamater, + width: _chipHeight, + height: _chipHeight, child: avatar, ), )); @@ -125,7 +135,8 @@ class Chip extends StatelessWidget { children.add(new Flexible( child: new DefaultTextStyle( - style: labelStyle ?? _kLabelStyle, + overflow: TextOverflow.ellipsis, + style: labelStyle, child: label, ), )); @@ -135,12 +146,14 @@ class Chip extends StatelessWidget { children.add(new GestureDetector( onTap: Feedback.wrapForTap(onDeleted, context), child: new Tooltip( + // TODO(gspencer): Internationalize this text. + // https://github.com/flutter/flutter/issues/12378 message: deleteButtonTooltipMessage ?? 'Delete "$label"', child: new Container( padding: const EdgeInsets.symmetric(horizontal: 4.0), child: new Icon( Icons.cancel, - size: 18.0, + size: 24.0, color: deleteIconColor ?? Colors.black54, ), ), @@ -151,17 +164,21 @@ class Chip extends StatelessWidget { return new Semantics( container: true, child: new Container( - height: _kChipHeight, + constraints: const BoxConstraints(minHeight: _chipHeight), padding: new EdgeInsetsDirectional.only(start: startPadding, end: endPadding), - decoration: new BoxDecoration( + decoration: new ShapeDecoration( color: backgroundColor ?? Colors.grey.shade300, - borderRadius: new BorderRadius.circular(16.0), + shape: border, ), - child: new Row( - children: children, - mainAxisSize: MainAxisSize.min, + child: new Center( + widthFactor: 1.0, + heightFactor: 1.0, + child: new Row( + children: children, + mainAxisSize: MainAxisSize.min, + ), ), ), ); } -} +} \ No newline at end of file diff --git a/packages/flutter/lib/src/painting/box_border.dart b/packages/flutter/lib/src/painting/box_border.dart index a2f110ff13e..9f470306523 100644 --- a/packages/flutter/lib/src/painting/box_border.dart +++ b/packages/flutter/lib/src/painting/box_border.dart @@ -475,16 +475,19 @@ class Border extends BoxBorder { case BorderStyle.none: return; case BorderStyle.solid: - if (shape == BoxShape.circle) { - assert(borderRadius == null, 'A borderRadius can only be given for rectangular boxes.'); - BoxBorder._paintUniformBorderWithCircle(canvas, rect, top); - return; + switch (shape) { + case BoxShape.circle: + assert(borderRadius == null, 'A borderRadius can only be given for rectangular boxes.'); + BoxBorder._paintUniformBorderWithCircle(canvas, rect, top); + break; + case BoxShape.rectangle: + if (borderRadius != null) { + BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius); + return; + } + BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top); + break; } - if (borderRadius != null) { - BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius); - return; - } - BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top); return; } } @@ -777,16 +780,19 @@ class BorderDirectional extends BoxBorder { case BorderStyle.none: return; case BorderStyle.solid: - if (shape == BoxShape.circle) { - assert(borderRadius == null, 'A borderRadius can only be given for rectangular boxes.'); - BoxBorder._paintUniformBorderWithCircle(canvas, rect, top); - return; + switch (shape) { + case BoxShape.circle: + assert(borderRadius == null, 'A borderRadius can only be given for rectangular boxes.'); + BoxBorder._paintUniformBorderWithCircle(canvas, rect, top); + break; + case BoxShape.rectangle: + if (borderRadius != null) { + BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius); + return; + } + BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top); + break; } - if (borderRadius != null) { - BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius); - return; - } - BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top); return; } } diff --git a/packages/flutter/lib/src/painting/circle_border.dart b/packages/flutter/lib/src/painting/circle_border.dart index adcf34e9cf4..76396c23c3d 100644 --- a/packages/flutter/lib/src/painting/circle_border.dart +++ b/packages/flutter/lib/src/painting/circle_border.dart @@ -25,7 +25,7 @@ class CircleBorder extends ShapeBorder { /// Create a circle border. /// /// The [side] argument must not be null. - const CircleBorder([ this.side = BorderSide.none ]) : assert(side != null); + const CircleBorder({ this.side = BorderSide.none }) : assert(side != null); /// The style of this border. final BorderSide side; @@ -36,19 +36,19 @@ class CircleBorder extends ShapeBorder { } @override - ShapeBorder scale(double t) => new CircleBorder(side.scale(t)); + ShapeBorder scale(double t) => new CircleBorder(side: side.scale(t)); @override ShapeBorder lerpFrom(ShapeBorder a, double t) { if (a is CircleBorder) - return new CircleBorder(BorderSide.lerp(a.side, side, t)); + return new CircleBorder(side: BorderSide.lerp(a.side, side, t)); return super.lerpFrom(a, t); } @override ShapeBorder lerpTo(ShapeBorder b, double t) { if (b is CircleBorder) - return new CircleBorder(BorderSide.lerp(side, b.side, t)); + return new CircleBorder(side: BorderSide.lerp(side, b.side, t)); return super.lerpTo(b, t); } diff --git a/packages/flutter/lib/src/painting/shape_decoration.dart b/packages/flutter/lib/src/painting/shape_decoration.dart index 84b2d99c9c1..7752f905c67 100644 --- a/packages/flutter/lib/src/painting/shape_decoration.dart +++ b/packages/flutter/lib/src/painting/shape_decoration.dart @@ -94,7 +94,7 @@ class ShapeDecoration extends Decoration { case BoxShape.circle: if (source.border != null) { assert(source.border.isUniform); - shape = new CircleBorder(source.border.top); + shape = new CircleBorder(side: source.border.top); } else { shape = const CircleBorder(); } diff --git a/packages/flutter/lib/src/painting/stadium_border.dart b/packages/flutter/lib/src/painting/stadium_border.dart new file mode 100644 index 00000000000..0223e1f341b --- /dev/null +++ b/packages/flutter/lib/src/painting/stadium_border.dart @@ -0,0 +1,414 @@ +// 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:ui' as ui show lerpDouble; + +import 'basic_types.dart'; +import 'border_radius.dart'; +import 'borders.dart'; +import 'circle_border.dart'; +import 'edge_insets.dart'; +import 'rounded_rectangle_border.dart'; + +/// A border that fits a stadium-shaped border (a box with semicircles on the ends) +/// within the rectangle of the widget it is applied to. +/// +/// Typically used with [ShapeDecoration] to draw a stadium border. +/// +/// If the rectangle is taller than it is wide, then the semicircles will be on the +/// top and bottom, and on the left and right otherwise. +/// +/// See also: +/// +/// * [BorderSide], which is used to describe the border of the stadium. +class StadiumBorder extends ShapeBorder { + /// Create a stadium border. + /// + /// The [side] argument must not be null. + const StadiumBorder({this.side = BorderSide.none}) : assert(side != null); + + /// The style of this border. + final BorderSide side; + + @override + EdgeInsetsGeometry get dimensions { + return new EdgeInsets.all(side.width); + } + + @override + ShapeBorder scale(double t) => new StadiumBorder(side: side.scale(t)); + + @override + ShapeBorder lerpFrom(ShapeBorder a, double t) { + if (a is StadiumBorder) + return new StadiumBorder(side: BorderSide.lerp(a.side, side, t)); + if (a is CircleBorder) { + return new _StadiumToCircleBorder( + side: BorderSide.lerp(a.side, side, t), + circleness: 1.0 - t, + ); + } + if (a is RoundedRectangleBorder) { + return new _StadiumToRoundedRectangleBorder( + side: BorderSide.lerp(a.side, side, t), + borderRadius: a.borderRadius, + rectness: 1.0 - t, + ); + } + return super.lerpFrom(a, t); + } + + @override + ShapeBorder lerpTo(ShapeBorder b, double t) { + if (b is StadiumBorder) + return new StadiumBorder(side: BorderSide.lerp(side, b.side, t)); + if (b is CircleBorder) { + return new _StadiumToCircleBorder( + side: BorderSide.lerp(side, b.side, t), + circleness: t, + ); + } + if (b is RoundedRectangleBorder) { + return new _StadiumToRoundedRectangleBorder( + side: BorderSide.lerp(side, b.side, t), + borderRadius: b.borderRadius, + rectness: t, + ); + } + return super.lerpTo(b, t); + } + + @override + Path getInnerPath(Rect rect, { TextDirection textDirection }) { + final Radius radius = new Radius.circular(rect.shortestSide / 2.0); + return new Path() + ..addRRect(new RRect.fromRectAndRadius(rect, radius).deflate(side.width)); + } + + @override + Path getOuterPath(Rect rect, { TextDirection textDirection }) { + final Radius radius = new Radius.circular(rect.shortestSide / 2.0); + return new Path() + ..addRRect(new RRect.fromRectAndRadius(rect, radius)); + } + + @override + void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) { + switch (side.style) { + case BorderStyle.none: + break; + case BorderStyle.solid: + final Radius radius = new Radius.circular(rect.shortestSide / 2.0); + canvas.drawRRect( + new RRect.fromRectAndRadius(rect, radius).deflate(side.width / 2.0), + side.toPaint(), + ); + } + } + + @override + bool operator ==(dynamic other) { + if (runtimeType != other.runtimeType) + return false; + final StadiumBorder typedOther = other; + return side == typedOther.side; + } + + @override + int get hashCode => side.hashCode; + + @override + String toString() { + return '$runtimeType($side)'; + } +} + +// Class to help with transitioning to/from a CircleBorder. +class _StadiumToCircleBorder extends ShapeBorder { + const _StadiumToCircleBorder({ + this.side: BorderSide.none, + this.circleness: 0.0, + }) : assert(side != null), + assert(circleness != null); + + final BorderSide side; + + final double circleness; + + @override + EdgeInsetsGeometry get dimensions { + return new EdgeInsets.all(side.width); + } + + @override + ShapeBorder scale(double t) { + return new _StadiumToCircleBorder( + side: side.scale(t), + circleness: t, + ); + } + + @override + ShapeBorder lerpFrom(ShapeBorder a, double t) { + if (a is StadiumBorder) { + return new _StadiumToCircleBorder( + side: BorderSide.lerp(a.side, side, t), + circleness: circleness * t, + ); + } + if (a is CircleBorder) { + return new _StadiumToCircleBorder( + side: BorderSide.lerp(a.side, side, t), + circleness: circleness + (1.0 - circleness) * (1.0 - t), + ); + } + if (a is _StadiumToCircleBorder) { + return new _StadiumToCircleBorder( + side: BorderSide.lerp(a.side, side, t), + circleness: ui.lerpDouble(a.circleness, circleness, t), + ); + } + return super.lerpFrom(a, t); + } + + @override + ShapeBorder lerpTo(ShapeBorder b, double t) { + if (b is StadiumBorder) { + return new _StadiumToCircleBorder( + side: BorderSide.lerp(side, b.side, t), + circleness: circleness * (1.0 - t), + ); + } + if (b is CircleBorder) { + return new _StadiumToCircleBorder( + side: BorderSide.lerp(side, b.side, t), + circleness: circleness + (1.0 - circleness) * t, + ); + } + if (b is _StadiumToCircleBorder) { + return new _StadiumToCircleBorder( + side: BorderSide.lerp(side, b.side, t), + circleness: ui.lerpDouble(circleness, b.circleness, t), + ); + } + return super.lerpTo(b, t); + } + + Rect _adjustRect(Rect rect) { + if (circleness == 0.0 || rect.width == rect.height) + return rect; + if (rect.width < rect.height) { + final double delta = circleness * (rect.height - rect.width) / 2.0; + return new Rect.fromLTRB( + rect.left, + rect.top + delta, + rect.right, + rect.bottom - delta, + ); + } else { + final double delta = circleness * (rect.width - rect.height) / 2.0; + return new Rect.fromLTRB( + rect.left + delta, + rect.top, + rect.right - delta, + rect.bottom, + ); + } + } + + BorderRadius _adjustBorderRadius(Rect rect) { + return new BorderRadius.circular(rect.shortestSide / 2.0); + } + + @override + Path getInnerPath(Rect rect, { TextDirection textDirection }) { + return new Path() + ..addRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)).deflate(side.width)); + } + + @override + Path getOuterPath(Rect rect, { TextDirection textDirection }) { + return new Path() + ..addRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect))); + } + + @override + void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) { + switch (side.style) { + case BorderStyle.none: + break; + case BorderStyle.solid: + final double width = side.width; + if (width == 0.0) { + canvas.drawRRect(_adjustBorderRadius(rect).toRRect(_adjustRect(rect)), side.toPaint()); + } else { + final RRect outer = _adjustBorderRadius(rect).toRRect(_adjustRect(rect)); + final RRect inner = outer.deflate(width); + final Paint paint = new Paint() + ..color = side.color; + canvas.drawDRRect(outer, inner, paint); + } + } + } + + @override + bool operator ==(dynamic other) { + if (runtimeType != other.runtimeType) + return false; + final _StadiumToCircleBorder typedOther = other; + return side == typedOther.side + && circleness == typedOther.circleness; + } + + @override + int get hashCode => hashValues(side, circleness); + + @override + String toString() { + return 'StadiumBorder($side, ${(circleness * 100).toStringAsFixed(1)}% ' + 'of the way to being a CircleBorder)'; + } +} + +// Class to help with transitioning to/from a RoundedRectBorder. +class _StadiumToRoundedRectangleBorder extends ShapeBorder { + const _StadiumToRoundedRectangleBorder({ + this.side: BorderSide.none, + this.borderRadius: BorderRadius.zero, + this.rectness: 0.0, + }) : assert(side != null), + assert(borderRadius != null), + assert(rectness != null); + + final BorderSide side; + + final BorderRadius borderRadius; + + final double rectness; + + @override + EdgeInsetsGeometry get dimensions { + return new EdgeInsets.all(side.width); + } + + @override + ShapeBorder scale(double t) { + return new _StadiumToRoundedRectangleBorder( + side: side.scale(t), + borderRadius: borderRadius * t, + rectness: t, + ); + } + + @override + ShapeBorder lerpFrom(ShapeBorder a, double t) { + if (a is StadiumBorder) { + return new _StadiumToRoundedRectangleBorder( + side: BorderSide.lerp(a.side, side, t), + borderRadius: borderRadius, + rectness: rectness * t, + ); + } + if (a is RoundedRectangleBorder) { + return new _StadiumToRoundedRectangleBorder( + side: BorderSide.lerp(a.side, side, t), + borderRadius: borderRadius, + rectness: rectness + (1.0 - rectness) * (1.0 - t), + ); + } + if (a is _StadiumToRoundedRectangleBorder) { + return new _StadiumToRoundedRectangleBorder( + side: BorderSide.lerp(a.side, side, t), + borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t), + rectness: ui.lerpDouble(a.rectness, rectness, t), + ); + } + return super.lerpFrom(a, t); + } + + @override + ShapeBorder lerpTo(ShapeBorder b, double t) { + if (b is StadiumBorder) { + return new _StadiumToRoundedRectangleBorder( + side: BorderSide.lerp(side, b.side, t), + borderRadius: borderRadius, + rectness: rectness * (1.0 - t), + ); + } + if (b is RoundedRectangleBorder) { + return new _StadiumToRoundedRectangleBorder( + side: BorderSide.lerp(side, b.side, t), + borderRadius: borderRadius, + rectness: rectness + (1.0 - rectness) * t, + ); + } + if (b is _StadiumToRoundedRectangleBorder) { + return new _StadiumToRoundedRectangleBorder( + side: BorderSide.lerp(side, b.side, t), + borderRadius: BorderRadius.lerp(borderRadius, b.borderRadius, t), + rectness: ui.lerpDouble(rectness, b.rectness, t), + ); + } + return super.lerpTo(b, t); + } + + BorderRadius _adjustBorderRadius(Rect rect) { + return BorderRadius.lerp( + borderRadius, + new BorderRadius.all(new Radius.circular(rect.shortestSide / 2.0)), + (1.0 - rectness) + ); + } + + @override + Path getInnerPath(Rect rect, { TextDirection textDirection }) { + return new Path() + ..addRRect(_adjustBorderRadius(rect).toRRect(rect).deflate(side.width)); + } + + @override + Path getOuterPath(Rect rect, { TextDirection textDirection }) { + return new Path() + ..addRRect(_adjustBorderRadius(rect).toRRect(rect)); + } + + @override + void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) { + switch (side.style) { + case BorderStyle.none: + break; + case BorderStyle.solid: + final double width = side.width; + if (width == 0.0) { + canvas.drawRRect(_adjustBorderRadius(rect).toRRect(rect), side.toPaint()); + } else { + final RRect outer = _adjustBorderRadius(rect).toRRect(rect); + final RRect inner = outer.deflate(width); + final Paint paint = new Paint() + ..color = side.color; + canvas.drawDRRect(outer, inner, paint); + } + } + } + + @override + bool operator ==(dynamic other) { + if (runtimeType != other.runtimeType) + return false; + final _StadiumToRoundedRectangleBorder typedOther = other; + return side == typedOther.side + && borderRadius == typedOther.borderRadius + && rectness == typedOther.rectness; + } + + @override + int get hashCode => hashValues(side, borderRadius, rectness); + + @override + String toString() { + return 'StadiumBorder($side, $borderRadius, ' + '${(rectness * 100).toStringAsFixed(1)}% of the way to being a ' + 'RoundedRectangleBorder)'; + } +} diff --git a/packages/flutter/lib/src/rendering/proxy_box.dart b/packages/flutter/lib/src/rendering/proxy_box.dart index c4aa4a1a278..76c5c3472c9 100644 --- a/packages/flutter/lib/src/rendering/proxy_box.dart +++ b/packages/flutter/lib/src/rendering/proxy_box.dart @@ -361,7 +361,7 @@ class RenderLimitedBox extends RenderProxyBox { /// Attempts to size the child to a specific aspect ratio. /// -/// The render object first tries the largest width permited by the layout +/// The render object first tries the largest width permitted by the layout /// constraints. The height of the render object is determined by applying the /// given aspect ratio to the width, expressed as a ratio of width to height. /// @@ -1372,12 +1372,15 @@ class RenderPhysicalModel extends _RenderCustomClip { @override RRect get _defaultClip { assert(hasSize); - if (_shape == BoxShape.rectangle) { - return (borderRadius ?? BorderRadius.zero).toRRect(Offset.zero & size); - } else { - final Rect rect = Offset.zero & size; - return new RRect.fromRectXY(rect, rect.width / 2, rect.height / 2); + assert(_shape != null); + switch (_shape) { + case BoxShape.rectangle: + return (borderRadius ?? BorderRadius.zero).toRRect(Offset.zero & size); + case BoxShape.circle: + final Rect rect = Offset.zero & size; + return new RRect.fromRectXY(rect, rect.width / 2, rect.height / 2); } + return null; } @override diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart index 90114c877d6..0073c86d786 100644 --- a/packages/flutter/lib/src/widgets/basic.dart +++ b/packages/flutter/lib/src/widgets/basic.dart @@ -1885,7 +1885,7 @@ class _OffstageElement extends SingleChildRenderObjectElement { /// A widget that attempts to size the child to a specific aspect ratio. /// -/// The widget first tries the largest width permited by the layout +/// The widget first tries the largest width permitted by the layout /// constraints. The height of the widget is determined by applying the /// given aspect ratio to the width, expressed as a ratio of width to height. /// diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 8e49efcb9dc..a40a48c0d91 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -36,12 +36,17 @@ class BoxConstraintsTween extends Tween { /// This class specializes the interpolation of [Tween] to use /// [Decoration.lerp]. /// -/// Typically this will only have useful results if the [begin] and [end] -/// decorations have the same type; decorations of differing types generally do -/// not have a useful animation defined, and will just jump to the [end] -/// immediately. +/// For [ShapeDecoration]s which know how to [ShapeDecoration.lerpTo] or +/// [ShapeDecoration.lerpFrom] each other, this will produce a smooth +/// interpolation between decorations. /// -/// See [Tween] for a discussion on how to use interpolation objects. +/// See also: +/// * [Tween] for a discussion on how to use interpolation objects. +/// * [ShapeDecoration], [RoundedRectangleBorder], [CircleBorder], and +/// [StadiumBorder] for examples of shape borders that can be smoothly +/// interpolated. +/// * [BoxBorder] for a border that can only be smoothly interpolated between other +/// [BoxBorder]s. class DecorationTween extends Tween { /// Creates a decoration tween. /// diff --git a/packages/flutter/test/material/chip_test.dart b/packages/flutter/test/material/chip_test.dart index aca551e9a19..5d146729ec8 100644 --- a/packages/flutter/test/material/chip_test.dart +++ b/packages/flutter/test/material/chip_test.dart @@ -254,6 +254,159 @@ void main() { expect(tester.getCenter(find.text('ABC')).dx, lessThan(tester.getCenter(find.byType(Icon)).dx)); }); + testWidgets('Chip responds to textScaleFactor', (WidgetTester tester) async { + await tester.pumpWidget( + new MaterialApp( + home: new Material( + child: new Column( + children: [ + const Chip( + avatar: const CircleAvatar( + child: const Text('A') + ), + label: const Text('Chip A'), + ), + const Chip( + avatar: const CircleAvatar( + child: const Text('B') + ), + label: const Text('Chip B'), + ), + ], + ), + ), + ), + ); + + // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs. + // https://github.com/flutter/flutter/issues/12357 + expect( + tester.getSize(find.text('Chip A')), + anyOf(const Size(79.0, 13.0), const Size(78.0, 13.0)), + ); + expect( + tester.getSize(find.text('Chip B')), + anyOf(const Size(79.0, 13.0), const Size(78.0, 13.0)), + ); + expect( + tester.getSize(find.byType(Chip).first), + anyOf(const Size(131.0, 32.0), const Size(130.0, 32.0)) + ); + expect( + tester.getSize(find.byType(Chip).last), + anyOf(const Size(131.0, 32.0), const Size(130.0, 32.0)) + ); + + await tester.pumpWidget( + new MaterialApp( + home: new MediaQuery( + data: const MediaQueryData(textScaleFactor: 3.0), + child: new Material( + child: new Column( + children: [ + const Chip( + avatar: const CircleAvatar( + child: const Text('A') + ), + label: const Text('Chip A'), + ), + const Chip( + avatar: const CircleAvatar( + child: const Text('B') + ), + label: const Text('Chip B'), + ), + ], + ), + ), + ), + ), + ); + + // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs. + // https://github.com/flutter/flutter/issues/12357 + expect(tester.getSize(find.text('Chip A')), anyOf(const Size(234.0, 39.0), const Size(235.0, 39.0))); + expect(tester.getSize(find.text('Chip B')), anyOf(const Size(234.0, 39.0), const Size(235.0, 39.0))); + expect(tester.getSize(find.byType(Chip).first).width, anyOf(286.0, 287.0)); + expect(tester.getSize(find.byType(Chip).first).height, equals(39.0)); + expect(tester.getSize(find.byType(Chip).last).width, anyOf(286.0, 287.0)); + expect(tester.getSize(find.byType(Chip).last).height, equals(39.0)); + + // Check that individual text scales are taken into account. + await tester.pumpWidget( + new MaterialApp( + home: new Material( + child: new Column( + children: [ + const Chip( + avatar: const CircleAvatar( + child: const Text('A') + ), + label: const Text('Chip A', textScaleFactor: 3.0), + ), + const Chip( + avatar: const CircleAvatar( + child: const Text('B') + ), + label: const Text('Chip B'), + ), + ], + ), + ), + ), + ); + + // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs. + // https://github.com/flutter/flutter/issues/12357 + expect(tester.getSize(find.text('Chip A')), anyOf(const Size(234.0, 39.0), const Size(235.0, 39.0))); + expect(tester.getSize(find.text('Chip B')), anyOf(const Size(78.0, 13.0), const Size(79.0, 13.0))); + expect(tester.getSize(find.byType(Chip).first).width, anyOf(286.0, 287.0)); + expect(tester.getSize(find.byType(Chip).first).height, equals(39.0)); + expect(tester.getSize(find.byType(Chip).last), anyOf(const Size(130.0, 32.0), const Size(131.0, 32.0))); + }); + + testWidgets('Labels can be non-text widgets', (WidgetTester tester) async { + final Key keyA = new GlobalKey(); + final Key keyB = new GlobalKey(); + await tester.pumpWidget( + new MaterialApp( + home: new Material( + child: new Column( + children: [ + new Chip( + avatar: const CircleAvatar( + child: const Text('A') + ), + label: new Text('Chip A', key: keyA), + ), + new Chip( + avatar: const CircleAvatar( + child: const Text('B') + ), + label: new Container(key: keyB, width: 10.0, height: 10.0), + ), + ], + ), + ), + ), + ); + + // TODO(gspencer): Update this test when the font metric bug is fixed to remove the anyOfs. + // https://github.com/flutter/flutter/issues/12357 + expect( + tester.getSize(find.byKey(keyA)), + anyOf(const Size(79.0, 13.0), const Size(78.0, 13.0)), + ); + expect(tester.getSize(find.byKey(keyB)), const Size(10.0, 10.0)); + expect( + tester.getSize(find.byType(Chip).first), + anyOf(const Size(131.0, 32.0), const Size(130.0, 32.0)), + ); + expect(tester.getSize(find.byType(Chip).last), const Size(62.0, 32.0)); + }); + + + testWidgets('Chip padding - LTR', (WidgetTester tester) async { final GlobalKey keyA = new GlobalKey(); final GlobalKey keyB = new GlobalKey(); @@ -281,10 +434,10 @@ void main() { ); expect(tester.getTopLeft(find.byKey(keyA)), const Offset(0.0, 284.0)); expect(tester.getBottomRight(find.byKey(keyA)), const Offset(32.0, 316.0)); - expect(tester.getTopLeft(find.byKey(keyB)), const Offset(40.0, 284.0)); - expect(tester.getBottomRight(find.byKey(keyB)), const Offset(774.0, 316.0)); - expect(tester.getTopLeft(find.byType(Icon)), const Offset(778.0, 291.0)); - expect(tester.getBottomRight(find.byType(Icon)), const Offset(796.0, 309.0)); + expect(tester.getTopLeft(find.byKey(keyB)), const Offset(40.0, 0.0)); + expect(tester.getBottomRight(find.byKey(keyB)), const Offset(768.0, 600.0)); + expect(tester.getTopLeft(find.byType(Icon)), const Offset(772.0, 288.0)); + expect(tester.getBottomRight(find.byType(Icon)), const Offset(796.0, 312.0)); }); testWidgets('Chip padding - RTL', (WidgetTester tester) async { @@ -314,9 +467,9 @@ void main() { ); expect(tester.getTopRight(find.byKey(keyA)), const Offset(800.0 - 0.0, 284.0)); expect(tester.getBottomLeft(find.byKey(keyA)), const Offset(800.0 - 32.0, 316.0)); - expect(tester.getTopRight(find.byKey(keyB)), const Offset(800.0 - 40.0, 284.0)); - expect(tester.getBottomLeft(find.byKey(keyB)), const Offset(800.0 - 774.0, 316.0)); - expect(tester.getTopRight(find.byType(Icon)), const Offset(800.0 - 778.0, 291.0)); - expect(tester.getBottomLeft(find.byType(Icon)), const Offset(800.0 - 796.0, 309.0)); + expect(tester.getTopRight(find.byKey(keyB)), const Offset(800.0 - 40.0, 0.0)); + expect(tester.getBottomLeft(find.byKey(keyB)), const Offset(800.0 - 768.0, 600.0)); + expect(tester.getTopRight(find.byType(Icon)), const Offset(800.0 - 772.0, 288.0)); + expect(tester.getBottomLeft(find.byType(Icon)), const Offset(800.0 - 796.0, 312.0)); }); } diff --git a/packages/flutter/test/painting/circle_border_test.dart b/packages/flutter/test/painting/circle_border_test.dart index 9c6ca0a633d..8f302e0c18b 100644 --- a/packages/flutter/test/painting/circle_border_test.dart +++ b/packages/flutter/test/painting/circle_border_test.dart @@ -10,9 +10,9 @@ import 'common_matchers.dart'; void main() { test('CircleBorder', () { - final CircleBorder c10 = const CircleBorder(const BorderSide(width: 10.0)); - final CircleBorder c15 = const CircleBorder(const BorderSide(width: 15.0)); - final CircleBorder c20 = const CircleBorder(const BorderSide(width: 20.0)); + final CircleBorder c10 = const CircleBorder(side: const BorderSide(width: 10.0)); + final CircleBorder c15 = const CircleBorder(side: const BorderSide(width: 15.0)); + final CircleBorder c20 = const CircleBorder(side: const BorderSide(width: 20.0)); expect(c10.dimensions, const EdgeInsets.all(10.0)); expect(c10.scale(2.0), c20); expect(c20.scale(0.5), c10); diff --git a/packages/flutter/test/painting/rounded_rectangle_border_test.dart b/packages/flutter/test/painting/rounded_rectangle_border_test.dart index 18aae11c53c..76d0a9cdac6 100644 --- a/packages/flutter/test/painting/rounded_rectangle_border_test.dart +++ b/packages/flutter/test/painting/rounded_rectangle_border_test.dart @@ -38,7 +38,7 @@ void main() { test('RoundedRectangleBorder and CircleBorder', () { final RoundedRectangleBorder r = new RoundedRectangleBorder(side: BorderSide.none, borderRadius: new BorderRadius.circular(10.0)); - final CircleBorder c = const CircleBorder(BorderSide.none); + final CircleBorder c = const CircleBorder(side: BorderSide.none); final Rect rect = new Rect.fromLTWH(0.0, 0.0, 100.0, 20.0); // center is x=40..60 y=10 final Matcher looksLikeR = isPathThat( includes: const [ const Offset(30.0, 10.0), const Offset(50.0, 10.0), ], diff --git a/packages/flutter/test/painting/shape_decoration_test.dart b/packages/flutter/test/painting/shape_decoration_test.dart index f139b0c5873..ba8613d39bd 100644 --- a/packages/flutter/test/painting/shape_decoration_test.dart +++ b/packages/flutter/test/painting/shape_decoration_test.dart @@ -21,7 +21,7 @@ void main() { expect(() => new ShapeDecoration(color: colorR, shape: null), throwsAssertionError); expect( new ShapeDecoration.fromBoxDecoration(const BoxDecoration(shape: BoxShape.circle)), - const ShapeDecoration(shape: const CircleBorder(BorderSide.none)), + const ShapeDecoration(shape: const CircleBorder(side: BorderSide.none)), ); expect( new ShapeDecoration.fromBoxDecoration(new BoxDecoration(shape: BoxShape.rectangle, borderRadius: new BorderRadiusDirectional.circular(100.0))), @@ -29,7 +29,7 @@ void main() { ); expect( new ShapeDecoration.fromBoxDecoration(new BoxDecoration(shape: BoxShape.circle, border: new Border.all(color: colorG))), - new ShapeDecoration(shape: new CircleBorder(new BorderSide(color: colorG))), + new ShapeDecoration(shape: new CircleBorder(side: new BorderSide(color: colorG))), ); expect( new ShapeDecoration.fromBoxDecoration(new BoxDecoration(shape: BoxShape.rectangle, border: new Border.all(color: colorR))), diff --git a/packages/flutter/test/painting/stadium_border_test.dart b/packages/flutter/test/painting/stadium_border_test.dart new file mode 100644 index 00000000000..a4613c8da23 --- /dev/null +++ b/packages/flutter/test/painting/stadium_border_test.dart @@ -0,0 +1,161 @@ +// 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 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; +import 'common_matchers.dart'; + +void main() { + test('StadiumBorder', () { + final StadiumBorder c10 = const StadiumBorder(side: const BorderSide(width: 10.0)); + final StadiumBorder c15 = const StadiumBorder(side: const BorderSide(width: 15.0)); + final StadiumBorder c20 = const StadiumBorder(side: const BorderSide(width: 20.0)); + expect(c10.dimensions, const EdgeInsets.all(10.0)); + expect(c10.scale(2.0), c20); + expect(c20.scale(0.5), c10); + expect(ShapeBorder.lerp(c10, c20, 0.0), c10); + expect(ShapeBorder.lerp(c10, c20, 0.5), c15); + expect(ShapeBorder.lerp(c10, c20, 1.0), c20); + + final StadiumBorder c1 = const StadiumBorder(side: const BorderSide(width: 1.0)); + expect(c1.getOuterPath(new Rect.fromCircle(center: Offset.zero, radius: 1.0)), isUnitCircle); + final StadiumBorder c2 = const StadiumBorder(side: const BorderSide(width: 1.0)); + expect(c2.getInnerPath(new Rect.fromCircle(center: Offset.zero, radius: 2.0)), isUnitCircle); + final Rect rect = new Rect.fromLTRB(10.0, 20.0, 100.0, 200.0); + expect( + (Canvas canvas) => c10.paint(canvas, rect), + paints + ..rrect( + rrect: new RRect.fromRectAndRadius(rect.deflate(5.0), new Radius.circular(rect.shortestSide / 2.0 - 5.0)), + strokeWidth: 10.0, + ) + ); + }); + + test('StadiumBorder and CircleBorder', () { + final StadiumBorder stadium = const StadiumBorder(side: BorderSide.none); + final CircleBorder circle = const CircleBorder(side: BorderSide.none); + final Rect rect = new Rect.fromLTWH(0.0, 0.0, 100.0, 20.0); + final Matcher looksLikeS = isPathThat( + includes: const [ const Offset(30.0, 10.0), const Offset(50.0, 10.0), ], + excludes: const [ const Offset(1.0, 1.0), const Offset(99.0, 19.0), ], + ); + final Matcher looksLikeC = isPathThat( + includes: const [ const Offset(50.0, 10.0), ], + excludes: const [ const Offset(1.0, 1.0), const Offset(30.0, 10.0), const Offset(99.0, 19.0), ], + ); + expect(stadium.getOuterPath(rect), looksLikeS); + expect(circle.getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(stadium, circle, 0.1).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(stadium, circle, 0.9).getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, circle, 0.9), stadium, 0.1).getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, circle, 0.9), stadium, 0.9).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, circle, 0.1), circle, 0.1).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, circle, 0.1), circle, 0.9).getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, circle, 0.1), ShapeBorder.lerp(stadium, circle, 0.9), 0.1).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, circle, 0.1), ShapeBorder.lerp(stadium, circle, 0.9), 0.9).getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(stadium, ShapeBorder.lerp(stadium, circle, 0.9), 0.1).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(stadium, ShapeBorder.lerp(stadium, circle, 0.9), 0.9).getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(circle, ShapeBorder.lerp(stadium, circle, 0.1), 0.1).getOuterPath(rect), looksLikeC); + expect(ShapeBorder.lerp(circle, ShapeBorder.lerp(stadium, circle, 0.1), 0.9).getOuterPath(rect), looksLikeS); + + expect(ShapeBorder.lerp(stadium, circle, 0.1).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), 10.0% of the way to being a CircleBorder)'); + expect(ShapeBorder.lerp(stadium, circle, 0.2).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), 20.0% of the way to being a CircleBorder)'); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, circle, 0.1), ShapeBorder.lerp(stadium, circle, 0.9), 0.9).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), 82.0% of the way to being a CircleBorder)'); + + expect(ShapeBorder.lerp(circle, stadium, 0.9).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), 10.0% of the way to being a CircleBorder)'); + expect(ShapeBorder.lerp(circle, stadium, 0.8).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), 20.0% of the way to being a CircleBorder)'); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, circle, 0.9), ShapeBorder.lerp(stadium, circle, 0.1), 0.1).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), 82.0% of the way to being a CircleBorder)'); + + expect(ShapeBorder.lerp(stadium, circle, 0.1), ShapeBorder.lerp(stadium, circle, 0.1)); + expect(ShapeBorder.lerp(stadium, circle, 0.1).hashCode, ShapeBorder.lerp(stadium, circle, 0.1).hashCode); + + final ShapeBorder direct50 = ShapeBorder.lerp(stadium, circle, 0.5); + final ShapeBorder indirect50 = ShapeBorder.lerp(ShapeBorder.lerp(circle, stadium, 0.1), ShapeBorder.lerp(circle, stadium, 0.9), 0.5); + expect(direct50, indirect50); + expect(direct50.hashCode, indirect50.hashCode); + expect(direct50.toString(), indirect50.toString()); + }); + + test('StadiumBorder and RoundedRectBorder', () { + final StadiumBorder stadium = const StadiumBorder(side: BorderSide.none); + final RoundedRectangleBorder rrect = const RoundedRectangleBorder(side: BorderSide.none); + final Rect rect = new Rect.fromLTWH(0.0, 0.0, 100.0, 50.0); + final Matcher looksLikeS = isPathThat( + includes: const [ + const Offset(25.0, 25.0), + const Offset(50.0, 25.0), + const Offset(7.33, 7.33), + ], + excludes: const [ + const Offset(0.001, 0.001), + const Offset(99.999, 0.001), + const Offset(99.999, 49.999), + const Offset(0.001, 49.999), + ], + ); + final Matcher looksLikeR = isPathThat( + includes: const [ + const Offset(25.0, 25.0), + const Offset(50.0, 25.0), + const Offset(7.33, 7.33), + const Offset(4.0, 4.0), + const Offset(96.0, 4.0), + const Offset(96.0, 46.0), + const Offset(4.0, 46.0), + ], + ); + expect(stadium.getOuterPath(rect), looksLikeS); + expect(rrect.getOuterPath(rect), looksLikeR); + expect(ShapeBorder.lerp(stadium, rrect, 0.1).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(stadium, rrect, 0.9).getOuterPath(rect), looksLikeR); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, rrect, 0.9), stadium, 0.1).getOuterPath(rect), looksLikeR); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, rrect, 0.9), stadium, 0.9).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, rrect, 0.1), rrect, 0.1).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, rrect, 0.1), rrect, 0.9).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, rrect, 0.1), ShapeBorder.lerp(stadium, rrect, 0.9), 0.1).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, rrect, 0.1), ShapeBorder.lerp(stadium, rrect, 0.9), 0.9).getOuterPath(rect), looksLikeR); + expect(ShapeBorder.lerp(stadium, ShapeBorder.lerp(stadium, rrect, 0.9), 0.1).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(stadium, ShapeBorder.lerp(stadium, rrect, 0.9), 0.9).getOuterPath(rect), looksLikeR); + expect(ShapeBorder.lerp(rrect, ShapeBorder.lerp(stadium, rrect, 0.1), 0.1).getOuterPath(rect), looksLikeS); + expect(ShapeBorder.lerp(rrect, ShapeBorder.lerp(stadium, rrect, 0.1), 0.9).getOuterPath(rect), looksLikeS); + + expect(ShapeBorder.lerp(stadium, rrect, 0.1).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), ' + 'BorderRadius.zero, 10.0% of the way to being a RoundedRectangleBorder)'); + expect(ShapeBorder.lerp(stadium, rrect, 0.2).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), ' + 'BorderRadius.zero, 20.0% of the way to being a RoundedRectangleBorder)'); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, rrect, 0.1), ShapeBorder.lerp(stadium, rrect, 0.9), 0.9).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), ' + 'BorderRadius.zero, 82.0% of the way to being a RoundedRectangleBorder)'); + + expect(ShapeBorder.lerp(rrect, stadium, 0.9).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), ' + 'BorderRadius.zero, 10.0% of the way to being a RoundedRectangleBorder)'); + expect(ShapeBorder.lerp(rrect, stadium, 0.8).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), ' + 'BorderRadius.zero, 20.0% of the way to being a RoundedRectangleBorder)'); + expect(ShapeBorder.lerp(ShapeBorder.lerp(stadium, rrect, 0.9), ShapeBorder.lerp(stadium, rrect, 0.1), 0.1).toString(), + 'StadiumBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), ' + 'BorderRadius.zero, 82.0% of the way to being a RoundedRectangleBorder)'); + + expect(ShapeBorder.lerp(stadium, rrect, 0.1), ShapeBorder.lerp(stadium, rrect, 0.1)); + expect(ShapeBorder.lerp(stadium, rrect, 0.1).hashCode, ShapeBorder.lerp(stadium, rrect, 0.1).hashCode); + + final ShapeBorder direct50 = ShapeBorder.lerp(stadium, rrect, 0.5); + final ShapeBorder indirect50 = ShapeBorder.lerp(ShapeBorder.lerp(rrect, stadium, 0.1), ShapeBorder.lerp(rrect, stadium, 0.9), 0.5); + expect(direct50, indirect50); + expect(direct50.hashCode, indirect50.hashCode); + expect(direct50.toString(), indirect50.toString()); + }); +}