mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Make NotchedShape more practical to use (#27487)
This PR does two things: - It allows BottomAppBar to have a custom shape even when it doesn't have a notch. - It adds AutomaticNotchedShape, an adapter from ShapeBorder to NotchedShape.
This commit is contained in:
parent
51780b87a7
commit
f4d5646b31
@ -1 +1 @@
|
||||
cbd3fa445868962b7e910e498791755c988e9890
|
||||
ec90c64e598804d691c1c6bfcd191a63480e3053
|
||||
|
@ -156,19 +156,20 @@ class _BottomAppBarClipper extends CustomClipper<Path> {
|
||||
|
||||
@override
|
||||
Path getClip(Size size) {
|
||||
final Rect appBar = Offset.zero & size;
|
||||
if (geometry.value.floatingActionButtonArea == null) {
|
||||
return Path()..addRect(appBar);
|
||||
}
|
||||
|
||||
// button is the floating action button's bounding rectangle in the
|
||||
// coordinate system that origins at the appBar's top left corner.
|
||||
final Rect button = geometry.value.floatingActionButtonArea
|
||||
.translate(0.0, geometry.value.bottomNavigationBarTop * -1.0);
|
||||
|
||||
return shape.getOuterPath(appBar, button.inflate(notchMargin));
|
||||
// coordinate system whose origin is at the appBar's top left corner,
|
||||
// or null if there is no floating action button.
|
||||
final Rect button = geometry.value.floatingActionButtonArea?.translate(
|
||||
0.0,
|
||||
geometry.value.bottomNavigationBarTop * -1.0,
|
||||
);
|
||||
return shape.getOuterPath(Offset.zero & size, button?.inflate(notchMargin));
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReclip(_BottomAppBarClipper oldClipper) => oldClipper.geometry != geometry;
|
||||
bool shouldReclip(_BottomAppBarClipper oldClipper) {
|
||||
return oldClipper.geometry != geometry
|
||||
|| oldClipper.shape != shape
|
||||
|| oldClipper.notchMargin != notchMargin;
|
||||
}
|
||||
}
|
||||
|
@ -263,7 +263,17 @@ class BorderSide {
|
||||
|
||||
/// Base class for shape outlines.
|
||||
///
|
||||
/// This class handles how to add multiple borders together.
|
||||
/// This class handles how to add multiple borders together. Subclasses define
|
||||
/// various shapes, like circles ([CircleBorder]), rounded rectangles
|
||||
/// ([RoundedRectangleBorder]), superellipses ([SuperellipseShape]), or beveled
|
||||
/// rectangles ([BeveledRectangleBorder]).
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ShapeDecoration], which can be used with [DecoratedBox] to show a shape.
|
||||
/// * [Material] (and many other widgets in the Material library), which takes
|
||||
/// a [ShapeBorder] to define its shape.
|
||||
/// * [NotchedShape], which describes a shape with a hole in it.
|
||||
@immutable
|
||||
abstract class ShapeBorder {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
|
@ -5,15 +5,18 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'basic_types.dart';
|
||||
import 'borders.dart';
|
||||
|
||||
/// A shape with a notch in its outline.
|
||||
///
|
||||
/// Typically used as the outline of a 'host' widget to make a notch that
|
||||
/// accommodates a 'guest' widget. e.g the [BottomAppBar] may have a notch to
|
||||
/// accommodate the [FloatingActionButton].
|
||||
|
||||
/// See also: [ShapeBorder], which defines a shaped border without a dynamic
|
||||
/// notch.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ShapeBorder], which defines a shaped border without a dynamic notch.
|
||||
/// * [AutomaticNotchedShape], an adapter from [ShapeBorder] to [NotchedShape].
|
||||
abstract class NotchedShape {
|
||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||
/// const constructors so that they can be used in const expressions.
|
||||
@ -23,13 +26,17 @@ abstract class NotchedShape {
|
||||
///
|
||||
/// The `host` is the bounding rectangle of the shape.
|
||||
///
|
||||
/// Rhe `guest` is the bounding rectangle of the shape for which a notch will
|
||||
/// be made.
|
||||
/// The `guest` is the bounding rectangle of the shape for which a notch will
|
||||
/// be made. It is null when there is no guest.
|
||||
Path getOuterPath(Rect host, Rect guest);
|
||||
}
|
||||
|
||||
/// A rectangle with a smooth circular notch.
|
||||
class CircularNotchedRectangle implements NotchedShape {
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CircleBorder], a [ShapeBorder] that describes a circle.
|
||||
class CircularNotchedRectangle extends NotchedShape {
|
||||
/// Creates a [CircularNotchedRectangle].
|
||||
///
|
||||
/// The same object can be used to create multiple shapes.
|
||||
@ -49,7 +56,7 @@ class CircularNotchedRectangle implements NotchedShape {
|
||||
// TODO(amirh): add an example diagram here.
|
||||
@override
|
||||
Path getOuterPath(Rect host, Rect guest) {
|
||||
if (!host.overlaps(guest))
|
||||
if (guest == null || !host.overlaps(guest))
|
||||
return Path()..addRect(host);
|
||||
|
||||
// The guest's shape is a circle bounded by the guest rectangle.
|
||||
@ -111,3 +118,47 @@ class CircularNotchedRectangle implements NotchedShape {
|
||||
..close();
|
||||
}
|
||||
}
|
||||
|
||||
/// A [NotchedShape] created from [ShapeBorder]s.
|
||||
///
|
||||
/// Two shapes can be provided. The [host] is the shape of the widget that
|
||||
/// uses the [NotchedShape] (typically a [BottomAppBar]). The [guest] is
|
||||
/// subtracted from the [host] to create the notch (typically to make room
|
||||
/// for a [FloatingActionButton]).
|
||||
class AutomaticNotchedShape extends NotchedShape {
|
||||
/// Creates a [NotchedShape] that is defined by two [ShapeBorder]s.
|
||||
///
|
||||
/// The [host] must not be null.
|
||||
///
|
||||
/// The [guest] may be null, in which case no notch is created even
|
||||
/// if a guest rectangle is provided to [getOuterPath].
|
||||
const AutomaticNotchedShape(this.host, [ this.guest ]);
|
||||
|
||||
/// The shape of the widget that uses the [NotchedShape] (typically a
|
||||
/// [BottomAppBar]).
|
||||
///
|
||||
/// This shape cannot depend on the [TextDirection], as no text direction
|
||||
/// is available to [NotchedShape]s.
|
||||
final ShapeBorder host;
|
||||
|
||||
/// The shape to subtract from the [host] to make the notch.
|
||||
///
|
||||
/// This shape cannot depend on the [TextDirection], as no text direction
|
||||
/// is available to [NotchedShape]s.
|
||||
///
|
||||
/// If this is null, [getOuterPath] ignores the guest rectangle.
|
||||
final ShapeBorder guest;
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect hostRect, Rect guestRect) { // ignore: avoid_renaming_method_parameters, the
|
||||
// parameters are renamed over the baseclass because they would clash
|
||||
// with properties of this object, and the use of all four of them in
|
||||
// the code below is really confusing if they have the same names.
|
||||
final Path hostPath = host.getOuterPath(hostRect);
|
||||
if (guest != null && guestRect != null) {
|
||||
final Path guestPath = guest.getOuterPath(guestRect);
|
||||
return Path.combine(PathOperation.difference, hostPath, guestPath);
|
||||
}
|
||||
return hostPath;
|
||||
}
|
||||
}
|
||||
|
@ -38,6 +38,49 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('custom shape', (WidgetTester tester) async {
|
||||
final Key key = UniqueKey();
|
||||
Future<void> pump(FloatingActionButtonLocation location) async {
|
||||
await tester.pumpWidget(
|
||||
SizedBox(
|
||||
width: 200,
|
||||
height: 200,
|
||||
child: RepaintBoundary(
|
||||
key: key,
|
||||
child: MaterialApp(
|
||||
home: Scaffold(
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () { },
|
||||
),
|
||||
floatingActionButtonLocation: location,
|
||||
bottomNavigationBar: BottomAppBar(
|
||||
shape: AutomaticNotchedShape(
|
||||
BeveledRectangleBorder(borderRadius: BorderRadius.circular(50.0)),
|
||||
SuperellipseShape(borderRadius: BorderRadius.circular(30.0)),
|
||||
),
|
||||
notchMargin: 10.0,
|
||||
color: Colors.green,
|
||||
child: const SizedBox(height: 100.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
await pump(FloatingActionButtonLocation.endDocked);
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('bottom_app_bar.custom_shape.1.png'),
|
||||
);
|
||||
await pump(FloatingActionButtonLocation.centerDocked);
|
||||
await tester.pumpAndSettle();
|
||||
await expectLater(
|
||||
find.byKey(key),
|
||||
matchesGoldenFile('bottom_app_bar.custom_shape.2.png'),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('color defaults to Theme.bottomAppBarColor', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
@ -92,7 +135,7 @@ void main() {
|
||||
|
||||
// This is a regression test for a bug we had where toggling the notch on/off
|
||||
// would crash, as the shouldReclip method of ShapeBorderClipper or
|
||||
// _BottomAppBarClipper will try an illegal downcast.
|
||||
// _BottomAppBarClipper would try an illegal downcast.
|
||||
testWidgets('toggle shape to null', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
@ -386,11 +429,13 @@ class ShapeListenerState extends State<ShapeListener> {
|
||||
|
||||
}
|
||||
|
||||
class RectangularNotch implements NotchedShape {
|
||||
class RectangularNotch extends NotchedShape {
|
||||
const RectangularNotch();
|
||||
|
||||
@override
|
||||
Path getOuterPath(Rect host, Rect guest) {
|
||||
if (guest == null)
|
||||
return Path()..addRect(host);
|
||||
return Path()
|
||||
..moveTo(host.left, host.top)
|
||||
..lineTo(guest.left, host.top)
|
||||
|
@ -47,6 +47,63 @@ void main() {
|
||||
expect(pathDoesNotContainCircle(actualPath, guest), isTrue);
|
||||
});
|
||||
|
||||
test('no guest is ok', () {
|
||||
final Rect host = Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
|
||||
expect(
|
||||
const CircularNotchedRectangle().getOuterPath(host, null),
|
||||
coversSameAreaAs(
|
||||
Path()..addRect(host),
|
||||
areaToCompare: host.inflate(800.0),
|
||||
sampleSize: 100,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('AutomaticNotchedShape - with guest', () {
|
||||
expect(
|
||||
const AutomaticNotchedShape(
|
||||
RoundedRectangleBorder(),
|
||||
RoundedRectangleBorder(),
|
||||
).getOuterPath(
|
||||
Rect.fromLTWH(-200.0, -100.0, 50.0, 100.0),
|
||||
Rect.fromLTWH(-175.0, -110.0, 100.0, 100.0),
|
||||
),
|
||||
coversSameAreaAs(
|
||||
Path()
|
||||
..moveTo(-200.0, -100.0)
|
||||
..lineTo(-150.0, -100.0)
|
||||
..lineTo(-150.0, -10.0)
|
||||
..lineTo(-175.0, -10.0)
|
||||
..lineTo(-175.0, 0.0)
|
||||
..lineTo(-200.0, 0.0)
|
||||
..close(),
|
||||
areaToCompare: Rect.fromLTWH(-300.0, -300.0, 600.0, 600.0),
|
||||
sampleSize: 100,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('AutomaticNotchedShape - no guest', () {
|
||||
expect(
|
||||
const AutomaticNotchedShape(
|
||||
RoundedRectangleBorder(),
|
||||
RoundedRectangleBorder(),
|
||||
).getOuterPath(
|
||||
Rect.fromLTWH(-200.0, -100.0, 50.0, 100.0),
|
||||
null,
|
||||
),
|
||||
coversSameAreaAs(
|
||||
Path()
|
||||
..moveTo(-200.0, -100.0)
|
||||
..lineTo(-150.0, -100.0)
|
||||
..lineTo(-150.0, 0.0)
|
||||
..lineTo(-200.0, 0.0)
|
||||
..close(),
|
||||
areaToCompare: Rect.fromLTWH(-300.0, -300.0, 600.0, 600.0),
|
||||
sampleSize: 100,
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user