Adding proper accommodation for textScaleFactor in chips, and StadiumBorder border. (#12533)

In order to allow chips to be properly drawn when they expand in size (without
using IntrinsicHeight), I needed a BoxDecoration shape that would be dependent
upon the rendered height of the widget. This seemed to be pretty generally
useful, so I added a new ShapeDecoration called StadiumBorder. It uses the
minimum dimension to adjust the BorderRadius of a rounded rect in the shape
decoration.

I also converted some uses of BoxShape to be case statements, updated the
chips to use the StadiumBorder decoration, and updated some of the metrics to match
the Material spec, as well as implementing lerping to and from StadiumBorder.
This commit is contained in:
Greg Spencer 2017-11-01 19:37:02 -07:00 committed by GitHub
parent 5c1320e5b9
commit 05e10633f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 835 additions and 75 deletions

View File

@ -28,7 +28,7 @@ class _ChipDemoState extends State<ChipDemo> {
),
const Chip(
avatar: const CircleAvatar(child: const Text('B')),
label: const Text('Blueberry')
label: const Text('Blueberry'),
),
];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -36,12 +36,17 @@ class BoxConstraintsTween extends Tween<BoxConstraints> {
/// This class specializes the interpolation of [Tween<BoxConstraints>] 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<Decoration> {
/// Creates a decoration tween.
///

View File

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

View File

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

View File

@ -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 <Offset>[ const Offset(30.0, 10.0), const Offset(50.0, 10.0), ],

View File

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

View File

@ -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 <Offset>[ const Offset(30.0, 10.0), const Offset(50.0, 10.0), ],
excludes: const <Offset>[ const Offset(1.0, 1.0), const Offset(99.0, 19.0), ],
);
final Matcher looksLikeC = isPathThat(
includes: const <Offset>[ const Offset(50.0, 10.0), ],
excludes: const <Offset>[ 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 <Offset>[
const Offset(25.0, 25.0),
const Offset(50.0, 25.0),
const Offset(7.33, 7.33),
],
excludes: const <Offset>[
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 <Offset>[
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());
});
}