Add RoundedSuperellipseBorder
and apply it to CupertinoActionSheet
(#166303)
This PR creates a new class `RoundedSuperellipseBorder`, which is the main way to draw a rounded superellipse with filling and/or stroking. The new class is very similar to `RoundedRectangleBorder` and shares a lot of private code, therefore they reside in the same file. For demonstration purposes, the rounded superellipse is also applied to `CupertinoActionSheet`, whose cancel button was drawn with the border class. https://github.com/user-attachments/assets/39599dcf-5cf1-46e1-ab34-8c477cbef9d4 (Sadly this demo wouldn't fit for dartpad because rounded superellipses are not yet supported on Web.) ## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [ ] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [ ] I signed the [CLA]. - [ ] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [ ] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This commit is contained in:
parent
242f413f6e
commit
5e42c80fb4
@ -1299,7 +1299,7 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
|
||||
|
||||
final List<Widget> children = <Widget>[
|
||||
Flexible(
|
||||
child: ClipRRect(
|
||||
child: ClipRSuperellipse(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(12.0)),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(
|
||||
@ -1593,24 +1593,30 @@ class _ActionSheetButtonBackgroundState extends State<_ActionSheetButtonBackgrou
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
late final Color backgroundColor;
|
||||
BorderRadius? borderRadius;
|
||||
late final Widget child;
|
||||
if (!widget.isCancel) {
|
||||
backgroundColor = widget.pressed ? _kActionSheetPressedColor : _kActionSheetBackgroundColor;
|
||||
} else {
|
||||
backgroundColor = widget.pressed ? _kActionSheetCancelPressedColor : _kActionSheetCancelColor;
|
||||
borderRadius = const BorderRadius.all(Radius.circular(_kCornerRadius));
|
||||
}
|
||||
return MetaData(
|
||||
metaData: this,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoDynamicColor.resolve(backgroundColor, context),
|
||||
borderRadius: borderRadius,
|
||||
child = ColoredBox(
|
||||
color: CupertinoDynamicColor.resolve(
|
||||
widget.pressed ? _kActionSheetPressedColor : _kActionSheetBackgroundColor,
|
||||
context,
|
||||
),
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
);
|
||||
} else {
|
||||
child = DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
shape: const RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(_kCornerRadius)),
|
||||
),
|
||||
color: CupertinoDynamicColor.resolve(
|
||||
widget.pressed ? _kActionSheetCancelPressedColor : _kActionSheetCancelColor,
|
||||
context,
|
||||
),
|
||||
),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
return MetaData(metaData: this, child: child);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,9 +24,11 @@ import 'edge_insets.dart';
|
||||
/// 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.
|
||||
/// An axis-aligned rectangle, optionally with rounded corners.
|
||||
///
|
||||
/// The amount of corner rounding, if any, is determined by the border radius
|
||||
/// specified by classes such as [BoxDecoration] or [Border]. The rectangle's
|
||||
/// edges match those of the box in which it is painted.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
|
@ -28,8 +28,9 @@ import 'image_provider.dart';
|
||||
///
|
||||
/// The box has a [border], a body, and may cast a [boxShadow].
|
||||
///
|
||||
/// The [shape] of the box can be a circle or a rectangle. If it is a rectangle,
|
||||
/// then the [borderRadius] property controls the roundness of the corners.
|
||||
/// The [shape] of the box can be [BoxShape.circle] or [BoxShape.rectangle]. If
|
||||
/// it is [BoxShape.rectangle], then the [borderRadius] property can be used to
|
||||
/// make it a rounded rectangle ([RRect]).
|
||||
///
|
||||
/// The body of the box is painted in layers. The bottom-most layer is the
|
||||
/// [color], which fills the box. Above that is the [gradient], which also fills
|
||||
|
@ -16,6 +16,11 @@ import 'border_radius.dart';
|
||||
import 'borders.dart';
|
||||
import 'circle_border.dart';
|
||||
|
||||
// A common interface for [RoundedRectangleBorder] and [RoundedSuperellipseBorder].
|
||||
mixin _RRectLikeBorder on OutlinedBorder {
|
||||
BorderRadiusGeometry get borderRadius;
|
||||
}
|
||||
|
||||
/// A rectangular border with rounded corners.
|
||||
///
|
||||
/// Typically used with [ShapeDecoration] to draw a box with a rounded
|
||||
@ -28,11 +33,14 @@ import 'circle_border.dart';
|
||||
/// * [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 OutlinedBorder {
|
||||
/// * [RoundedSuperellipseBorder], which uses a smoother shape similar to the one
|
||||
/// used in iOS design.
|
||||
class RoundedRectangleBorder extends OutlinedBorder with _RRectLikeBorder {
|
||||
/// Creates a rounded rectangle border.
|
||||
const RoundedRectangleBorder({super.side, this.borderRadius = BorderRadius.zero});
|
||||
|
||||
/// The radii for each corner.
|
||||
@override
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
|
||||
@override
|
||||
@ -149,21 +157,244 @@ class RoundedRectangleBorder extends OutlinedBorder {
|
||||
}
|
||||
}
|
||||
|
||||
class _RoundedRectangleToCircleBorder extends OutlinedBorder {
|
||||
class _RoundedRectangleToCircleBorder extends _ShapeToCircleBorder<RoundedRectangleBorder> {
|
||||
const _RoundedRectangleToCircleBorder({
|
||||
super.side,
|
||||
super.borderRadius = BorderRadius.zero,
|
||||
required super.circularity,
|
||||
required super.eccentricity,
|
||||
});
|
||||
|
||||
@override
|
||||
void drawShape(Canvas canvas, Rect rect, BorderRadius radius, Paint paint, [double? inflation]) {
|
||||
RRect rrect = radius.toRRect(rect);
|
||||
if (inflation != null) {
|
||||
rrect = rrect.inflate(inflation);
|
||||
}
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
Path buildPath(Rect rect, BorderRadius radius, [double? inflation]) {
|
||||
RRect rrect = radius.toRRect(rect);
|
||||
if (inflation != null) {
|
||||
rrect = rrect.inflate(inflation);
|
||||
}
|
||||
return Path()..addRRect(rrect);
|
||||
}
|
||||
|
||||
@override
|
||||
_RoundedRectangleToCircleBorder copyWith({
|
||||
BorderSide? side,
|
||||
BorderRadiusGeometry? borderRadius,
|
||||
double? circularity,
|
||||
double? eccentricity,
|
||||
}) {
|
||||
return _RoundedRectangleToCircleBorder(
|
||||
side: side ?? this.side,
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
circularity: circularity ?? this.circularity,
|
||||
eccentricity: eccentricity ?? this.eccentricity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A rectangular border with rounded corners following the shape of an
|
||||
/// [RSuperellipse].
|
||||
///
|
||||
/// Typically used with [ShapeDecoration] to draw a box that mimics the rounded
|
||||
/// rectangle style commonly seen in iOS design.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [RSuperellipse], which defines the shape.
|
||||
/// * [RoundedRectangleBorder], which uses the traditional [RRect] shape.
|
||||
class RoundedSuperellipseBorder extends OutlinedBorder with _RRectLikeBorder {
|
||||
/// Creates a rounded rectangle border.
|
||||
const RoundedSuperellipseBorder({super.side, this.borderRadius = BorderRadius.zero});
|
||||
|
||||
/// The radii for each corner.
|
||||
@override
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
|
||||
@override
|
||||
ShapeBorder scale(double t) {
|
||||
return RoundedSuperellipseBorder(side: side.scale(t), borderRadius: borderRadius * t);
|
||||
}
|
||||
|
||||
@override
|
||||
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
|
||||
if (a is RoundedSuperellipseBorder) {
|
||||
return RoundedSuperellipseBorder(
|
||||
side: BorderSide.lerp(a.side, side, t),
|
||||
borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
|
||||
);
|
||||
}
|
||||
if (a is CircleBorder) {
|
||||
return _RoundedSuperellipseToCircleBorder(
|
||||
side: BorderSide.lerp(a.side, side, t),
|
||||
borderRadius: borderRadius,
|
||||
circularity: 1.0 - t,
|
||||
eccentricity: a.eccentricity,
|
||||
);
|
||||
}
|
||||
return super.lerpFrom(a, t);
|
||||
}
|
||||
|
||||
@override
|
||||
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
|
||||
if (b is RoundedSuperellipseBorder) {
|
||||
return RoundedSuperellipseBorder(
|
||||
side: BorderSide.lerp(side, b.side, t),
|
||||
borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
|
||||
);
|
||||
}
|
||||
if (b is CircleBorder) {
|
||||
return _RoundedSuperellipseToCircleBorder(
|
||||
side: BorderSide.lerp(side, b.side, t),
|
||||
borderRadius: borderRadius,
|
||||
circularity: t,
|
||||
eccentricity: b.eccentricity,
|
||||
);
|
||||
}
|
||||
return super.lerpTo(b, t);
|
||||
}
|
||||
|
||||
/// Returns a copy of this RoundedSuperellipseBorder with the given fields
|
||||
/// replaced with the new values.
|
||||
@override
|
||||
RoundedSuperellipseBorder copyWith({BorderSide? side, BorderRadiusGeometry? borderRadius}) {
|
||||
return RoundedSuperellipseBorder(
|
||||
side: side ?? this.side,
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
|
||||
final RSuperellipse borderRect = borderRadius.resolve(textDirection).toRSuperellipse(rect);
|
||||
final RSuperellipse adjustedRect = borderRect.deflate(side.strokeInset);
|
||||
return Path()..addRSuperellipse(adjustedRect);
|
||||
}
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
|
||||
return Path()..addRSuperellipse(borderRadius.resolve(textDirection).toRSuperellipse(rect));
|
||||
}
|
||||
|
||||
@override
|
||||
void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
|
||||
if (borderRadius == BorderRadius.zero) {
|
||||
canvas.drawRect(rect, paint);
|
||||
} else {
|
||||
canvas.drawRSuperellipse(borderRadius.resolve(textDirection).toRSuperellipse(rect), paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get preferPaintInterior => true;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
|
||||
switch (side.style) {
|
||||
case BorderStyle.none:
|
||||
break;
|
||||
case BorderStyle.solid:
|
||||
if (side.width == 0.0) {
|
||||
canvas.drawRSuperellipse(
|
||||
borderRadius.resolve(textDirection).toRSuperellipse(rect),
|
||||
side.toPaint(),
|
||||
);
|
||||
} else {
|
||||
final double strokeOffset = (side.strokeOutset - side.strokeInset) / 2;
|
||||
final RSuperellipse base = borderRadius
|
||||
.resolve(textDirection)
|
||||
.toRSuperellipse(rect)
|
||||
.inflate(strokeOffset);
|
||||
canvas.drawRSuperellipse(base, side.toPaint());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is RoundedSuperellipseBorder &&
|
||||
other.side == side &&
|
||||
other.borderRadius == borderRadius;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(side, borderRadius);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${objectRuntimeType(this, 'RoundedSuperellipseBorder')}($side, $borderRadius)';
|
||||
}
|
||||
}
|
||||
|
||||
class _RoundedSuperellipseToCircleBorder extends _ShapeToCircleBorder<RoundedSuperellipseBorder> {
|
||||
const _RoundedSuperellipseToCircleBorder({
|
||||
super.side,
|
||||
super.borderRadius = BorderRadius.zero,
|
||||
required super.circularity,
|
||||
required super.eccentricity,
|
||||
});
|
||||
|
||||
@override
|
||||
void drawShape(Canvas canvas, Rect rect, BorderRadius radius, Paint paint, [double? inflation]) {
|
||||
RSuperellipse rsuperellipse = radius.toRSuperellipse(rect);
|
||||
if (inflation != null) {
|
||||
rsuperellipse = rsuperellipse.inflate(inflation);
|
||||
}
|
||||
canvas.drawRSuperellipse(rsuperellipse, paint);
|
||||
}
|
||||
|
||||
@override
|
||||
Path buildPath(Rect rect, BorderRadius radius, [double? inflation]) {
|
||||
RSuperellipse rsuperellipse = radius.toRSuperellipse(rect);
|
||||
if (inflation != null) {
|
||||
rsuperellipse = rsuperellipse.inflate(inflation);
|
||||
}
|
||||
return Path()..addRSuperellipse(rsuperellipse);
|
||||
}
|
||||
|
||||
@override
|
||||
_RoundedSuperellipseToCircleBorder copyWith({
|
||||
BorderSide? side,
|
||||
BorderRadiusGeometry? borderRadius,
|
||||
double? circularity,
|
||||
double? eccentricity,
|
||||
}) {
|
||||
return _RoundedSuperellipseToCircleBorder(
|
||||
side: side ?? this.side,
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
circularity: circularity ?? this.circularity,
|
||||
eccentricity: eccentricity ?? this.eccentricity,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _ShapeToCircleBorder<T extends _RRectLikeBorder> extends OutlinedBorder {
|
||||
const _ShapeToCircleBorder({
|
||||
super.side,
|
||||
this.borderRadius = BorderRadius.zero,
|
||||
required this.circularity,
|
||||
required this.eccentricity,
|
||||
});
|
||||
|
||||
void drawShape(Canvas canvas, Rect rect, BorderRadius radius, Paint paint, [double? inflation]);
|
||||
Path buildPath(Rect rect, BorderRadius radius, [double? inflation]);
|
||||
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
final double circularity;
|
||||
final double eccentricity;
|
||||
|
||||
@override
|
||||
ShapeBorder scale(double t) {
|
||||
return _RoundedRectangleToCircleBorder(
|
||||
return copyWith(
|
||||
side: side.scale(t),
|
||||
borderRadius: borderRadius * t,
|
||||
circularity: t,
|
||||
@ -173,27 +404,27 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder {
|
||||
|
||||
@override
|
||||
ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
|
||||
if (a is RoundedRectangleBorder) {
|
||||
return _RoundedRectangleToCircleBorder(
|
||||
if (a is T) {
|
||||
return copyWith(
|
||||
side: BorderSide.lerp(a.side, side, t),
|
||||
borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
|
||||
borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t),
|
||||
circularity: circularity * t,
|
||||
eccentricity: eccentricity,
|
||||
);
|
||||
}
|
||||
if (a is CircleBorder) {
|
||||
return _RoundedRectangleToCircleBorder(
|
||||
return copyWith(
|
||||
side: BorderSide.lerp(a.side, side, t),
|
||||
borderRadius: borderRadius,
|
||||
circularity: circularity + (1.0 - circularity) * (1.0 - t),
|
||||
eccentricity: a.eccentricity,
|
||||
);
|
||||
}
|
||||
if (a is _RoundedRectangleToCircleBorder) {
|
||||
return _RoundedRectangleToCircleBorder(
|
||||
if (a is _ShapeToCircleBorder<T>) {
|
||||
return copyWith(
|
||||
side: BorderSide.lerp(a.side, side, t),
|
||||
borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t)!,
|
||||
circularity: ui.lerpDouble(a.circularity, circularity, t)!,
|
||||
borderRadius: BorderRadiusGeometry.lerp(a.borderRadius, borderRadius, t),
|
||||
circularity: ui.lerpDouble(a.circularity, circularity, t),
|
||||
eccentricity: eccentricity,
|
||||
);
|
||||
}
|
||||
@ -202,27 +433,27 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder {
|
||||
|
||||
@override
|
||||
ShapeBorder? lerpTo(ShapeBorder? b, double t) {
|
||||
if (b is RoundedRectangleBorder) {
|
||||
return _RoundedRectangleToCircleBorder(
|
||||
if (b is T) {
|
||||
return copyWith(
|
||||
side: BorderSide.lerp(side, b.side, t),
|
||||
borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
|
||||
borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t),
|
||||
circularity: circularity * (1.0 - t),
|
||||
eccentricity: eccentricity,
|
||||
);
|
||||
}
|
||||
if (b is CircleBorder) {
|
||||
return _RoundedRectangleToCircleBorder(
|
||||
return copyWith(
|
||||
side: BorderSide.lerp(side, b.side, t),
|
||||
borderRadius: borderRadius,
|
||||
circularity: circularity + (1.0 - circularity) * t,
|
||||
eccentricity: b.eccentricity,
|
||||
);
|
||||
}
|
||||
if (b is _RoundedRectangleToCircleBorder) {
|
||||
return _RoundedRectangleToCircleBorder(
|
||||
if (b is _ShapeToCircleBorder<T>) {
|
||||
return copyWith(
|
||||
side: BorderSide.lerp(side, b.side, t),
|
||||
borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t)!,
|
||||
circularity: ui.lerpDouble(circularity, b.circularity, t)!,
|
||||
borderRadius: BorderRadiusGeometry.lerp(borderRadius, b.borderRadius, t),
|
||||
circularity: ui.lerpDouble(circularity, b.circularity, t),
|
||||
eccentricity: eccentricity,
|
||||
);
|
||||
}
|
||||
@ -244,7 +475,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder {
|
||||
}
|
||||
}
|
||||
|
||||
BorderRadius? _adjustBorderRadius(Rect rect, TextDirection? textDirection) {
|
||||
BorderRadius _adjustBorderRadius(Rect rect, TextDirection? textDirection) {
|
||||
final BorderRadius resolvedRadius = borderRadius.resolve(textDirection);
|
||||
if (circularity == 0.0) {
|
||||
return resolvedRadius;
|
||||
@ -272,48 +503,42 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder {
|
||||
resolvedRadius,
|
||||
BorderRadius.circular(rect.shortestSide / 2),
|
||||
circularity,
|
||||
);
|
||||
)!;
|
||||
}
|
||||
|
||||
@override
|
||||
Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
|
||||
final RRect borderRect = _adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect));
|
||||
final RRect adjustedRect = borderRect.deflate(ui.lerpDouble(side.width, 0, side.strokeAlign)!);
|
||||
return Path()..addRRect(adjustedRect);
|
||||
return buildPath(
|
||||
_adjustRect(rect),
|
||||
_adjustBorderRadius(rect, textDirection),
|
||||
-ui.lerpDouble(side.width, 0, side.strokeAlign)!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
|
||||
return Path()..addRRect(_adjustBorderRadius(rect, textDirection)!.toRRect(_adjustRect(rect)));
|
||||
return buildPath(_adjustRect(rect), _adjustBorderRadius(rect, textDirection));
|
||||
}
|
||||
|
||||
@override
|
||||
void paintInterior(Canvas canvas, Rect rect, Paint paint, {TextDirection? textDirection}) {
|
||||
final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection)!;
|
||||
final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection);
|
||||
if (adjustedBorderRadius == BorderRadius.zero) {
|
||||
canvas.drawRect(_adjustRect(rect), paint);
|
||||
} else {
|
||||
canvas.drawRRect(adjustedBorderRadius.toRRect(_adjustRect(rect)), paint);
|
||||
drawShape(canvas, _adjustRect(rect), adjustedBorderRadius, paint);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool get preferPaintInterior => true;
|
||||
|
||||
@override
|
||||
_RoundedRectangleToCircleBorder copyWith({
|
||||
_ShapeToCircleBorder<T> copyWith({
|
||||
BorderSide? side,
|
||||
BorderRadiusGeometry? borderRadius,
|
||||
double? circularity,
|
||||
double? eccentricity,
|
||||
}) {
|
||||
return _RoundedRectangleToCircleBorder(
|
||||
side: side ?? this.side,
|
||||
borderRadius: borderRadius ?? this.borderRadius,
|
||||
circularity: circularity ?? this.circularity,
|
||||
eccentricity: eccentricity ?? this.eccentricity,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
|
||||
@ -321,9 +546,13 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder {
|
||||
case BorderStyle.none:
|
||||
break;
|
||||
case BorderStyle.solid:
|
||||
final BorderRadius adjustedBorderRadius = _adjustBorderRadius(rect, textDirection)!;
|
||||
final RRect borderRect = adjustedBorderRadius.toRRect(_adjustRect(rect));
|
||||
canvas.drawRRect(borderRect.inflate(side.strokeOffset / 2), side.toPaint());
|
||||
drawShape(
|
||||
canvas,
|
||||
_adjustRect(rect),
|
||||
_adjustBorderRadius(rect, textDirection),
|
||||
side.toPaint(),
|
||||
side.strokeOffset / 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -332,7 +561,7 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder {
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is _RoundedRectangleToCircleBorder &&
|
||||
return other is _ShapeToCircleBorder<T> &&
|
||||
other.side == side &&
|
||||
other.borderRadius == borderRadius &&
|
||||
other.circularity == circularity;
|
||||
@ -344,8 +573,8 @@ class _RoundedRectangleToCircleBorder extends OutlinedBorder {
|
||||
@override
|
||||
String toString() {
|
||||
if (eccentricity != 0.0) {
|
||||
return 'RoundedRectangleBorder($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)';
|
||||
return '$T($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder that is ${(eccentricity * 100).toStringAsFixed(1)}% oval)';
|
||||
}
|
||||
return 'RoundedRectangleBorder($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)';
|
||||
return '$T($side, $borderRadius, ${(circularity * 100).toStringAsFixed(1)}% of the way to being a CircleBorder)';
|
||||
}
|
||||
}
|
||||
|
@ -380,19 +380,19 @@ void main() {
|
||||
// Content section should be at the bottom left of action sheet
|
||||
// (minus padding).
|
||||
expect(
|
||||
tester.getBottomLeft(find.byType(ClipRRect)),
|
||||
tester.getBottomLeft(find.byType(ClipRSuperellipse)),
|
||||
tester.getBottomLeft(find.byType(CupertinoActionSheet)) - const Offset(-8.0, 8.0),
|
||||
);
|
||||
|
||||
// Check that the dialog size is the same as the content section size
|
||||
// (minus padding).
|
||||
expect(
|
||||
tester.getSize(find.byType(ClipRRect)).height,
|
||||
tester.getSize(find.byType(ClipRSuperellipse)).height,
|
||||
tester.getSize(find.byType(CupertinoActionSheet)).height - 16.0,
|
||||
);
|
||||
|
||||
expect(
|
||||
tester.getSize(find.byType(ClipRRect)).width,
|
||||
tester.getSize(find.byType(ClipRSuperellipse)).width,
|
||||
tester.getSize(find.byType(CupertinoActionSheet)).width - 16.0,
|
||||
);
|
||||
});
|
||||
|
@ -0,0 +1,210 @@
|
||||
// Copyright 2014 The Flutter 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 'common_matchers.dart';
|
||||
|
||||
void main() {
|
||||
test('RoundedSuperellipseBorder defaults', () {
|
||||
const RoundedSuperellipseBorder border = RoundedSuperellipseBorder();
|
||||
expect(border.side, BorderSide.none);
|
||||
expect(border.borderRadius, BorderRadius.zero);
|
||||
});
|
||||
|
||||
test('RoundedSuperellipseBorder copyWith, ==, hashCode', () {
|
||||
expect(const RoundedSuperellipseBorder(), const RoundedSuperellipseBorder().copyWith());
|
||||
expect(
|
||||
const RoundedSuperellipseBorder().hashCode,
|
||||
const RoundedSuperellipseBorder().copyWith().hashCode,
|
||||
);
|
||||
const BorderSide side = BorderSide(width: 10.0, color: Color(0xff123456));
|
||||
const BorderRadius radius = BorderRadius.all(Radius.circular(16.0));
|
||||
const BorderRadiusDirectional directionalRadius = BorderRadiusDirectional.all(
|
||||
Radius.circular(16.0),
|
||||
);
|
||||
|
||||
expect(
|
||||
const RoundedSuperellipseBorder().copyWith(side: side, borderRadius: radius),
|
||||
const RoundedSuperellipseBorder(side: side, borderRadius: radius),
|
||||
);
|
||||
|
||||
expect(
|
||||
const RoundedSuperellipseBorder().copyWith(side: side, borderRadius: directionalRadius),
|
||||
const RoundedSuperellipseBorder(side: side, borderRadius: directionalRadius),
|
||||
);
|
||||
});
|
||||
|
||||
test('RoundedSuperellipseBorder', () {
|
||||
const RoundedSuperellipseBorder c10 = RoundedSuperellipseBorder(
|
||||
side: BorderSide(width: 10.0),
|
||||
borderRadius: BorderRadius.all(Radius.circular(100.0)),
|
||||
);
|
||||
const RoundedSuperellipseBorder c15 = RoundedSuperellipseBorder(
|
||||
side: BorderSide(width: 15.0),
|
||||
borderRadius: BorderRadius.all(Radius.circular(150.0)),
|
||||
);
|
||||
const RoundedSuperellipseBorder c20 = RoundedSuperellipseBorder(
|
||||
side: BorderSide(width: 20.0),
|
||||
borderRadius: BorderRadius.all(Radius.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);
|
||||
|
||||
const RoundedSuperellipseBorder c1 = RoundedSuperellipseBorder(
|
||||
side: BorderSide(),
|
||||
borderRadius: BorderRadius.all(Radius.circular(1.0)),
|
||||
);
|
||||
const RoundedSuperellipseBorder c2 = RoundedSuperellipseBorder(
|
||||
side: BorderSide(),
|
||||
borderRadius: BorderRadius.all(Radius.circular(2.0)),
|
||||
);
|
||||
expect(c2.getInnerPath(Rect.fromCircle(center: Offset.zero, radius: 2.0)), isUnitCircle);
|
||||
expect(c1.getOuterPath(Rect.fromCircle(center: Offset.zero, radius: 1.0)), isUnitCircle);
|
||||
const Rect rect = Rect.fromLTRB(10.0, 20.0, 80.0, 190.0);
|
||||
expect(
|
||||
(Canvas canvas) => c10.paint(canvas, rect),
|
||||
paints..rsuperellipse(
|
||||
rsuperellipse: RSuperellipse.fromRectAndRadius(
|
||||
rect.deflate(5.0),
|
||||
const Radius.circular(95.0),
|
||||
),
|
||||
strokeWidth: 10.0,
|
||||
),
|
||||
);
|
||||
|
||||
const RoundedSuperellipseBorder directional = RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadiusDirectional.only(topStart: Radius.circular(20)),
|
||||
);
|
||||
expect(ShapeBorder.lerp(directional, c10, 1.0), ShapeBorder.lerp(c10, directional, 0.0));
|
||||
});
|
||||
|
||||
test('RoundedSuperellipseBorder and CircleBorder', () {
|
||||
const RoundedSuperellipseBorder r = RoundedSuperellipseBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10.0)),
|
||||
);
|
||||
const CircleBorder c = CircleBorder();
|
||||
const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 20.0); // center is x=40..60 y=10
|
||||
final Matcher looksLikeR = isPathThat(
|
||||
includes: const <Offset>[Offset(30.0, 10.0), Offset(50.0, 10.0)],
|
||||
excludes: const <Offset>[Offset(1.0, 1.0), Offset(99.0, 19.0)],
|
||||
);
|
||||
final Matcher looksLikeC = isPathThat(
|
||||
includes: const <Offset>[Offset(50.0, 10.0)],
|
||||
excludes: const <Offset>[Offset(1.0, 1.0), Offset(30.0, 10.0), 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(),
|
||||
'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(10.0), 10.0% of the way to being a CircleBorder)',
|
||||
);
|
||||
expect(
|
||||
ShapeBorder.lerp(r, c, 0.2).toString(),
|
||||
'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: 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(),
|
||||
'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(10.0), 82.0% of the way to being a CircleBorder)',
|
||||
);
|
||||
|
||||
expect(
|
||||
ShapeBorder.lerp(c, r, 0.9).toString(),
|
||||
'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: none), BorderRadius.circular(10.0), 10.0% of the way to being a CircleBorder)',
|
||||
);
|
||||
expect(
|
||||
ShapeBorder.lerp(c, r, 0.8).toString(),
|
||||
'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: 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(),
|
||||
'RoundedSuperellipseBorder(BorderSide(width: 0.0, style: 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());
|
||||
});
|
||||
|
||||
test('RoundedSuperellipseBorder.dimensions and CircleBorder.dimensions', () {
|
||||
const RoundedSuperellipseBorder insideRoundedSuperellipseBorder = RoundedSuperellipseBorder(
|
||||
side: BorderSide(width: 10),
|
||||
);
|
||||
expect(insideRoundedSuperellipseBorder.dimensions, const EdgeInsets.all(10));
|
||||
|
||||
const RoundedSuperellipseBorder centerRoundedSuperellipseBorder = RoundedSuperellipseBorder(
|
||||
side: BorderSide(width: 10, strokeAlign: BorderSide.strokeAlignCenter),
|
||||
);
|
||||
expect(centerRoundedSuperellipseBorder.dimensions, const EdgeInsets.all(5));
|
||||
|
||||
const RoundedSuperellipseBorder outsideRoundedSuperellipseBorder = RoundedSuperellipseBorder(
|
||||
side: BorderSide(width: 10, strokeAlign: BorderSide.strokeAlignOutside),
|
||||
);
|
||||
expect(outsideRoundedSuperellipseBorder.dimensions, EdgeInsets.zero);
|
||||
|
||||
const CircleBorder insideCircleBorder = CircleBorder(side: BorderSide(width: 10));
|
||||
expect(insideCircleBorder.dimensions, const EdgeInsets.all(10));
|
||||
|
||||
const CircleBorder centerCircleBorder = CircleBorder(
|
||||
side: BorderSide(width: 10, strokeAlign: BorderSide.strokeAlignCenter),
|
||||
);
|
||||
expect(centerCircleBorder.dimensions, const EdgeInsets.all(5));
|
||||
|
||||
const CircleBorder outsideCircleBorder = CircleBorder(
|
||||
side: BorderSide(width: 10, strokeAlign: BorderSide.strokeAlignOutside),
|
||||
);
|
||||
expect(outsideCircleBorder.dimensions, EdgeInsets.zero);
|
||||
});
|
||||
|
||||
test('RoundedSuperellipseBorder.lerp with different StrokeAlign', () {
|
||||
const RoundedSuperellipseBorder rInside = RoundedSuperellipseBorder(
|
||||
side: BorderSide(width: 10.0),
|
||||
);
|
||||
const RoundedSuperellipseBorder rOutside = RoundedSuperellipseBorder(
|
||||
side: BorderSide(width: 20.0, strokeAlign: BorderSide.strokeAlignOutside),
|
||||
);
|
||||
const RoundedSuperellipseBorder rCenter = RoundedSuperellipseBorder(
|
||||
side: BorderSide(width: 15.0, strokeAlign: BorderSide.strokeAlignCenter),
|
||||
);
|
||||
expect(ShapeBorder.lerp(rInside, rOutside, 0.5), rCenter);
|
||||
});
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user