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:
Tong Mu 2025-04-07 17:05:23 -07:00 committed by GitHub
parent 242f413f6e
commit 5e42c80fb4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 515 additions and 67 deletions

View File

@ -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);
}
}

View File

@ -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:
///

View File

@ -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

View File

@ -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)';
}
}

View File

@ -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,
);
});

View File

@ -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);
});
}