Commit d920fdd1 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

RoundedRectangleBorder (#12591)

parent d8380201
......@@ -35,6 +35,7 @@ export 'src/painting/geometry.dart';
export 'src/painting/gradient.dart';
export 'src/painting/images.dart';
export 'src/painting/matrix_utils.dart';
export 'src/painting/rounded_rectangle_border.dart';
export 'src/painting/text_painter.dart';
export 'src/painting/text_span.dart';
export 'src/painting/text_style.dart';
......@@ -10,16 +10,29 @@ import 'borders.dart';
import 'edge_insets.dart';
/// The shape to use when rendering a [Border] or [BoxDecoration].
///
/// Consider using [ShapeBorder] subclasses directly (with [ShapeDecoration]),
/// instead of using [BoxShape] and [Border], if the shapes will need to be
/// interpolated or animated. The [Border] class cannot interpolate between
/// different shapes.
enum BoxShape {
/// An axis-aligned, 2D rectangle. May have rounded corners (described by a
/// [BorderRadius]). The edges of the rectangle will match the edges of the box
/// into which the [Border] or [BoxDecoration] is painted.
///
/// See also:
///
/// * [RoundedRectangleBorder], the equivalent [ShapeBorder].
rectangle,
/// A circle centered in the middle of the box into which the [Border] or
/// [BoxDecoration] is painted. The diameter of the circle is the shortest
/// dimension of the box, either the width or the height, such that the circle
/// touches the edges of the box.
///
/// See also:
///
/// * [CircleBorder], the equivalent [ShapeBorder].
circle,
}
......
......@@ -140,7 +140,14 @@ class BoxDecoration extends Decoration {
/// The shape to fill the background [color], [gradient], and [image] into and
/// to cast as the [boxShadow].
///
/// If this is [BoxShape.rectangle] then [borderRadius] is ignored.
/// If this is [BoxShape.circle] then [borderRadius] is ignored.
///
/// The [shape] cannot be interpolated; animating between two [BoxDecoration]s
/// with different [shape]s will result in a discontinuity in the rendering.
/// To interpolate between two shapes, consider using [ShapeDecoration] and
/// different [ShapeBorder]s; in particular, [CircleBorder] instead of
/// [BoxShape.circle] and [RoundedRectangleBorder] instead of
/// [BoxShape.rectangle].
final BoxShape shape;
@override
......
// 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 'package:flutter/foundation.dart';
import 'basic_types.dart';
import 'border_radius.dart';
import 'borders.dart';
import 'circle_border.dart';
import 'edge_insets.dart';
/// A rectangular border with rounded corners.
///
/// Typically used with [ShapeDecoration] to draw a box with a rounded
/// rectangle.
///
/// This shape can interpolate to and from [CircleBorder].
///
/// See also:
///
/// * [BorderSide], which is used to describe each side of the box.
/// * [Border], which, when used with [BoxDecoration], can also
/// describe a rounded rectangle.
class RoundedRectangleBorder extends ShapeBorder {
/// Creates a rounded rectangle border.
///
/// The arguments must not be null.
RoundedRectangleBorder({
this.side: BorderSide.none,
this.borderRadius: BorderRadius.zero,
}) : assert(side != null),
assert(borderRadius != null);
/// The style of this border.
final BorderSide side;
/// The radii for each corner.
final BorderRadius borderRadius;
@override
EdgeInsetsGeometry get dimensions {
return new EdgeInsets.all(side.width);
}
@override
ShapeBorder scale(double t) {
return new RoundedRectangleBorder(
side: side.scale(t),
borderRadius: borderRadius * t,
);
}
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a is RoundedRectangleBorder) {
return new RoundedRectangleBorder(
side: BorderSide.lerp(a.side, side, t),
borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t),
);
}
if (a is CircleBorder) {
return new _RoundedRectangleToCircleBorder(
side: BorderSide.lerp(a.side, side, t),
borderRadius: borderRadius,
circleness: 1.0 - t,
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
if (b is RoundedRectangleBorder) {
return new RoundedRectangleBorder(
side: BorderSide.lerp(side, b.side, t),
borderRadius: BorderRadius.lerp(borderRadius, b.borderRadius, t),
);
}
if (b is CircleBorder) {
return new _RoundedRectangleToCircleBorder(
side: BorderSide.lerp(side, b.side, t),
borderRadius: borderRadius,
circleness: t,
);
}
return super.lerpTo(b, t);
}
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRRect(borderRadius.toRRect(rect).deflate(side.width));
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addRRect(borderRadius.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(borderRadius.toRRect(rect), side.toPaint());
} else {
final RRect outer = borderRadius.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 RoundedRectangleBorder typedOther = other;
return side == typedOther.side
&& borderRadius == typedOther.borderRadius;
}
@override
int get hashCode => hashValues(side, borderRadius);
@override
String toString() {
return '$runtimeType($side, $borderRadius)';
}
}
class _RoundedRectangleToCircleBorder extends ShapeBorder {
_RoundedRectangleToCircleBorder({
this.side: BorderSide.none,
this.borderRadius: BorderRadius.zero,
@required this.circleness,
}) : assert(side != null),
assert(borderRadius != null),
assert(circleness != null);
final BorderSide side;
final BorderRadius borderRadius;
final double circleness;
@override
EdgeInsetsGeometry get dimensions {
return new EdgeInsets.all(side.width);
}
@override
ShapeBorder scale(double t) {
return new _RoundedRectangleToCircleBorder(
side: side.scale(t),
borderRadius: borderRadius * t,
circleness: t,
);
}
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a is RoundedRectangleBorder) {
return new _RoundedRectangleToCircleBorder(
side: BorderSide.lerp(a.side, side, t),
borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t),
circleness: circleness * t,
);
}
if (a is CircleBorder) {
return new _RoundedRectangleToCircleBorder(
side: BorderSide.lerp(a.side, side, t),
borderRadius: borderRadius,
circleness: circleness + (1.0 - circleness) * (1.0 - t),
);
}
if (a is _RoundedRectangleToCircleBorder) {
return new _RoundedRectangleToCircleBorder(
side: BorderSide.lerp(a.side, side, t),
borderRadius: BorderRadius.lerp(a.borderRadius, borderRadius, t),
circleness: ui.lerpDouble(a.circleness, circleness, t),
);
}
return super.lerpFrom(a, t);
}
@override
ShapeBorder lerpTo(ShapeBorder b, double t) {
if (b is RoundedRectangleBorder) {
return new _RoundedRectangleToCircleBorder(
side: BorderSide.lerp(side, b.side, t),
borderRadius: BorderRadius.lerp(borderRadius, b.borderRadius, t),
circleness: circleness * (1.0 - t),
);
}
if (b is CircleBorder) {
return new _RoundedRectangleToCircleBorder(
side: BorderSide.lerp(side, b.side, t),
borderRadius: borderRadius,
circleness: circleness + (1.0 - circleness) * t,
);
}
if (b is _RoundedRectangleToCircleBorder) {
return new _RoundedRectangleToCircleBorder(
side: BorderSide.lerp(side, b.side, t),
borderRadius: BorderRadius.lerp(borderRadius, b.borderRadius, 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) {
if (circleness == 0.0)
return borderRadius;
return BorderRadius.lerp(borderRadius, new BorderRadius.circular(rect.shortestSide / 2.0), circleness);
}
@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 _RoundedRectangleToCircleBorder typedOther = other;
return side == typedOther.side
&& borderRadius == typedOther.borderRadius
&& circleness == typedOther.circleness;
}
@override
int get hashCode => hashValues(side, borderRadius, circleness);
@override
String toString() {
return 'RoundedRectangleBorder($side, $borderRadius, ${(circleness * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)';
}
}
......@@ -7,6 +7,54 @@ import 'package:flutter_test/flutter_test.dart';
import '../rendering/mock_canvas.dart';
final Matcher isUnitCircle = isPathThat(
includes: <Offset>[
const Offset(-0.6035617555492896, 0.2230970398703236),
const Offset(-0.7738478165627277, 0.5640447581420576),
const Offset(-0.46090034164788385, -0.692017006684612),
const Offset(-0.2138540316101296, -0.09997005339529785),
const Offset(-0.46919827227410416, 0.29581721423767027),
const Offset(-0.43628713652733153, 0.5065324817995975),
const Offset(0.0, 0.0),
const Offset(0.49296904381712725, -0.5922438805080081),
const Offset(0.2901141594861445, -0.3181478162967859),
const Offset(0.45229946324502146, 0.4324593232323706),
const Offset(0.11827752132593572, 0.806442226027837),
const Offset(0.8854165569581154, -0.08604230149167624),
],
excludes: <Offset>[
const Offset(-100.0, -100.0),
const Offset(-100.0, 100.0),
const Offset(-1.1104403014186688, -1.1234939207590569),
const Offset(-1.1852827482514838, -0.5029551986333607),
const Offset(-1.0253256532179804, -0.02034402043932526),
const Offset(-1.4488532714237397, 0.4948740308904742),
const Offset(-1.03142206223176, 0.81070400258819),
const Offset(-1.006747917852356, 1.3712062218039343),
const Offset(-0.5241429900291878, -1.2852518410112541),
const Offset(-0.8879593765104428, -0.9999680025850874),
const Offset(-0.9120835110799488, -0.4361605900585557),
const Offset(-0.8184877240407303, 1.1202520775469589),
const Offset(-0.15746058420492282, -1.1905035795387513),
const Offset(-0.11519948876183506, 1.3848147258237393),
const Offset(0.0035741796943844495, -1.3383908620447724),
const Offset(0.34408827443814394, 1.4514436242950461),
const Offset(0.709487222145941, -1.3468012918181573),
const Offset(0.6287522653614315, -0.8315879623940617),
const Offset(0.9716071801865485, 0.24311969613525442),
const Offset(0.7632982576031955, 0.8329765574976169),
const Offset(0.9923766847309081, 1.0592617071813715),
const Offset(1.2696730082820435, -1.0353385446957046),
const Offset(1.4266154921521208, -0.8382633931857755),
const Offset(1.298035226938996, -0.11544603567954526),
const Offset(1.4143230992455558, 0.10842501221141165),
const Offset(1.465352952354424, 0.6999947490821032),
const Offset(1.0462985816010146, 1.3874230508561505),
const Offset(100.0, -100.0),
const Offset(100.0, 100.0),
],
);
void main() {
test('CircleBorder', () {
final CircleBorder c10 = const CircleBorder(const BorderSide(width: 10.0));
......@@ -18,53 +66,6 @@ void main() {
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 Matcher isUnitCircle = isPathThat(
includes: <Offset>[
const Offset(-0.6035617555492896, 0.2230970398703236),
const Offset(-0.7738478165627277, 0.5640447581420576),
const Offset(-0.46090034164788385, -0.692017006684612),
const Offset(-0.2138540316101296, -0.09997005339529785),
const Offset(-0.46919827227410416, 0.29581721423767027),
const Offset(-0.43628713652733153, 0.5065324817995975),
const Offset(0.0, 0.0),
const Offset(0.49296904381712725, -0.5922438805080081),
const Offset(0.2901141594861445, -0.3181478162967859),
const Offset(0.45229946324502146, 0.4324593232323706),
const Offset(0.11827752132593572, 0.806442226027837),
const Offset(0.8854165569581154, -0.08604230149167624),
],
excludes: <Offset>[
const Offset(-100.0, -100.0),
const Offset(-100.0, 100.0),
const Offset(-1.1104403014186688, -1.1234939207590569),
const Offset(-1.1852827482514838, -0.5029551986333607),
const Offset(-1.0253256532179804, -0.02034402043932526),
const Offset(-1.4488532714237397, 0.4948740308904742),
const Offset(-1.03142206223176, 0.81070400258819),
const Offset(-1.006747917852356, 1.3712062218039343),
const Offset(-0.5241429900291878, -1.2852518410112541),
const Offset(-0.8879593765104428, -0.9999680025850874),
const Offset(-0.9120835110799488, -0.4361605900585557),
const Offset(-0.8184877240407303, 1.1202520775469589),
const Offset(-0.15746058420492282, -1.1905035795387513),
const Offset(-0.11519948876183506, 1.3848147258237393),
const Offset(0.0035741796943844495, -1.3383908620447724),
const Offset(0.34408827443814394, 1.4514436242950461),
const Offset(0.709487222145941, -1.3468012918181573),
const Offset(0.6287522653614315, -0.8315879623940617),
const Offset(0.9716071801865485, 0.24311969613525442),
const Offset(0.7632982576031955, 0.8329765574976169),
const Offset(0.9923766847309081, 1.0592617071813715),
const Offset(1.2696730082820435, -1.0353385446957046),
const Offset(1.4266154921521208, -0.8382633931857755),
const Offset(1.298035226938996, -0.11544603567954526),
const Offset(1.4143230992455558, 0.10842501221141165),
const Offset(1.465352952354424, 0.6999947490821032),
const Offset(1.0462985816010146, 1.3874230508561505),
const Offset(100.0, -100.0),
const Offset(100.0, 100.0),
],
);
expect(c10.getInnerPath(new Rect.fromCircle(center: Offset.zero, radius: 1.0).inflate(10.0)), isUnitCircle);
expect(c10.getOuterPath(new Rect.fromCircle(center: Offset.zero, radius: 1.0)), isUnitCircle);
expect(
......
// 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 'circle_border_test.dart';
void main() {
test('RoundedRectangleBorder', () {
final RoundedRectangleBorder c10 = new RoundedRectangleBorder(side: const BorderSide(width: 10.0), borderRadius: new BorderRadius.circular(100.0));
final RoundedRectangleBorder c15 = new RoundedRectangleBorder(side: const BorderSide(width: 15.0), borderRadius: new BorderRadius.circular(150.0));
final RoundedRectangleBorder c20 = new RoundedRectangleBorder(side: const BorderSide(width: 20.0), borderRadius: new BorderRadius.circular(200.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 RoundedRectangleBorder c1 = new RoundedRectangleBorder(side: const BorderSide(width: 1.0), borderRadius: new BorderRadius.circular(1.0));
final RoundedRectangleBorder c2 = new RoundedRectangleBorder(side: const BorderSide(width: 1.0), borderRadius: new BorderRadius.circular(2.0));
expect(c2.getInnerPath(new Rect.fromCircle(center: Offset.zero, radius: 2.0)), isUnitCircle);
expect(c1.getOuterPath(new Rect.fromCircle(center: Offset.zero, radius: 1.0)), isUnitCircle);
final Rect rect = new Rect.fromLTRB(10.0, 20.0, 80.0, 190.0);
expect(
(Canvas canvas) => c10.paint(canvas, rect),
paints
..drrect(
outer: new RRect.fromRectAndRadius(rect, const Radius.circular(100.0)),
inner: new RRect.fromRectAndRadius(rect.deflate(10.0), const Radius.circular(90.0)),
strokeWidth: 0.0,
)
);
});
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 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), ],
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(r.getOuterPath(rect), looksLikeR);
expect(c.getOuterPath(rect), looksLikeC);
expect(ShapeBorder.lerp(r, c, 0.1).getOuterPath(rect), looksLikeR);
expect(ShapeBorder.lerp(r, c, 0.9).getOuterPath(rect), looksLikeC);
expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.9), r, 0.1).getOuterPath(rect), looksLikeC);
expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.9), r, 0.9).getOuterPath(rect), looksLikeR);
expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.1), c, 0.1).getOuterPath(rect), looksLikeR);
expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.1), c, 0.9).getOuterPath(rect), looksLikeC);
expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.1), ShapeBorder.lerp(r, c, 0.9), 0.1).getOuterPath(rect), looksLikeR);
expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.1), ShapeBorder.lerp(r, c, 0.9), 0.9).getOuterPath(rect), looksLikeC);
expect(ShapeBorder.lerp(r, ShapeBorder.lerp(r, c, 0.9), 0.1).getOuterPath(rect), looksLikeR);
expect(ShapeBorder.lerp(r, ShapeBorder.lerp(r, c, 0.9), 0.9).getOuterPath(rect), looksLikeC);
expect(ShapeBorder.lerp(c, ShapeBorder.lerp(r, c, 0.1), 0.1).getOuterPath(rect), looksLikeC);
expect(ShapeBorder.lerp(c, ShapeBorder.lerp(r, c, 0.1), 0.9).getOuterPath(rect), looksLikeR);
expect(ShapeBorder.lerp(r, c, 0.1).toString(),
'RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(10.0), 10.0% of the way to being a CircleBorder)');
expect(ShapeBorder.lerp(r, c, 0.2).toString(),
'RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(10.0), 20.0% of the way to being a CircleBorder)');
expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.1), ShapeBorder.lerp(r, c, 0.9), 0.9).toString(),
'RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(10.0), 82.0% of the way to being a CircleBorder)');
expect(ShapeBorder.lerp(c, r, 0.9).toString(),
'RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(10.0), 10.0% of the way to being a CircleBorder)');
expect(ShapeBorder.lerp(c, r, 0.8).toString(),
'RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(10.0), 20.0% of the way to being a CircleBorder)');
expect(ShapeBorder.lerp(ShapeBorder.lerp(r, c, 0.9), ShapeBorder.lerp(r, c, 0.1), 0.1).toString(),
'RoundedRectangleBorder(BorderSide(Color(0xff000000), 0.0, BorderStyle.none), BorderRadius.circular(10.0), 82.0% of the way to being a CircleBorder)');
expect(ShapeBorder.lerp(r, c, 0.1), ShapeBorder.lerp(r, c, 0.1));
expect(ShapeBorder.lerp(r, c, 0.1).hashCode, ShapeBorder.lerp(r, c, 0.1).hashCode);
final ShapeBorder direct50 = ShapeBorder.lerp(r, c, 0.5);
final ShapeBorder indirect50 = ShapeBorder.lerp(ShapeBorder.lerp(c, r, 0.1), ShapeBorder.lerp(c, r, 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