Skip to content

Commit 22cb06b

Browse files
authored
Add OvalBorder and BoxShape.oval (#103833)
1 parent c8c2e3d commit 22cb06b

File tree

10 files changed

+349
-63
lines changed

10 files changed

+349
-63
lines changed

packages/flutter/lib/painting.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export 'src/painting/image_stream.dart';
4949
export 'src/painting/inline_span.dart';
5050
export 'src/painting/matrix_utils.dart';
5151
export 'src/painting/notched_shapes.dart';
52+
export 'src/painting/oval_border.dart';
5253
export 'src/painting/paint_utilities.dart';
5354
export 'src/painting/placeholder_span.dart';
5455
export 'src/painting/rounded_rectangle_border.dart';

packages/flutter/lib/src/painting/circle_border.dart

Lines changed: 96 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'dart:math' as math;
5+
import 'dart:ui' as ui show lerpDouble;
66

77
import 'package:flutter/foundation.dart';
88

@@ -18,16 +18,32 @@ import 'edge_insets.dart';
1818
/// When applied to a rectangular space, the border paints in the center of the
1919
/// rectangle.
2020
///
21+
/// The [eccentricity] parameter describes how much a circle will deform to
22+
/// fit the rectangle it is a border for. A value of zero implies no
23+
/// deformation (a circle touching at least two sides of the rectangle), a
24+
/// value of one implies full deformation (an oval touching all sides of the
25+
/// rectangle).
26+
///
2127
/// See also:
2228
///
29+
/// * [OvalBorder], which draws a Circle touching all the edges of the box.
2330
/// * [BorderSide], which is used to describe each side of the box.
24-
/// * [Border], which, when used with [BoxDecoration], can also
25-
/// describe a circle.
31+
/// * [Border], which, when used with [BoxDecoration], can also describe a circle.
2632
class CircleBorder extends OutlinedBorder {
2733
/// Create a circle border.
2834
///
2935
/// The [side] argument must not be null.
30-
const CircleBorder({ super.side }) : assert(side != null);
36+
const CircleBorder({ super.side, this.eccentricity = 0.0 })
37+
: assert(side != null),
38+
assert(eccentricity != null),
39+
assert(eccentricity >= 0.0, 'The eccentricity argument $eccentricity is not greater than or equal to zero.'),
40+
assert(eccentricity <= 1.0, 'The eccentricity argument $eccentricity is not less than or equal to one.');
41+
42+
/// Defines the ratio (0.0-1.0) from which the border will deform
43+
/// to fit a rectangle.
44+
/// When 0.0, it draws a circle touching at least two sides of the rectangle.
45+
/// When 1.0, it draws an oval touching all sides of the rectangle.
46+
final double eccentricity;
3147

3248
@override
3349
EdgeInsetsGeometry get dimensions {
@@ -42,58 +58,56 @@ class CircleBorder extends OutlinedBorder {
4258
}
4359

4460
@override
45-
ShapeBorder scale(double t) => CircleBorder(side: side.scale(t));
61+
ShapeBorder scale(double t) => CircleBorder(side: side.scale(t), eccentricity: eccentricity);
4662

4763
@override
4864
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
4965
if (a is CircleBorder) {
50-
return CircleBorder(side: BorderSide.lerp(a.side, side, t));
66+
return CircleBorder(
67+
side: BorderSide.lerp(a.side, side, t),
68+
eccentricity: clampDouble(ui.lerpDouble(a.eccentricity, eccentricity, t)!, 0.0, 1.0),
69+
);
5170
}
5271
return super.lerpFrom(a, t);
5372
}
5473

5574
@override
5675
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
5776
if (b is CircleBorder) {
58-
return CircleBorder(side: BorderSide.lerp(side, b.side, t));
77+
return CircleBorder(
78+
side: BorderSide.lerp(side, b.side, t),
79+
eccentricity: clampDouble(ui.lerpDouble(eccentricity, b.eccentricity, t)!, 0.0, 1.0),
80+
);
5981
}
6082
return super.lerpTo(b, t);
6183
}
6284

6385
@override
6486
Path getInnerPath(Rect rect, { TextDirection? textDirection }) {
65-
final double radius = rect.shortestSide / 2.0;
66-
final double adjustedRadius;
87+
final double delta;
6788
switch (side.strokeAlign) {
6889
case StrokeAlign.inside:
69-
adjustedRadius = radius - side.width;
90+
delta = side.width;
7091
break;
7192
case StrokeAlign.center:
72-
adjustedRadius = radius - side.width / 2.0;
93+
delta = side.width / 2.0;
7394
break;
7495
case StrokeAlign.outside:
75-
adjustedRadius = radius;
96+
delta = 0;
7697
break;
7798
}
78-
return Path()
79-
..addOval(Rect.fromCircle(
80-
center: rect.center,
81-
radius: math.max(0.0, adjustedRadius),
82-
));
99+
final Rect adjustedRect = _adjustRect(rect).deflate(delta);
100+
return Path()..addOval(adjustedRect);
83101
}
84102

85103
@override
86104
Path getOuterPath(Rect rect, { TextDirection? textDirection }) {
87-
return Path()
88-
..addOval(Rect.fromCircle(
89-
center: rect.center,
90-
radius: rect.shortestSide / 2.0,
91-
));
105+
return Path()..addOval(_adjustRect(rect));
92106
}
93107

94108
@override
95-
CircleBorder copyWith({ BorderSide? side }) {
96-
return CircleBorder(side: side ?? this.side);
109+
CircleBorder copyWith({ BorderSide? side, double? eccentricity }) {
110+
return CircleBorder(side: side ?? this.side, eccentricity: eccentricity ?? this.eccentricity);
97111
}
98112

99113
@override
@@ -102,19 +116,59 @@ class CircleBorder extends OutlinedBorder {
102116
case BorderStyle.none:
103117
break;
104118
case BorderStyle.solid:
105-
final double radius;
106-
switch (side.strokeAlign) {
107-
case StrokeAlign.inside:
108-
radius = (rect.shortestSide - side.width) / 2.0;
109-
break;
110-
case StrokeAlign.center:
111-
radius = rect.shortestSide / 2.0;
112-
break;
113-
case StrokeAlign.outside:
114-
radius = (rect.shortestSide + side.width) / 2.0;
115-
break;
119+
if (eccentricity != 0.0) {
120+
final Rect borderRect = _adjustRect(rect);
121+
final Rect adjustedRect;
122+
switch (side.strokeAlign) {
123+
case StrokeAlign.inside:
124+
adjustedRect = borderRect.deflate(side.width / 2.0);
125+
break;
126+
case StrokeAlign.center:
127+
adjustedRect = borderRect;
128+
break;
129+
case StrokeAlign.outside:
130+
adjustedRect = borderRect.inflate(side.width / 2.0);
131+
break;
132+
}
133+
canvas.drawOval(adjustedRect, side.toPaint());
134+
} else {
135+
final double radius;
136+
switch (side.strokeAlign) {
137+
case StrokeAlign.inside:
138+
radius = (rect.shortestSide - side.width) / 2.0;
139+
break;
140+
case StrokeAlign.center:
141+
radius = rect.shortestSide / 2.0;
142+
break;
143+
case StrokeAlign.outside:
144+
radius = (rect.shortestSide + side.width) / 2.0;
145+
break;
146+
}
147+
canvas.drawCircle(rect.center, radius, side.toPaint());
116148
}
117-
canvas.drawCircle(rect.center, radius, side.toPaint());
149+
}
150+
}
151+
152+
Rect _adjustRect(Rect rect) {
153+
if (eccentricity == 0.0 || rect.width == rect.height) {
154+
return Rect.fromCircle(center: rect.center, radius: rect.shortestSide / 2.0);
155+
}
156+
if (rect.width < rect.height) {
157+
final double delta = (1.0 - eccentricity) * (rect.height - rect.width) / 2.0;
158+
return Rect.fromLTRB(
159+
rect.left,
160+
rect.top + delta,
161+
rect.right,
162+
rect.bottom - delta,
163+
);
164+
} else {
165+
final double delta = (1.0 - eccentricity) * (rect.width - rect.height) / 2.0;
166+
return Rect.fromLTRB(
167+
rect.left + delta,
168+
rect.top,
169+
rect.right - delta,
170+
rect.bottom,
171+
);
118172
}
119173
}
120174

@@ -124,14 +178,18 @@ class CircleBorder extends OutlinedBorder {
124178
return false;
125179
}
126180
return other is CircleBorder
127-
&& other.side == side;
181+
&& other.side == side
182+
&& other.eccentricity == eccentricity;
128183
}
129184

130185
@override
131-
int get hashCode => side.hashCode;
186+
int get hashCode => Object.hash(side, eccentricity);
132187

133188
@override
134189
String toString() {
190+
if (eccentricity != 0.0) {
191+
return '${objectRuntimeType(this, 'CircleBorder')}($side, eccentricity: $eccentricity)';
192+
}
135193
return '${objectRuntimeType(this, 'CircleBorder')}($side)';
136194
}
137195
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:ui' as ui show lerpDouble;
6+
7+
import 'package:flutter/foundation.dart';
8+
9+
import 'borders.dart';
10+
import 'circle_border.dart';
11+
12+
/// A border that fits an elliptical shape.
13+
///
14+
/// Typically used with [ShapeDecoration] to draw an oval. Instead of centering
15+
/// the [Border] to a square, like [CircleBorder], it fills the available space,
16+
/// such that it touches the edges of the box. There is no difference between
17+
/// `CircleBorder(eccentricity = 1.0)` and `OvalBorder()`. [OvalBorder] works as
18+
/// an alias for users to discover this feature.
19+
///
20+
/// See also:
21+
///
22+
/// * [CircleBorder], which draws a circle, centering when the box is rectangular.
23+
/// * [Border], which, when used with [BoxDecoration], can also describe an oval.
24+
class OvalBorder extends CircleBorder {
25+
/// Create an oval border.
26+
const OvalBorder({ super.side, super.eccentricity = 1.0 });
27+
28+
@override
29+
ShapeBorder scale(double t) => OvalBorder(side: side.scale(t), eccentricity: eccentricity);
30+
31+
@override
32+
OvalBorder copyWith({ BorderSide? side, double? eccentricity }) {
33+
return OvalBorder(side: side ?? this.side, eccentricity: eccentricity ?? this.eccentricity);
34+
}
35+
36+
@override
37+
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
38+
if (a is OvalBorder) {
39+
return OvalBorder(
40+
side: BorderSide.lerp(a.side, side, t),
41+
eccentricity: clampDouble(ui.lerpDouble(a.eccentricity, eccentricity, t)!, 0.0, 1.0),
42+
);
43+
}
44+
return super.lerpFrom(a, t);
45+
}
46+
47+
@override
48+
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
49+
if (b is OvalBorder) {
50+
return OvalBorder(
51+
side: BorderSide.lerp(side, b.side, t),
52+
eccentricity: clampDouble(ui.lerpDouble(eccentricity, b.eccentricity, t)!, 0.0, 1.0),
53+
);
54+
}
55+
return super.lerpTo(b, t);
56+
}
57+
58+
@override
59+
String toString() {
60+
if (eccentricity != 1.0) {
61+
return '${objectRuntimeType(this, 'OvalBorder')}($side, eccentricity: $eccentricity)';
62+
}
63+
return '${objectRuntimeType(this, 'OvalBorder')}($side)';
64+
}
65+
}

0 commit comments

Comments
 (0)