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
This commit is contained in:
parent
ca677011f8
commit
7996d7fb82
@ -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';
|
||||
|
146
packages/flutter/lib/src/material/bottom_app_bar.dart
Normal file
146
packages/flutter/lib/src/material/bottom_app_bar.dart
Normal file
@ -0,0 +1,146 @@
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user