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
|
@override
|
||||||
Path getClip(Size size) {
|
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
|
// button is the floating action button's bounding rectangle in the
|
||||||
// coordinate system that origins at the appBar's top left corner.
|
// coordinate system whose origin is at the appBar's top left corner,
|
||||||
final Rect button = geometry.value.floatingActionButtonArea
|
// or null if there is no floating action button.
|
||||||
.translate(0.0, geometry.value.bottomNavigationBarTop * -1.0);
|
final Rect button = geometry.value.floatingActionButtonArea?.translate(
|
||||||
|
0.0,
|
||||||
return shape.getOuterPath(appBar, button.inflate(notchMargin));
|
geometry.value.bottomNavigationBarTop * -1.0,
|
||||||
|
);
|
||||||
|
return shape.getOuterPath(Offset.zero & size, button?.inflate(notchMargin));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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.
|
/// 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
|
@immutable
|
||||||
abstract class ShapeBorder {
|
abstract class ShapeBorder {
|
||||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||||
|
@ -5,15 +5,18 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'basic_types.dart';
|
import 'basic_types.dart';
|
||||||
|
import 'borders.dart';
|
||||||
|
|
||||||
/// A shape with a notch in its outline.
|
/// A shape with a notch in its outline.
|
||||||
///
|
///
|
||||||
/// Typically used as the outline of a 'host' widget to make a notch that
|
/// 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
|
/// accommodates a 'guest' widget. e.g the [BottomAppBar] may have a notch to
|
||||||
/// accommodate the [FloatingActionButton].
|
/// accommodate the [FloatingActionButton].
|
||||||
|
///
|
||||||
/// See also: [ShapeBorder], which defines a shaped border without a dynamic
|
/// See also:
|
||||||
/// notch.
|
///
|
||||||
|
/// * [ShapeBorder], which defines a shaped border without a dynamic notch.
|
||||||
|
/// * [AutomaticNotchedShape], an adapter from [ShapeBorder] to [NotchedShape].
|
||||||
abstract class NotchedShape {
|
abstract class NotchedShape {
|
||||||
/// Abstract const constructor. This constructor enables subclasses to provide
|
/// Abstract const constructor. This constructor enables subclasses to provide
|
||||||
/// const constructors so that they can be used in const expressions.
|
/// 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.
|
/// The `host` is the bounding rectangle of the shape.
|
||||||
///
|
///
|
||||||
/// Rhe `guest` is the bounding rectangle of the shape for which a notch will
|
/// The `guest` is the bounding rectangle of the shape for which a notch will
|
||||||
/// be made.
|
/// be made. It is null when there is no guest.
|
||||||
Path getOuterPath(Rect host, Rect guest);
|
Path getOuterPath(Rect host, Rect guest);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A rectangle with a smooth circular notch.
|
/// 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].
|
/// Creates a [CircularNotchedRectangle].
|
||||||
///
|
///
|
||||||
/// The same object can be used to create multiple shapes.
|
/// 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.
|
// TODO(amirh): add an example diagram here.
|
||||||
@override
|
@override
|
||||||
Path getOuterPath(Rect host, Rect guest) {
|
Path getOuterPath(Rect host, Rect guest) {
|
||||||
if (!host.overlaps(guest))
|
if (guest == null || !host.overlaps(guest))
|
||||||
return Path()..addRect(host);
|
return Path()..addRect(host);
|
||||||
|
|
||||||
// The guest's shape is a circle bounded by the guest rectangle.
|
// The guest's shape is a circle bounded by the guest rectangle.
|
||||||
@ -111,3 +118,47 @@ class CircularNotchedRectangle implements NotchedShape {
|
|||||||
..close();
|
..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 {
|
testWidgets('color defaults to Theme.bottomAppBarColor', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
MaterialApp(
|
MaterialApp(
|
||||||
@ -92,7 +135,7 @@ void main() {
|
|||||||
|
|
||||||
// This is a regression test for a bug we had where toggling the notch on/off
|
// 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
|
// 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 {
|
testWidgets('toggle shape to null', (WidgetTester tester) async {
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
const MaterialApp(
|
const MaterialApp(
|
||||||
@ -386,11 +429,13 @@ class ShapeListenerState extends State<ShapeListener> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class RectangularNotch implements NotchedShape {
|
class RectangularNotch extends NotchedShape {
|
||||||
const RectangularNotch();
|
const RectangularNotch();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Path getOuterPath(Rect host, Rect guest) {
|
Path getOuterPath(Rect host, Rect guest) {
|
||||||
|
if (guest == null)
|
||||||
|
return Path()..addRect(host);
|
||||||
return Path()
|
return Path()
|
||||||
..moveTo(host.left, host.top)
|
..moveTo(host.left, host.top)
|
||||||
..lineTo(guest.left, host.top)
|
..lineTo(guest.left, host.top)
|
||||||
|
@ -47,6 +47,63 @@ void main() {
|
|||||||
expect(pathDoesNotContainCircle(actualPath, guest), isTrue);
|
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