Commit 0e34c7f2 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

CircleBorder (#12570)

parent 52fbcefe
......@@ -25,6 +25,7 @@ export 'src/painting/box_border.dart';
export 'src/painting/box_decoration.dart';
export 'src/painting/box_fit.dart';
export 'src/painting/box_shadow.dart';
export 'src/painting/circle_border.dart';
export 'src/painting/colors.dart';
export 'src/painting/decoration.dart';
export 'src/painting/edge_insets.dart';
......
......@@ -484,7 +484,7 @@ class Border extends BoxBorder {
@override
String toString() {
if (isUniform)
return 'Border.all($top)';
return '$runtimeType.all($top)';
final List<String> arguments = <String>[];
if (top != BorderSide.none)
arguments.add('top: $top');
......@@ -494,7 +494,7 @@ class Border extends BoxBorder {
arguments.add('bottom: $bottom');
if (left != BorderSide.none)
arguments.add('left: $left');
return 'Border(${arguments.join(", ")})';
return '$runtimeType(${arguments.join(", ")})';
}
}
......@@ -807,6 +807,6 @@ class BorderDirectional extends BoxBorder {
arguments.add('end: $end');
if (bottom != BorderSide.none)
arguments.add('bottom: $bottom');
return 'BorderDirectional(${arguments.join(", ")})';
return '$runtimeType(${arguments.join(", ")})';
}
}
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'basic_types.dart';
import 'borders.dart';
import 'edge_insets.dart';
/// A border that fits a circle within the available space.
///
/// Typically used with [ShapeDecoration] to draw a circle.
///
/// The [dimensions] assume that the border is being used in a square space.
/// When applied to a rectangular space, the border paints in the center of the
/// rectangle.
///
/// See also:
///
/// * [BorderSide], which is used to describe each side of the box.
/// * [Border], which, when used with [BoxDecoration], can also
/// describe a circle.
class CircleBorder extends ShapeBorder {
/// Create a circle border.
///
/// The [side] argument must not be null.
const CircleBorder(this.side) : 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 CircleBorder(side.scale(t));
@override
ShapeBorder lerpFrom(ShapeBorder a, double t) {
if (a is CircleBorder)
return new CircleBorder(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 super.lerpTo(b, t);
}
@override
Path getInnerPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addOval(new Rect.fromCircle(
center: rect.center,
radius: math.max(0.0, rect.shortestSide / 2.0 - side.width),
));
}
@override
Path getOuterPath(Rect rect, { TextDirection textDirection }) {
return new Path()
..addOval(new Rect.fromCircle(
center: rect.center,
radius: rect.shortestSide / 2.0,
));
}
@override
void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) {
switch (side.style) {
case BorderStyle.none:
break;
case BorderStyle.solid:
canvas.drawCircle(rect.center, (rect.shortestSide - side.width) / 2.0, side.toPaint());
}
}
@override
bool operator ==(dynamic other) {
if (runtimeType != other.runtimeType)
return false;
final CircleBorder typedOther = other;
return side == typedOther.side;
}
@override
int get hashCode => side.hashCode;
@override
String toString() {
return '$runtimeType($side)';
}
}
......@@ -115,22 +115,13 @@ void main() {
expect(() => BoxBorder.lerp(new SillyBorder(), const Border(), 2.0), throwsFlutterError);
});
void verifyPath(Path path, {
Iterable<Offset> includes: const <Offset>[],
Iterable<Offset> excludes: const <Offset>[],
}) {
for (Offset offset in includes)
expect(path.contains(offset), isTrue, reason: 'Offset $offset should be inside the path.');
for (Offset offset in excludes)
expect(path.contains(offset), isFalse, reason: 'Offset $offset should be outside the path.');
}
test('BoxBorder.getInnerPath / BoxBorder.getOuterPath', () {
// for Border, BorderDirectional
final Border border = const Border(top: const BorderSide(width: 10.0), right: const BorderSide(width: 20.0));
final BorderDirectional borderDirectional = const BorderDirectional(top: const BorderSide(width: 10.0), end: const BorderSide(width: 20.0));
verifyPath(
expect(
border.getOuterPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl),
isPathThat(
includes: <Offset>[
const Offset(50.0, 60.0),
const Offset(60.0, 60.0),
......@@ -152,10 +143,12 @@ void main() {
const Offset(-10.0, 0.0),
const Offset(1000.0, 1000.0),
],
),
);
verifyPath(
expect(
border.getInnerPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl),
// inner path is a rect from 50.0,70.0 to 90.0,190.0
isPathThat(
includes: <Offset>[
const Offset(50.0, 70.0),
const Offset(55.0, 70.0),
......@@ -188,9 +181,11 @@ void main() {
const Offset(110.0, 190.0),
const Offset(1000.0, 1000.0),
],
)
);
verifyPath(
expect(
borderDirectional.getOuterPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl),
isPathThat(
includes: <Offset>[
const Offset(50.0, 60.0),
const Offset(60.0, 60.0),
......@@ -212,10 +207,12 @@ void main() {
const Offset(-10.0, 0.0),
const Offset(1000.0, 1000.0),
],
),
);
verifyPath(
expect(
borderDirectional.getInnerPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.rtl),
// inner path is a rect from 70.0,70.0 to 110.0,190.0
isPathThat(
includes: <Offset>[
const Offset(70.0, 70.0),
const Offset(70.0, 71.0),
......@@ -251,9 +248,11 @@ void main() {
const Offset(111.0, 191.0),
const Offset(1000.0, 1000.0),
],
),
);
verifyPath(
expect(
borderDirectional.getOuterPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.ltr),
isPathThat(
includes: <Offset>[
const Offset(50.0, 60.0),
const Offset(60.0, 60.0),
......@@ -275,10 +274,12 @@ void main() {
const Offset(-10.0, 0.0),
const Offset(1000.0, 1000.0),
],
),
);
verifyPath(
expect(
borderDirectional.getInnerPath(new Rect.fromLTRB(50.0, 60.0, 110.0, 190.0), textDirection: TextDirection.ltr),
// inner path is a rect from 50.0,70.0 to 90.0,190.0
isPathThat(
includes: <Offset>[
const Offset(50.0, 70.0),
const Offset(50.0, 75.0),
......@@ -311,6 +312,7 @@ void main() {
const Offset(110.0, 190.0),
const Offset(1000.0, 1000.0),
],
),
);
});
......
// 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';
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));
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 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(
(Canvas canvas) => c10.paint(canvas, new Rect.fromLTWH(10.0, 20.0, 30.0, 40.0)),
paints
..circle(x: 25.0, y: 40.0, radius: 10.0, strokeWidth: 10.0)
);
});
}
......@@ -327,6 +327,65 @@ abstract class PaintPattern {
void something(PaintPatternPredicate predicate);
}
/// Matches a [Path] that contains (as defined by [Path.contains]) the given
/// `includes` points and does not contain the given `excludes` points.
Matcher isPathThat({
Iterable<Offset> includes: const <Offset>[],
Iterable<Offset> excludes: const <Offset>[],
}) {
return new _PathMatcher(includes.toList(), excludes.toList());
}
class _PathMatcher extends Matcher {
_PathMatcher(this.includes, this.excludes);
List<Offset> includes;
List<Offset> excludes;
@override
bool matches(Object object, Map<dynamic, dynamic> matchState) {
if (object is! Path) {
matchState[this] = 'The given object ($object) was not a Path.';
return false;
}
final Path path = object;
final List<String> errors = <String>[];
for (Offset offset in includes) {
if (!path.contains(offset))
errors.add('Offset $offset should be inside the path, but is not.');
}
for (Offset offset in excludes) {
if (path.contains(offset))
errors.add('Offset $offset should be outside the path, but is not.');
}
if (errors.isEmpty)
return true;
matchState[this] = 'Not all the given points were inside or outside the path as expected:\n ${errors.join("\n ")}';
return false;
}
@override
Description describe(Description description) {
String points(List<Offset> list) {
final int count = list.length;
if (count == 1)
return 'one particular point';
return '$count particular points';
}
return description.add('A Path that contains ${points(includes)} but does not contain ${points(excludes)}.');
}
@override
Description describeMismatch(
dynamic item,
Description description,
Map<dynamic, dynamic> matchState,
bool verbose,
) {
return description.add(matchState[this]);
}
}
class _MismatchedCall {
const _MismatchedCall(this.message, this.callIntroduction, this.call) : assert(call != null);
final String message;
......
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