Unverified Commit 7996d7fb authored by amirh's avatar amirh Committed by GitHub

Initial implementation of BottomAppBar (#14713)

* add a FAB NotchMaker to ScaffoldGeometry

* add a notchMaker to FloatingActionButton

* Initial implementation of BottomAppBar

Mainly includes the notch making logic.
Not yet tested as currently there is no way to make the FAB and the BAB
overlap, once #14368 to lands we could add unit tests to the
BottomAppBar as well.

* use a closeable for clearing the FAB notchmaker
parent ca677011
......@@ -20,6 +20,7 @@ export 'src/material/app.dart';
export 'src/material/app_bar.dart';
export 'src/material/arc.dart';
export 'src/material/back_button.dart';
export 'src/material/bottom_app_bar.dart';
export 'src/material/bottom_navigation_bar.dart';
export 'src/material/bottom_sheet.dart';
export 'src/material/button.dart';
......
// Copyright 2018 The Chromium 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/foundation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'material.dart';
import 'scaffold.dart';
// Examples can assume:
// Widget bottomAppBarContents;
/// A container that s typically ised with [Scaffold.bottomNavigationBar], and
/// can have a notch along the top that makes room for an overlapping
/// [FloatingActionButton].
///
/// Typically used with a [Scaffold] and a [FloatingActionButton].
///
/// ## Sample code
///
/// ```dart
/// new Scaffold(
/// bottomNavigationBar: new BottomAppBar(
/// color: Colors.white,
/// child: bottomAppBarContents,
/// ),
/// floatingActionButton: new FloatingActionButton(onPressed: null),
/// )
/// ```
///
/// See also:
///
/// * [ComputeNotch] a function used for creating a notch in a shape.
/// * [ScaffoldGeometry.floatingActionBarComputeNotch] the [ComputeNotch] used to
/// make a notch for the [FloatingActionButton]
/// * [FloatingActionButton] which the [BottomAppBar] makes a notch for.
/// * [AppBar] for a toolbar that is shown at the top of the screen.
class BottomAppBar extends StatefulWidget {
/// Creates a bottom application bar.
///
/// The [color] and [elevation] arguments must not be null.
const BottomAppBar({
Key key,
this.color,
this.elevation: 8.0,
this.child,
}) : assert(elevation != null),
assert(elevation >= 0.0),
super(key: key);
/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.child}
///
/// Typically this the child will be a [Row], with the first child
/// being an [IconButton] with the [Icons.menu] icon.
final Widget child;
/// The bottom app bar's background color.
final Color color;
/// The z-coordinate at which to place this bottom app bar. This controls the
/// size of the shadow below the bottom app bar.
///
/// Defaults to 8, the appropriate elevation for bottom app bars.
final double elevation;
@override
State createState() => new _BottomAppBarState();
}
class _BottomAppBarState extends State<BottomAppBar> {
ValueListenable<ScaffoldGeometry> geometryListenable;
@override
void didChangeDependencies() {
super.didChangeDependencies();
geometryListenable = Scaffold.geometryOf(context);
}
@override
Widget build(BuildContext context) {
return new PhysicalShape(
clipper: new _BottomAppBarClipper(geometry: geometryListenable),
elevation: widget.elevation,
// TODO(amirh): use a default color from the theme.
color: widget.color ?? Colors.white,
child: new Material(
type: MaterialType.transparency,
child: widget.child,
),
);
}
}
class _BottomAppBarClipper extends CustomClipper<Path> {
const _BottomAppBarClipper({
@required this.geometry
}) : assert(geometry != null),
super(reclip: geometry);
final ValueListenable<ScaffoldGeometry> geometry;
@override
Path getClip(Size size) {
final Rect appBar = Offset.zero & size;
if (geometry.value.floatingActionButtonArea == null ||
geometry.value.floatingActionButtonNotch == null) {
return new 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);
if (appBar.overlaps(button)) {
return new Path()..addRect(appBar);
}
final ComputeNotch computeNotch = geometry.value.floatingActionButtonNotch;
return new Path()
..moveTo(appBar.left, appBar.top)
..addPath(
computeNotch(
appBar,
button,
new Offset(appBar.left, appBar.top),
new Offset(appBar.right, appBar.top)
),
Offset.zero
)
..lineTo(appBar.right, appBar.top)
..lineTo(appBar.right, appBar.bottom)
..lineTo(appBar.left, appBar.bottom)
..close();
}
@override
bool shouldReclip(covariant _BottomAppBarClipper oldClipper) {
return oldClipper.geometry != geometry;
}
}
......@@ -2,12 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
import 'ink_well.dart';
import 'material.dart';
import 'scaffold.dart';
import 'theme.dart';
import 'tooltip.dart';
......@@ -22,6 +25,7 @@ class _DefaultHeroTag {
String toString() => '<default FloatingActionButton tag>';
}
// TODO(amirh): update the documentation once the BAB notch can be disabled.
/// A material design floating action button.
///
/// A floating action button is a circular icon button that hovers over content
......@@ -35,6 +39,12 @@ class _DefaultHeroTag {
/// If the [onPressed] callback is null, then the button will be disabled and
/// will not react to touch.
///
/// If the floating action button is a descendant of a [Scaffold] that also has a
/// [BottomAppBar], the [BottomAppBar] will show a notch to accomodate the
/// [FloatingActionButton] when it overlaps the [BottomAppBar]. The notch's
/// shape is an arc for a circle whose radius is the floating action button's
/// radius plus [FloatingActionButton.notchMargin].
///
/// See also:
///
/// * [Scaffold]
......@@ -54,7 +64,8 @@ class FloatingActionButton extends StatefulWidget {
this.elevation: 6.0,
this.highlightElevation: 12.0,
@required this.onPressed,
this.mini: false
this.mini: false,
this.notchMargin: 4.0,
}) : super(key: key);
/// The widget below this widget in the tree.
......@@ -117,6 +128,19 @@ class FloatingActionButton extends StatefulWidget {
/// and width of 40.0 logical pixels.
final bool mini;
/// The margin to keep around the floating action button when creating a
/// notch for it.
///
/// The notch is an arc of a circle with radius r+[notchMargin] where r is the
/// radius of the floating action button. This expanded radius leaves a margin
/// around the floating action button.
///
/// See also:
///
/// * [BottomAppBar], a material design elements that shows a notch for the
/// floating action button.
final double notchMargin;
@override
_FloatingActionButtonState createState() => new _FloatingActionButtonState();
}
......@@ -124,6 +148,8 @@ class FloatingActionButton extends StatefulWidget {
class _FloatingActionButtonState extends State<FloatingActionButton> {
bool _highlight = false;
VoidCallback _clearComputeNotch;
void _handleHighlightChanged(bool value) {
setState(() {
_highlight = value;
......@@ -186,4 +212,80 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
return result;
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_clearComputeNotch = Scaffold.setFloatingActionButtonNotchFor(context, _computeNotch);
}
@override
void deactivate() {
if (_clearComputeNotch != null)
_clearComputeNotch();
super.deactivate();
}
Path _computeNotch(Rect host, Rect guest, Offset start, Offset end) {
assert(() {
if (end.dy != host.top)
throw new FlutterError(
'The floating action button\'s notch maker must only be used for a notch in the top edge of the host.\n'
'The notch\'s path end point: $end is not in the top edge of $host'
);
if (start.dy != host.top)
throw new FlutterError(
'The floating action button\'s notch maker must only be used for a notch in the top edge the host.\n'
'The notch\'s path start point: $start is not in the top edge of $host'
);
return true;
}());
assert(() {
if (!host.overlaps(guest))
throw new FlutterError('Notch host must intersect with its guest');
return true;
}());
// The FAB's shape is a circle bounded by the guest rectangle.
// So the FAB's radius is half the guest width.
final double fabRadius = guest.width / 2.0;
final double notchRadius = fabRadius + widget.notchMargin;
assert(() {
if (guest.center.dx - notchRadius < start.dx)
throw new FlutterError(
'The notch\'s path start point must be to the left of the notch.\n'
'Start point was $start, guest was $guest, notchMargin was ${widget.notchMargin}.'
);
if (guest.center.dx + notchRadius > end.dx)
throw new FlutterError(
'The notch\'s end point must be to the right of the guest.\n'
'End point was $start, notch was $guest, notchMargin was ${widget.notchMargin}.'
);
return true;
}());
// We find the intersection of the notch's circle with the top edge of the host
// using the Pythagorean theorem for the right triangle that connects the
// center of the notch and the intersection of the notch's circle and the host's
// top edge.
//
// The hypotenuse of this triangle equals the notch's radius, and one side
// (a) is the distance from the notch's center to the top edge.
//
// The other side (b) would be the distance on the horizontal axis between the
// notch's center and the intersection points with it's top edge.
final double a = host.top - guest.center.dy;
final double b = math.sqrt(notchRadius * notchRadius - a * a);
return new Path()
..lineTo(guest.center.dx - b, host.top)
..arcToPoint(
new Offset(guest.center.dx + b, host.top),
radius: new Radius.circular(notchRadius),
clockwise: false,
)
..lineTo(end.dx, end.dy);
}
}
......@@ -24,6 +24,24 @@ const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be de
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 200);
final Tween<double> _kFloatingActionButtonTurnTween = new Tween<double>(begin: -0.125, end: 0.0);
/// Returns a path for a notch in the outline of a shape.
///
/// The path makes a notch in the host shape that can contain the guest shape.
///
/// The `host` is the bounding rectangle for the shape into which the notch will
/// be applied. The `guest` is the bounding rectangle of the shape for which we
/// are creating a notch in the host.
///
/// The `start` and `end` arguments are points on the outline of the host shape
/// that will be connected by the returned path.
///
/// The returned path may pass anywhere, including inside the guest bounds area,
/// and may contain multiple subpaths. The returned path ends at `end` and does
/// not end with a [Path.close]. The returned [Path] is built under the
/// assumption it will be added to an existing path that is at the `start`
/// coordinates using [Path.addPath].
typedef Path ComputeNotch(Rect host, Rect guest, Offset start, Offset end);
enum _ScaffoldSlot {
body,
appBar,
......@@ -47,6 +65,7 @@ class ScaffoldGeometry {
const ScaffoldGeometry({
this.bottomNavigationBarTop,
this.floatingActionButtonArea,
this.floatingActionButtonNotch,
});
/// The distance from the scaffold's top edge to the top edge of the
......@@ -62,25 +81,60 @@ class ScaffoldGeometry {
/// This is null when there is no floating action button showing.
final Rect floatingActionButtonArea;
ScaffoldGeometry _scaleFab(double scaleFactor) {
/// A [ComputeNotch] for the floating action button.
///
/// The contract for this [ComputeNotch] is described in [ComputeNotch] and
/// [Scaffold.setFloatingActionButtonNotchFor].
final ComputeNotch floatingActionButtonNotch;
ScaffoldGeometry _scaleFloatingActionButton(double scaleFactor) {
if (scaleFactor == 1.0)
return this;
if (scaleFactor == 0.0)
return new ScaffoldGeometry(bottomNavigationBarTop: bottomNavigationBarTop);
if (scaleFactor == 0.0) {
return new ScaffoldGeometry(
bottomNavigationBarTop: bottomNavigationBarTop,
floatingActionButtonNotch: floatingActionButtonNotch,
);
}
final Rect scaledFab = Rect.lerp(
final Rect scaledButton = Rect.lerp(
floatingActionButtonArea.center & Size.zero,
floatingActionButtonArea,
scaleFactor
);
return copyWith(floatingActionButtonArea: scaledButton);
}
/// Creates a copy of this [ScaffoldGeometry] but with the given fields replaced with
/// the new values.
ScaffoldGeometry copyWith({
double bottomNavigationBarTop,
Rect floatingActionButtonArea,
ComputeNotch floatingActionButtonNotch,
}) {
return new ScaffoldGeometry(
bottomNavigationBarTop: bottomNavigationBarTop,
floatingActionButtonArea: scaledFab,
bottomNavigationBarTop: bottomNavigationBarTop ?? this.bottomNavigationBarTop,
floatingActionButtonArea: floatingActionButtonArea ?? this.floatingActionButtonArea,
floatingActionButtonNotch: floatingActionButtonNotch ?? this.floatingActionButtonNotch,
);
}
}
class _Closeable {
_Closeable(this.closeCallback) : assert(closeCallback != null);
VoidCallback closeCallback;
void close() {
if (closeCallback == null)
return;
closeCallback();
closeCallback = null;
}
}
class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenable<ScaffoldGeometry> {
_ScaffoldGeometryNotifier(this.geometry, this.context)
: assert (context != null);
......@@ -88,6 +142,7 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl
final BuildContext context;
double fabScale;
ScaffoldGeometry geometry;
_Closeable computeNotchCloseable;
@override
ScaffoldGeometry get value {
......@@ -101,18 +156,36 @@ class _ScaffoldGeometryNotifier extends ChangeNotifier implements ValueListenabl
);
return true;
}());
return geometry._scaleFab(fabScale);
return geometry._scaleFloatingActionButton(fabScale);
}
void _updateWith({
double bottomNavigationBarTop,
Rect floatingActionButtonArea,
double floatingActionButtonScale,
ComputeNotch floatingActionButtonNotch,
}) {
fabScale = floatingActionButtonScale ?? fabScale;
geometry = geometry.copyWith(
bottomNavigationBarTop: bottomNavigationBarTop,
floatingActionButtonArea: floatingActionButtonArea,
floatingActionButtonNotch: floatingActionButtonNotch,
);
notifyListeners();
}
VoidCallback _updateFloatingActionButtonNotch(ComputeNotch fabComputeNotch) {
computeNotchCloseable?.close();
_setFloatingActionButtonNotchAndNotify(fabComputeNotch);
computeNotchCloseable = new _Closeable(() { _setFloatingActionButtonNotchAndNotify(null); });
return computeNotchCloseable.close;
}
void _setFloatingActionButtonNotchAndNotify(ComputeNotch fabComputeNotch) {
geometry = new ScaffoldGeometry(
bottomNavigationBarTop: bottomNavigationBarTop ?? geometry?.bottomNavigationBarTop,
floatingActionButtonArea: floatingActionButtonArea ?? geometry?.floatingActionButtonArea,
bottomNavigationBarTop: geometry.bottomNavigationBarTop,
floatingActionButtonArea: geometry.floatingActionButtonArea,
floatingActionButtonNotch: fabComputeNotch,
);
notifyListeners();
}
......@@ -418,6 +491,8 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr
///
/// * [AppBar], which is a horizontal bar typically shown at the top of an app
/// using the [appBar] property.
/// * [BottomAppBar], which is a horizontal bar typically shown at the bottom
/// of an app using the [bottomNavigationBar] property.
/// * [FloatingActionButton], which is a circular button typically shown in the
/// bottom right corner of the app using the [floatingActionButton] property.
/// * [Drawer], which is a vertical panel that is typically displayed to the
......@@ -676,6 +751,32 @@ class Scaffold extends StatefulWidget {
return scaffoldScope.geometryNotifier;
}
/// Sets the [ScaffoldGeometry.floatingActionButtonNotch] for the closest
/// [Scaffold] ancestor of the given context, if one exists.
///
/// It is guaranteed that `computeNotch` will only be used for making notches
/// in the top edge of the [bottomNavigationBar], the start and end offsets given to
/// it will always be on the top edge of the [bottomNavigationBar], the start offset
/// will be to the left of the floating action button's bounds, and the end
/// offset will be to the right of the floating action button's bounds.
///
/// Returns null if there was no [Scaffold] ancestor.
/// Otherwise, returns a [VoidCallback] that clears the notch maker that was
/// set.
///
/// Callers must invoke the callback when the notch is no longer required.
/// This method is typically called from [State.didChangeDependencies] and the
/// callback should then be invoked from [State.deactivate].
///
/// If there was a previously set [ScaffoldGeometry.floatingActionButtonNotch]
/// it will be overriden.
static VoidCallback setFloatingActionButtonNotchFor(BuildContext context, ComputeNotch computeNotch) {
final _ScaffoldScope scaffoldScope = context.inheritFromWidgetOfExactType(_ScaffoldScope);
if (scaffoldScope == null)
return null;
return scaffoldScope.geometryNotifier._updateFloatingActionButtonNotch(computeNotch);
}
/// Whether the Scaffold that most tightly encloses the given context has a
/// drawer.
///
......@@ -965,7 +1066,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
@override
void initState() {
super.initState();
_geometryNotifier = new _ScaffoldGeometryNotifier(null, context);
_geometryNotifier = new _ScaffoldGeometryNotifier(const ScaffoldGeometry(), context);
}
@override
......
......@@ -4,6 +4,7 @@
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -201,4 +202,194 @@ void main() {
semantics.dispose();
});
group('ComputeNotch', () {
testWidgets('host and guest must intersect', (WidgetTester tester) async {
final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null));
final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
final Rect guest = new Rect.fromLTWH(50.0, 50.0, 10.0, 10.0);
final Offset start = const Offset(10.0, 100.0);
final Offset end = const Offset(60.0, 100.0);
expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);
});
testWidgets('start/end must be on top edge', (WidgetTester tester) async {
final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null));
final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);
Offset start = const Offset(180.0, 100.0);
Offset end = const Offset(220.0, 110.0);
expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);
start = const Offset(180.0, 110.0);
end = const Offset(220.0, 100.0);
expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);
});
testWidgets('start must be to the left of the notch', (WidgetTester tester) async {
final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null));
final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);
final Offset start = const Offset(191.0, 100.0);
final Offset end = const Offset(220.0, 100.0);
expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);
});
testWidgets('end must be to the right of the notch', (WidgetTester tester) async {
final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null));
final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);
final Offset start = const Offset(180.0, 100.0);
final Offset end = const Offset(209.0, 100.0);
expect(() {computeNotch(host, guest, start, end);}, throwsFlutterError);
});
testWidgets('notch no margin', (WidgetTester tester) async {
final ComputeNotch computeNotch = await fetchComputeNotch(tester, const FloatingActionButton(onPressed: null, notchMargin: 0.0));
final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);
final Offset start = const Offset(180.0, 100.0);
final Offset end = const Offset(220.0, 100.0);
final Path actualNotch = computeNotch(host, guest, start, end);
final Path expectedNotch = new Path()
..lineTo(190.0, 100.0)
..arcToPoint(
const Offset(210.0, 100.0),
radius: const Radius.circular(10.0),
clockwise: false
)
..lineTo(220.0, 100.0);
expect(
createNotchedRectangle(host, start.dx, end.dx, actualNotch),
coversSameAreaAs(
createNotchedRectangle(host, start.dx, end.dx, expectedNotch),
areaToCompare: host.inflate(10.0)
)
);
expect(
createNotchedRectangle(host, start.dx, end.dx, actualNotch),
coversSameAreaAs(
createNotchedRectangle(host, start.dx, end.dx, expectedNotch),
areaToCompare: guest.inflate(10.0),
sampleSize: 50,
)
);
});
testWidgets('notch with margin', (WidgetTester tester) async {
final ComputeNotch computeNotch = await fetchComputeNotch(tester,
const FloatingActionButton(onPressed: null, notchMargin: 4.0)
);
final Rect host = new Rect.fromLTRB(0.0, 100.0, 300.0, 300.0);
final Rect guest = new Rect.fromLTRB(190.0, 90.0, 210.0, 110.0);
final Offset start = const Offset(180.0, 100.0);
final Offset end = const Offset(220.0, 100.0);
final Path actualNotch = computeNotch(host, guest, start, end);
final Path expectedNotch = new Path()
..lineTo(186.0, 100.0)
..arcToPoint(
const Offset(214.0, 100.0),
radius: const Radius.circular(14.0),
clockwise: false
)
..lineTo(220.0, 100.0);
expect(
createNotchedRectangle(host, start.dx, end.dx, actualNotch),
coversSameAreaAs(
createNotchedRectangle(host, start.dx, end.dx, expectedNotch),
areaToCompare: host.inflate(10.0)
)
);
expect(
createNotchedRectangle(host, start.dx, end.dx, actualNotch),
coversSameAreaAs(
createNotchedRectangle(host, start.dx, end.dx, expectedNotch),
areaToCompare: guest.inflate(10.0),
sampleSize: 50,
)
);
});
});
}
Path createNotchedRectangle(Rect container, double startX, double endX, Path notch) {
return new Path()
..moveTo(container.left, container.top)
..lineTo(startX, container.top)
..addPath(notch, Offset.zero)
..lineTo(container.right, container.top)
..lineTo(container.right, container.bottom)
..lineTo(container.left, container.bottom)
..close();
}
Future<ComputeNotch> fetchComputeNotch(WidgetTester tester, FloatingActionButton fab) async {
await tester.pumpWidget(new MaterialApp(
home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
),
floatingActionButton: fab,
)
));
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
return listenerState.cache.value.floatingActionButtonNotch;
}
class GeometryListener extends StatefulWidget {
@override
State createState() => new GeometryListenerState();
}
class GeometryListenerState extends State<GeometryListener> {
@override
Widget build(BuildContext context) {
return new CustomPaint(
painter: cache
);
}
ValueListenable<ScaffoldGeometry> geometryListenable;
GeometryCachePainter cache;
@override
void didChangeDependencies() {
super.didChangeDependencies();
final ValueListenable<ScaffoldGeometry> newListenable = Scaffold.geometryOf(context);
if (geometryListenable == newListenable)
return;
geometryListenable = newListenable;
cache = new GeometryCachePainter(geometryListenable);
}
}
// The Scaffold.geometryOf() value is only available at paint time.
// To fetch it for the tests we implement this CustomPainter that just
// caches the ScaffoldGeometry value in its paint method.
class GeometryCachePainter extends CustomPainter {
GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);
final ValueListenable<ScaffoldGeometry> geometryListenable;
ScaffoldGeometry value;
@override
void paint(Canvas canvas, Size size) {
value = geometryListenable.value;
}
@override
bool shouldRepaint(GeometryCachePainter oldDelegate) {
return true;
}
}
......@@ -939,6 +939,94 @@ void main() {
expect(listenerState.numNotifications, greaterThan(numNotificationsAtLastFrame));
numNotificationsAtLastFrame = listenerState.numNotifications;
});
testWidgets('set floatingActionButtonNotch', (WidgetTester tester) async {
final ComputeNotch computeNotch = (Rect container, Rect notch, Offset start, Offset end) => null;
await tester.pumpWidget(new MaterialApp(
home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
),
floatingActionButton: new ComputeNotchSetter(computeNotch),
)
));
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
ScaffoldGeometry geometry = listenerState.cache.value;
expect(
geometry.floatingActionButtonNotch,
computeNotch,
);
await tester.pumpWidget(new MaterialApp(
home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
),
)
));
await tester.pump(const Duration(seconds: 3));
geometry = listenerState.cache.value;
expect(
geometry.floatingActionButtonNotch,
null,
);
});
testWidgets('closing an inactive floatingActionButtonNotch is a no-op', (WidgetTester tester) async {
final ComputeNotch computeNotch = (Rect container, Rect notch, Offset start, Offset end) => null;
await tester.pumpWidget(new MaterialApp(
home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
),
floatingActionButton: new ComputeNotchSetter(computeNotch),
)
));
final ComputeNotchSetterState computeNotchSetterState = tester.state(find.byType(ComputeNotchSetter));
final VoidCallback clearFirstComputeNotch = computeNotchSetterState.clearComputeNotch;
final ComputeNotch computeNotch2 = (Rect container, Rect notch, Offset start, Offset end) => null;
await tester.pumpWidget(new MaterialApp(
home: new Scaffold(
body: new ConstrainedBox(
constraints: const BoxConstraints.expand(height: 80.0),
child: new GeometryListener(),
),
floatingActionButton: new ComputeNotchSetter(
computeNotch2,
// We're setting a key to make sure a new ComputeNotchSetterState is
// created.
key: new GlobalKey(),
),
)
));
await tester.pump(const Duration(seconds: 3));
// At this point the first notch maker was replaced by the second one.
// We call the clear callback for the first notch maker and verify that
// the second notch maker is still set.
clearFirstComputeNotch();
final GeometryListenerState listenerState = tester.state(find.byType(GeometryListener));
final ScaffoldGeometry geometry = listenerState.cache.value;
expect(
geometry.floatingActionButtonNotch,
computeNotch2,
);
});
});
}
......@@ -983,7 +1071,7 @@ class GeometryListenerState extends State<GeometryListener> {
// To fetch it for the tests we implement this CustomPainter that just
// caches the ScaffoldGeometry value in its paint method.
class GeometryCachePainter extends CustomPainter {
GeometryCachePainter(this.geometryListenable);
GeometryCachePainter(this.geometryListenable) : super(repaint: geometryListenable);
final ValueListenable<ScaffoldGeometry> geometryListenable;
......@@ -998,3 +1086,33 @@ class GeometryCachePainter extends CustomPainter {
return true;
}
}
class ComputeNotchSetter extends StatefulWidget {
const ComputeNotchSetter(this.computeNotch, {Key key}): super(key: key);
final ComputeNotch computeNotch;
@override
State createState() => new ComputeNotchSetterState();
}
class ComputeNotchSetterState extends State<ComputeNotchSetter> {
VoidCallback clearComputeNotch;
@override
void didChangeDependencies() {
super.didChangeDependencies();
clearComputeNotch = Scaffold.setFloatingActionButtonNotchFor(context, widget.computeNotch);
}
@override
void deactivate() {
clearComputeNotch();
super.deactivate();
}
@override
Widget build(BuildContext context) {
return new Container();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment