Unverified Commit 05e10633 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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.
parent 5c1320e5
......@@ -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'),
),
];
......
......@@ -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';
......@@ -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
......@@ -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;
}
if (borderRadius != null) {
BoxBorder._paintUniformBorderWithRadius(canvas, rect, top, borderRadius);
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;
}
BoxBorder._paintUniformBorderWithRectangle(canvas, rect, top);
return;
}
}
......
......@@ -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);
}
......
......@@ -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();
}
......
// 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)';
}
}
......@@ -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
......
......@@ -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.
///
......
......@@ -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.
///
......
......@@ -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));
});
}
......@@ -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);
......
......@@ -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), ],
......
......@@ -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))),
......
// 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());
});
}
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