Add clipRSuperellipse
, and use them for dialogs (#161111)
This PR makes Flutter support `clipRSuperellipse`, and apply it to `CupertinoAlertDialog`. Hit tests related to RSuperellipse are performed based on its bounding box, since the computation is too complicated and pixel perfect hit test is not needed practically. Native: <img src="https://github.com/user-attachments/assets/8f9b472a-e624-4eef-9cea-e81b80f32b86" width="400"/> Native vs before: (The two screenshots are stacked and offset by (1, 1) pixels. See the bottom right corner for comparison.) <img src="https://github.com/user-attachments/assets/ffaf62fc-a82f-4c7a-9ff1-52374f4f2a67" width="400"/> Native vs after: <img src="https://github.com/user-attachments/assets/3dfde2b0-bcc6-492a-8d97-ecabdf97f6f0" width="400"/> After only: <img src="https://github.com/user-attachments/assets/32b2a665-a0da-498f-acdb-598553940964" width="400"/> ## 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
c35fd09792
commit
0357b45337
@ -570,7 +570,7 @@ class CupertinoPopupSurface extends StatelessWidget {
|
||||
static const double defaultBlurSigma = 30.0;
|
||||
|
||||
/// The default corner radius of a [CupertinoPopupSurface].
|
||||
static const BorderRadius _clipper = BorderRadius.all(Radius.circular(14));
|
||||
static const BorderRadius _clipper = BorderRadius.all(Radius.circular(12));
|
||||
|
||||
// The [ColorFilter] matrix used to saturate widgets underlying a
|
||||
// [CupertinoPopupSurface] when the ambient [CupertinoThemeData.brightness] is
|
||||
@ -719,13 +719,13 @@ class CupertinoPopupSurface extends StatelessWidget {
|
||||
}
|
||||
|
||||
if (filter != null) {
|
||||
return ClipRRect(
|
||||
return ClipRSuperellipse(
|
||||
borderRadius: _clipper,
|
||||
child: BackdropFilter(filter: filter, child: contents),
|
||||
);
|
||||
}
|
||||
|
||||
return ClipRRect(borderRadius: _clipper, child: contents);
|
||||
return ClipRSuperellipse(borderRadius: _clipper, child: contents);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,7 @@ export 'dart:ui'
|
||||
PathOperation,
|
||||
RRect,
|
||||
RSTransform,
|
||||
RSuperellipse,
|
||||
Radius,
|
||||
Rect,
|
||||
Shader,
|
||||
|
@ -383,6 +383,23 @@ class BorderRadius extends BorderRadiusGeometry {
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates an [RSuperellipse] from the current border radius and a [Rect].
|
||||
///
|
||||
/// If any of the radii have negative values in x or y, those values will be
|
||||
/// clamped to zero in order to produce a valid [RRect].
|
||||
RSuperellipse toRSuperellipse(Rect rect) {
|
||||
// Because the current radii could be negative, we must clamp them before
|
||||
// converting them to an RRect to be rendered, since negative radii on
|
||||
// RRects don't make sense.
|
||||
return RSuperellipse.fromRectAndCorners(
|
||||
rect,
|
||||
topLeft: topLeft.clamp(minimum: Radius.zero),
|
||||
topRight: topRight.clamp(minimum: Radius.zero),
|
||||
bottomLeft: bottomLeft.clamp(minimum: Radius.zero),
|
||||
bottomRight: bottomRight.clamp(minimum: Radius.zero),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
BorderRadiusGeometry subtract(BorderRadiusGeometry other) {
|
||||
if (other is BorderRadius) {
|
||||
|
@ -5,7 +5,7 @@
|
||||
/// @docImport 'package:flutter/rendering.dart';
|
||||
library;
|
||||
|
||||
import 'dart:ui' show Canvas, Clip, Paint, Path, RRect, Rect, VoidCallback;
|
||||
import 'dart:ui' show Canvas, Clip, Paint, Path, RRect, RSuperellipse, Rect, VoidCallback;
|
||||
|
||||
/// Clip utilities used by [PaintingContext].
|
||||
abstract class ClipContext {
|
||||
@ -63,6 +63,25 @@ abstract class ClipContext {
|
||||
);
|
||||
}
|
||||
|
||||
/// Clip [canvas] with [Path] according to the given rounded superellipse and
|
||||
/// then paint. [canvas] is restored to the pre-clip status afterwards.
|
||||
///
|
||||
/// The `bounds` is the saveLayer bounds used for
|
||||
/// [Clip.antiAliasWithSaveLayer].
|
||||
void clipRSuperellipseAndPaint(
|
||||
RSuperellipse rse,
|
||||
Clip clipBehavior,
|
||||
Rect bounds,
|
||||
VoidCallback painter,
|
||||
) {
|
||||
_clipAndPaint(
|
||||
(bool doAntiAlias) => canvas.clipRSuperellipse(rse, doAntiAlias: doAntiAlias),
|
||||
clipBehavior,
|
||||
bounds,
|
||||
painter,
|
||||
);
|
||||
}
|
||||
|
||||
/// Clip [canvas] with [Path] according to `rect` and then paint. [canvas] is
|
||||
/// restored to the pre-clip status afterwards.
|
||||
///
|
||||
|
@ -1773,6 +1773,96 @@ class ClipRRectLayer extends ContainerLayer {
|
||||
}
|
||||
}
|
||||
|
||||
/// A composite layer that clips its children using a rounded superellipse.
|
||||
///
|
||||
/// When debugging, setting [debugDisableClipLayers] to true will cause this
|
||||
/// layer to be skipped (directly replaced by its children). This can be helpful
|
||||
/// to track down the cause of performance problems.
|
||||
///
|
||||
/// Hit tests are performed based on the bounding box of the rounded
|
||||
/// superellipse.
|
||||
class ClipRSuperellipseLayer extends ContainerLayer {
|
||||
/// Creates a layer with a rounded-rectangular clip.
|
||||
///
|
||||
/// The [clipRSuperellipse] and [clipBehavior] properties must be non-null before the
|
||||
/// compositing phase of the pipeline.
|
||||
ClipRSuperellipseLayer({RSuperellipse? clipRSuperellipse, Clip clipBehavior = Clip.antiAlias})
|
||||
: _clipRSuperellipse = clipRSuperellipse,
|
||||
_clipBehavior = clipBehavior,
|
||||
assert(clipBehavior != Clip.none);
|
||||
|
||||
/// The rounded-rect to clip in the parent's coordinate system.
|
||||
///
|
||||
/// The scene must be explicitly recomposited after this property is changed
|
||||
/// (as described at [Layer]).
|
||||
RSuperellipse? get clipRSuperellipse => _clipRSuperellipse;
|
||||
RSuperellipse? _clipRSuperellipse;
|
||||
set clipRSuperellipse(RSuperellipse? value) {
|
||||
if (value != _clipRSuperellipse) {
|
||||
_clipRSuperellipse = value;
|
||||
markNeedsAddToScene();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Rect? describeClipBounds() => clipRSuperellipse?.outerRect;
|
||||
|
||||
/// {@macro flutter.rendering.ClipRectLayer.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.antiAlias].
|
||||
Clip get clipBehavior => _clipBehavior;
|
||||
Clip _clipBehavior;
|
||||
set clipBehavior(Clip value) {
|
||||
assert(value != Clip.none);
|
||||
if (value != _clipBehavior) {
|
||||
_clipBehavior = value;
|
||||
markNeedsAddToScene();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
bool findAnnotations<S extends Object>(
|
||||
AnnotationResult<S> result,
|
||||
Offset localPosition, {
|
||||
required bool onlyFirst,
|
||||
}) {
|
||||
if (!clipRSuperellipse!.outerRect.contains(localPosition)) {
|
||||
return false;
|
||||
}
|
||||
return super.findAnnotations<S>(result, localPosition, onlyFirst: onlyFirst);
|
||||
}
|
||||
|
||||
@override
|
||||
void addToScene(ui.SceneBuilder builder) {
|
||||
assert(clipRSuperellipse != null);
|
||||
bool enabled = true;
|
||||
assert(() {
|
||||
enabled = !debugDisableClipLayers;
|
||||
return true;
|
||||
}());
|
||||
if (enabled) {
|
||||
engineLayer = builder.pushClipRSuperellipse(
|
||||
clipRSuperellipse!,
|
||||
clipBehavior: clipBehavior,
|
||||
oldLayer: _engineLayer as ui.ClipRSuperellipseEngineLayer?,
|
||||
);
|
||||
} else {
|
||||
engineLayer = null;
|
||||
}
|
||||
addChildrenToScene(builder);
|
||||
if (enabled) {
|
||||
builder.pop();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<RSuperellipse>('clipRSuperellipse', clipRSuperellipse));
|
||||
properties.add(DiagnosticsProperty<Clip>('clipBehavior', clipBehavior));
|
||||
}
|
||||
}
|
||||
|
||||
/// A composite layer that clips its children using a path.
|
||||
///
|
||||
/// When debugging, setting [debugDisableClipLayers] to true will cause this
|
||||
|
@ -626,6 +626,60 @@ class PaintingContext extends ClipContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip further painting using a rounded superellipse.
|
||||
///
|
||||
/// {@macro flutter.rendering.PaintingContext.pushClipRect.needsCompositing}
|
||||
///
|
||||
/// {@macro flutter.rendering.PaintingContext.pushClipRect.offset}
|
||||
///
|
||||
/// The `bounds` argument is used to specify the region of the canvas (in the
|
||||
/// caller's coordinate system) into which `painter` will paint.
|
||||
///
|
||||
/// The `clipRSuperellipse` argument specifies the rounded-superellipse (in the caller's
|
||||
/// coordinate system) to use to clip the painting done by `painter`. It
|
||||
/// should not include the `offset`.
|
||||
///
|
||||
/// The `painter` callback will be called while the `clipRSuperellipse` is applied. It
|
||||
/// is called synchronously during the call to [pushClipRSuperellipse].
|
||||
///
|
||||
/// The `clipBehavior` argument controls how the rounded rectangle is clipped.
|
||||
///
|
||||
/// Hit tests are performed based on the bounding box of the [RSuperellipse].
|
||||
///
|
||||
/// {@macro flutter.rendering.PaintingContext.pushClipRect.oldLayer}
|
||||
ClipRSuperellipseLayer? pushClipRSuperellipse(
|
||||
bool needsCompositing,
|
||||
Offset offset,
|
||||
Rect bounds,
|
||||
RSuperellipse clipRSuperellipse,
|
||||
PaintingContextCallback painter, {
|
||||
Clip clipBehavior = Clip.antiAlias,
|
||||
ClipRSuperellipseLayer? oldLayer,
|
||||
}) {
|
||||
if (clipBehavior == Clip.none) {
|
||||
painter(this, offset);
|
||||
return null;
|
||||
}
|
||||
final Rect offsetBounds = bounds.shift(offset);
|
||||
final RSuperellipse offsetShape = clipRSuperellipse.shift(offset);
|
||||
if (needsCompositing) {
|
||||
final ClipRSuperellipseLayer layer = oldLayer ?? ClipRSuperellipseLayer();
|
||||
layer
|
||||
..clipRSuperellipse = offsetShape
|
||||
..clipBehavior = clipBehavior;
|
||||
pushLayer(layer, painter, offset, childPaintBounds: offsetBounds);
|
||||
return layer;
|
||||
} else {
|
||||
clipRSuperellipseAndPaint(
|
||||
offsetShape,
|
||||
clipBehavior,
|
||||
offsetBounds,
|
||||
() => painter(this, offset),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Clip further painting using a path.
|
||||
///
|
||||
/// {@macro flutter.rendering.PaintingContext.pushClipRect.needsCompositing}
|
||||
|
@ -1714,6 +1714,113 @@ class RenderClipRRect extends _RenderCustomClip<RRect> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clips its child using a rounded superellipse.
|
||||
///
|
||||
/// By default, [RenderClipRSuperellipse] uses its own bounds as the base
|
||||
/// rectangle for the clip, but the size and location of the clip can be
|
||||
/// customized using a custom [clipper].
|
||||
///
|
||||
/// Hit tests are performed based on the bounding box of the RSuperellipse.
|
||||
class RenderClipRSuperellipse extends _RenderCustomClip<RSuperellipse> {
|
||||
/// Creates a rounded-superellipse clip.
|
||||
///
|
||||
/// The [borderRadius] defaults to [BorderRadius.zero], i.e. a rectangle with
|
||||
/// right-angled corners.
|
||||
///
|
||||
/// If [clipBehavior] is [Clip.none], no clipping will be applied.
|
||||
RenderClipRSuperellipse({
|
||||
super.child,
|
||||
BorderRadiusGeometry borderRadius = BorderRadius.zero,
|
||||
super.clipper,
|
||||
super.clipBehavior,
|
||||
TextDirection? textDirection,
|
||||
}) : _borderRadius = borderRadius,
|
||||
_textDirection = textDirection;
|
||||
|
||||
/// The border radius of the rounded corners.
|
||||
///
|
||||
/// Values are clamped so that horizontal and vertical radii sums do not
|
||||
/// exceed width/height.
|
||||
///
|
||||
/// This value is ignored if [clipper] is non-null.
|
||||
BorderRadiusGeometry get borderRadius => _borderRadius;
|
||||
BorderRadiusGeometry _borderRadius;
|
||||
set borderRadius(BorderRadiusGeometry value) {
|
||||
if (_borderRadius == value) {
|
||||
return;
|
||||
}
|
||||
_borderRadius = value;
|
||||
_markNeedsClip();
|
||||
}
|
||||
|
||||
/// The text direction with which to resolve [borderRadius].
|
||||
TextDirection? get textDirection => _textDirection;
|
||||
TextDirection? _textDirection;
|
||||
set textDirection(TextDirection? value) {
|
||||
if (_textDirection == value) {
|
||||
return;
|
||||
}
|
||||
_textDirection = value;
|
||||
_markNeedsClip();
|
||||
}
|
||||
|
||||
@override
|
||||
RSuperellipse get _defaultClip =>
|
||||
_borderRadius.resolve(textDirection).toRSuperellipse(Offset.zero & size);
|
||||
|
||||
@override
|
||||
bool hitTest(BoxHitTestResult result, {required Offset position}) {
|
||||
if (_clipper != null) {
|
||||
_updateClip();
|
||||
assert(_clip != null);
|
||||
if (!_clip!.outerRect.contains(position)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return super.hitTest(result, position: position);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
if (child != null) {
|
||||
if (clipBehavior != Clip.none) {
|
||||
_updateClip();
|
||||
layer = context.pushClipRSuperellipse(
|
||||
needsCompositing,
|
||||
offset,
|
||||
_clip!.outerRect,
|
||||
_clip!,
|
||||
super.paint,
|
||||
clipBehavior: clipBehavior,
|
||||
oldLayer: layer as ClipRSuperellipseLayer?,
|
||||
);
|
||||
} else {
|
||||
context.paintChild(child!, offset);
|
||||
layer = null;
|
||||
}
|
||||
} else {
|
||||
layer = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void debugPaintSize(PaintingContext context, Offset offset) {
|
||||
assert(() {
|
||||
if (child != null) {
|
||||
super.debugPaintSize(context, offset);
|
||||
if (clipBehavior != Clip.none) {
|
||||
context.canvas.drawRSuperellipse(_clip!.shift(offset), _debugPaint!);
|
||||
_debugText!.paint(
|
||||
context.canvas,
|
||||
offset + Offset(_clip!.tlRadiusX, -_debugText!.text!.style!.fontSize! * 1.1),
|
||||
);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
/// Clips its child using an oval.
|
||||
///
|
||||
/// By default, inscribes an axis-aligned oval into its layout dimensions and
|
||||
|
@ -965,6 +965,11 @@ class ClipRect extends SingleChildRenderObjectWidget {
|
||||
///
|
||||
/// * [CustomClipper], for information about creating custom clips.
|
||||
/// * [ClipRect], for more efficient clips without rounded corners.
|
||||
/// * [ClipRSuperellipse], for a similar clipping shape with smoother
|
||||
/// transitions between the straight sides and the rounded corners. This
|
||||
/// shape closely matches the rounded rectangles commonly used in Apple’s
|
||||
/// design language, resembling the `RoundedRectangle` shape in SwiftUI with
|
||||
/// the `.continuous` corner style.
|
||||
/// * [ClipOval], for an elliptical clip.
|
||||
/// * [ClipPath], for an arbitrarily shaped clip.
|
||||
class ClipRRect extends SingleChildRenderObjectWidget {
|
||||
@ -1036,6 +1041,94 @@ class ClipRRect extends SingleChildRenderObjectWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that clips its child using a rounded superellipse.
|
||||
///
|
||||
/// A rounded superellipse is a shape similar to a typical rounded rectangle
|
||||
/// ([ClipRRect]), but with smoother transitions between the straight sides and
|
||||
/// the rounded corners. It resembles the `RoundedRectangle` shape in SwiftUI
|
||||
/// with the `.continuous` corner style. Technically, it is created by replacing
|
||||
/// the four corners of a superellipse (also known as a Lamé curve) with
|
||||
/// circular arcs.
|
||||
///
|
||||
/// By default, [ClipRSuperellipse] uses its own bounds as the base rectangle
|
||||
/// for the clip, but the size and location of the clip can be customized using
|
||||
/// a custom [clipper].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CustomClipper], for information about creating custom clips.
|
||||
/// * [ClipRect], for more efficient clips without rounded corners.
|
||||
/// * [ClipRRect], for a typical rounded rectangle, which is created by
|
||||
/// replacing the four corners of a rectangle with circular arcs.
|
||||
/// * [ClipOval], for an elliptical clip.
|
||||
/// * [ClipPath], for an arbitrarily shaped clip.
|
||||
class ClipRSuperellipse extends SingleChildRenderObjectWidget {
|
||||
/// Creates a rounded-superellipse clip.
|
||||
///
|
||||
/// The [borderRadius] defaults to [BorderRadius.zero], i.e. a rectangle with
|
||||
/// right-angled corners.
|
||||
///
|
||||
/// If [clipBehavior] is [Clip.none], no clipping will be applied.
|
||||
const ClipRSuperellipse({
|
||||
super.key,
|
||||
this.borderRadius = BorderRadius.zero,
|
||||
this.clipper,
|
||||
this.clipBehavior = Clip.antiAlias,
|
||||
super.child,
|
||||
});
|
||||
|
||||
/// The border radius of the rounded corners.
|
||||
///
|
||||
/// Values are clamped so that horizontal and vertical radii sums do not
|
||||
/// exceed width/height.
|
||||
///
|
||||
/// This value is ignored if [clipper] is non-null.
|
||||
final BorderRadiusGeometry borderRadius;
|
||||
|
||||
/// If non-null, determines which clip to use.
|
||||
final CustomClipper<RSuperellipse>? clipper;
|
||||
|
||||
/// {@macro flutter.rendering.ClipRectLayer.clipBehavior}
|
||||
///
|
||||
/// Defaults to [Clip.antiAlias].
|
||||
final Clip clipBehavior;
|
||||
|
||||
@override
|
||||
RenderClipRSuperellipse createRenderObject(BuildContext context) {
|
||||
return RenderClipRSuperellipse(
|
||||
borderRadius: borderRadius,
|
||||
clipBehavior: clipBehavior,
|
||||
clipper: clipper,
|
||||
textDirection: Directionality.maybeOf(context),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void updateRenderObject(BuildContext context, RenderClipRSuperellipse renderObject) {
|
||||
renderObject
|
||||
..borderRadius = borderRadius
|
||||
..clipBehavior = clipBehavior
|
||||
..clipper = clipper
|
||||
..textDirection = Directionality.maybeOf(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(
|
||||
DiagnosticsProperty<BorderRadiusGeometry>(
|
||||
'borderRadius',
|
||||
borderRadius,
|
||||
showName: false,
|
||||
defaultValue: null,
|
||||
),
|
||||
);
|
||||
properties.add(
|
||||
DiagnosticsProperty<CustomClipper<RSuperellipse>>('clipper', clipper, defaultValue: null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A widget that clips its child using an oval.
|
||||
///
|
||||
/// {@youtube 560 315 https://www.youtube.com/watch?v=vzWWDO6whIM}
|
||||
|
@ -922,7 +922,10 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect the modal dialog box to take all available height.
|
||||
expect(tester.getSize(find.byType(ClipRRect)), equals(const Size(310.0, 560.0 - 24.0 * 2)));
|
||||
expect(
|
||||
tester.getSize(find.byType(ClipRSuperellipse)),
|
||||
equals(const Size(310.0, 560.0 - 24.0 * 2)),
|
||||
);
|
||||
|
||||
// Check sizes/locations of the text. The text is large so these 2 buttons are stacked.
|
||||
// Visually the "Cancel" button and "OK" button are the same height when using the
|
||||
@ -974,7 +977,7 @@ void main() {
|
||||
const double topAndBottomMargin = 40.0;
|
||||
const double topAndBottomPadding = 24.0 * 2;
|
||||
const double leftAndRightPadding = 40.0 * 2;
|
||||
final Finder modalFinder = find.byType(ClipRRect);
|
||||
final Finder modalFinder = find.byType(ClipRSuperellipse);
|
||||
expect(
|
||||
tester.getSize(modalFinder),
|
||||
equals(
|
||||
@ -1081,7 +1084,7 @@ void main() {
|
||||
return element.widget.runtimeType.toString() == '_CupertinoAlertActionSection';
|
||||
});
|
||||
|
||||
final Finder modalBoundaryFinder = find.byType(ClipRRect);
|
||||
final Finder modalBoundaryFinder = find.byType(ClipRSuperellipse);
|
||||
|
||||
expect(tester.getSize(contentSectionFinder), tester.getSize(modalBoundaryFinder));
|
||||
|
||||
@ -1128,7 +1131,7 @@ void main() {
|
||||
return element.widget.runtimeType.toString() == '_CupertinoAlertContentSection';
|
||||
});
|
||||
|
||||
final Finder modalBoundaryFinder = find.byType(ClipRRect);
|
||||
final Finder modalBoundaryFinder = find.byType(ClipRSuperellipse);
|
||||
|
||||
expect(tester.getSize(contentSectionFinder), tester.getSize(modalBoundaryFinder));
|
||||
});
|
||||
@ -1449,7 +1452,7 @@ void main() {
|
||||
// The buttons should be out of the screen
|
||||
expect(
|
||||
tester.getTopLeft(find.text('Button 0')).dy,
|
||||
greaterThan(tester.getBottomLeft(find.byType(ClipRRect)).dy),
|
||||
greaterThan(tester.getBottomLeft(find.byType(ClipRSuperellipse)).dy),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(CupertinoAlertDialog),
|
||||
|
@ -357,6 +357,14 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('ClipRSuperellipseLayer prints clipBehavior in debug info', () {
|
||||
expect(getDebugInfo(ClipRSuperellipseLayer()), contains('clipBehavior: Clip.antiAlias'));
|
||||
expect(
|
||||
getDebugInfo(ClipRSuperellipseLayer(clipBehavior: Clip.antiAliasWithSaveLayer)),
|
||||
contains('clipBehavior: Clip.antiAliasWithSaveLayer'),
|
||||
);
|
||||
});
|
||||
|
||||
test('ClipPathLayer prints clipBehavior in debug info', () {
|
||||
expect(getDebugInfo(ClipPathLayer()), contains('clipBehavior: Clip.antiAlias'));
|
||||
expect(
|
||||
@ -464,6 +472,18 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
test('mutating ClipRSuperellipseLayer fields triggers needsAddToScene', () {
|
||||
final ClipRSuperellipseLayer layer = ClipRSuperellipseLayer(
|
||||
clipRSuperellipse: RSuperellipse.zero,
|
||||
);
|
||||
checkNeedsAddToScene(layer, () {
|
||||
layer.clipRSuperellipse = RSuperellipse.fromRectAndRadius(unitRect, Radius.zero);
|
||||
});
|
||||
checkNeedsAddToScene(layer, () {
|
||||
layer.clipBehavior = Clip.antiAliasWithSaveLayer;
|
||||
});
|
||||
});
|
||||
|
||||
test('mutating ClipPath fields triggers needsAddToScene', () {
|
||||
final ClipPathLayer layer = ClipPathLayer(clipPath: Path());
|
||||
checkNeedsAddToScene(layer, () {
|
||||
@ -726,12 +746,16 @@ void main() {
|
||||
expect(layer.describeClipBounds(), null);
|
||||
|
||||
const Rect bounds = Rect.fromLTRB(10, 10, 20, 20);
|
||||
final RRect rbounds = RRect.fromRectXY(bounds, 2, 2);
|
||||
final RRect rrBounds = RRect.fromRectXY(bounds, 2, 2);
|
||||
final RSuperellipse rseBounds = RSuperellipse.fromRectXY(bounds, 2, 2);
|
||||
layer = ClipRectLayer(clipRect: bounds);
|
||||
expect(layer.describeClipBounds(), bounds);
|
||||
|
||||
layer = ClipRRectLayer(clipRRect: rbounds);
|
||||
expect(layer.describeClipBounds(), rbounds.outerRect);
|
||||
layer = ClipRRectLayer(clipRRect: rrBounds);
|
||||
expect(layer.describeClipBounds(), rrBounds.outerRect);
|
||||
|
||||
layer = ClipRSuperellipseLayer(clipRSuperellipse: rseBounds);
|
||||
expect(layer.describeClipBounds(), rseBounds.outerRect);
|
||||
|
||||
layer = ClipPathLayer(clipPath: Path()..addRect(bounds));
|
||||
expect(layer.describeClipBounds(), bounds);
|
||||
@ -990,6 +1014,7 @@ void main() {
|
||||
final OpacityLayer opacityLayer = OpacityLayer();
|
||||
final ClipRectLayer clipRectLayer = ClipRectLayer();
|
||||
final ClipRRectLayer clipRRectLayer = ClipRRectLayer();
|
||||
final ClipRSuperellipseLayer clipRSuperellipseLayer = ClipRSuperellipseLayer();
|
||||
final ImageFilterLayer imageFilterLayer = ImageFilterLayer();
|
||||
final BackdropFilterLayer backdropFilterLayer = BackdropFilterLayer();
|
||||
final ColorFilterLayer colorFilterLayer = ColorFilterLayer();
|
||||
@ -999,6 +1024,7 @@ void main() {
|
||||
expect(opacityLayer.supportsRasterization(), true);
|
||||
expect(clipRectLayer.supportsRasterization(), true);
|
||||
expect(clipRRectLayer.supportsRasterization(), true);
|
||||
expect(clipRSuperellipseLayer.supportsRasterization(), true);
|
||||
expect(imageFilterLayer.supportsRasterization(), true);
|
||||
expect(backdropFilterLayer.supportsRasterization(), true);
|
||||
expect(colorFilterLayer.supportsRasterization(), true);
|
||||
|
@ -266,6 +266,24 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
test('PaintingContext.pushClipRSuperellipse reuses the layer', () {
|
||||
_testPaintingContextLayerReuse<ClipRSuperellipseLayer>((
|
||||
PaintingContextCallback painter,
|
||||
PaintingContext context,
|
||||
Offset offset,
|
||||
Layer? oldLayer,
|
||||
) {
|
||||
return context.pushClipRSuperellipse(
|
||||
true,
|
||||
offset,
|
||||
Rect.zero,
|
||||
RSuperellipse.fromRectAndRadius(Rect.zero, const Radius.circular(1.0)),
|
||||
painter,
|
||||
oldLayer: oldLayer as ClipRSuperellipseLayer?,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('PaintingContext.pushClipPath reuses the layer', () {
|
||||
_testPaintingContextLayerReuse<ClipPathLayer>((
|
||||
PaintingContextCallback painter,
|
||||
|
@ -289,6 +289,19 @@ abstract class PaintPattern {
|
||||
PaintingStyle style,
|
||||
});
|
||||
|
||||
/// Indicates that a rounded superellipse clip is expected next.
|
||||
///
|
||||
/// The next rounded superellipse clip is examined. Any arguments that are
|
||||
/// passed to this method are compared to the actual
|
||||
/// [Canvas.clipRSuperellipse] call's argument and any mismatches result in
|
||||
/// failure.
|
||||
///
|
||||
/// If no call to [Canvas.clipRSuperellipse] was made, then this results in failure.
|
||||
///
|
||||
/// Any calls made between the last matched call (if any) and the
|
||||
/// [Canvas.clipRSuperellipse] call are ignored.
|
||||
void clipRSuperellipse({RSuperellipse? rsuperellipse});
|
||||
|
||||
/// Indicates that a circle is expected next.
|
||||
///
|
||||
/// The next circle is examined. Any arguments that are passed to this method
|
||||
@ -903,6 +916,11 @@ class _TestRecordingCanvasPatternMatcher extends _TestRecordingCanvasMatcher
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void clipRSuperellipse({RSuperellipse? rsuperellipse}) {
|
||||
_predicates.add(_FunctionPaintPredicate(#clipRSuperellipse, <dynamic>[rsuperellipse]));
|
||||
}
|
||||
|
||||
@override
|
||||
void circle({
|
||||
double? x,
|
||||
|
@ -151,6 +151,25 @@ class TestRecordingPaintingContext extends ClipContext implements PaintingContex
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
ClipRSuperellipseLayer? pushClipRSuperellipse(
|
||||
bool needsCompositing,
|
||||
Offset offset,
|
||||
Rect bounds,
|
||||
RSuperellipse clipRSuperellipse,
|
||||
PaintingContextCallback painter, {
|
||||
Clip clipBehavior = Clip.antiAlias,
|
||||
ClipRSuperellipseLayer? oldLayer,
|
||||
}) {
|
||||
clipRSuperellipseAndPaint(
|
||||
clipRSuperellipse.shift(offset),
|
||||
clipBehavior,
|
||||
bounds.shift(offset),
|
||||
() => painter(this, offset),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
ClipPathLayer? pushClipPath(
|
||||
bool needsCompositing,
|
||||
|
Loading…
x
Reference in New Issue
Block a user