Fix floating SnackBar throws when FAB is on the top (#129274)
## Description This PR updates how a floating snack bar is positionned when a `Scaffold` defines a FAB with `Scaffold.floatingActionButtonLocation` sets to one of the top locations. **Before this PR:** - When a FAB location is set to the top of the `Scaffold`, a floating `SnackBar` can't be displayed and an assert throws in debug mode. **After this PR:** - When a FAB location is set to the top of the `Scaffold`, a floating `SnackBar` will be displayed at the bottom of the screen, above a `NavigationBar` for instance (the top FAB is ignored when computing the floating snack bar position).  ## Motivation This is a edge case related to a discrepancy between the Material spec and the Flutter `Scaffold` customizability: - Material spec states that a floating `SnackBar` should be displayed above a FAB. But, in Material spec, FABs are expected to be on the bottom. - Since https://github.com/flutter/flutter/issues/51465, Flutter `Scaffold` makes it valid to show a FAB on the top of the `Scaffold`. ## Related Issue fixes https://github.com/flutter/flutter/issues/128150 ## Tests Adds 1 test.
This commit is contained in:
parent
283437a2d2
commit
97e0a0589e
@ -1142,7 +1142,31 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final double snackBarYOffsetBase;
|
final double snackBarYOffsetBase;
|
||||||
if (floatingActionButtonRect.size != Size.zero && isSnackBarFloating) {
|
final bool showAboveFab = switch (currentFloatingActionButtonLocation) {
|
||||||
|
FloatingActionButtonLocation.startTop
|
||||||
|
|| FloatingActionButtonLocation.centerTop
|
||||||
|
|| FloatingActionButtonLocation.endTop
|
||||||
|
|| FloatingActionButtonLocation.miniStartTop
|
||||||
|
|| FloatingActionButtonLocation.miniCenterTop
|
||||||
|
|| FloatingActionButtonLocation.miniEndTop => false,
|
||||||
|
FloatingActionButtonLocation.startDocked
|
||||||
|
|| FloatingActionButtonLocation.startFloat
|
||||||
|
|| FloatingActionButtonLocation.centerDocked
|
||||||
|
|| FloatingActionButtonLocation.centerFloat
|
||||||
|
|| FloatingActionButtonLocation.endContained
|
||||||
|
|| FloatingActionButtonLocation.endDocked
|
||||||
|
|| FloatingActionButtonLocation.endFloat
|
||||||
|
|| FloatingActionButtonLocation.miniStartDocked
|
||||||
|
|| FloatingActionButtonLocation.miniStartFloat
|
||||||
|
|| FloatingActionButtonLocation.miniCenterDocked
|
||||||
|
|| FloatingActionButtonLocation.miniCenterFloat
|
||||||
|
|| FloatingActionButtonLocation.miniEndDocked
|
||||||
|
|| FloatingActionButtonLocation.miniEndFloat => true,
|
||||||
|
FloatingActionButtonLocation() => throw FlutterError(
|
||||||
|
'$currentFloatingActionButtonLocation is an unknown FloatingActionButtonLocation value.'
|
||||||
|
),
|
||||||
|
};
|
||||||
|
if (floatingActionButtonRect.size != Size.zero && isSnackBarFloating && showAboveFab) {
|
||||||
snackBarYOffsetBase = floatingActionButtonRect.top;
|
snackBarYOffsetBase = floatingActionButtonRect.top;
|
||||||
} else {
|
} else {
|
||||||
// SnackBarBehavior.fixed applies a SafeArea automatically.
|
// SnackBarBehavior.fixed applies a SafeArea automatically.
|
||||||
|
@ -12,19 +12,22 @@ import 'theme.dart';
|
|||||||
|
|
||||||
/// Defines where a [SnackBar] should appear within a [Scaffold] and how its
|
/// Defines where a [SnackBar] should appear within a [Scaffold] and how its
|
||||||
/// location should be adjusted when the scaffold also includes a
|
/// location should be adjusted when the scaffold also includes a
|
||||||
/// [FloatingActionButton] or a [BottomNavigationBar].
|
/// [FloatingActionButton], a [BottomNavigationBar], or a [NavigationBar].
|
||||||
enum SnackBarBehavior {
|
enum SnackBarBehavior {
|
||||||
/// Fixes the [SnackBar] at the bottom of the [Scaffold].
|
/// Fixes the [SnackBar] at the bottom of the [Scaffold].
|
||||||
///
|
///
|
||||||
/// The exception is that the [SnackBar] will be shown above a
|
/// The exception is that the [SnackBar] will be shown above a
|
||||||
/// [BottomNavigationBar]. Additionally, the [SnackBar] will cause other
|
/// [BottomNavigationBar] or a [NavigationBar]. Additionally, the [SnackBar]
|
||||||
/// non-fixed widgets inside [Scaffold] to be pushed above (for example, the
|
/// will cause other non-fixed widgets inside [Scaffold] to be pushed above
|
||||||
/// [FloatingActionButton]).
|
/// (for example, the [FloatingActionButton]).
|
||||||
fixed,
|
fixed,
|
||||||
|
|
||||||
/// This behavior will cause [SnackBar] to be shown above other widgets in the
|
/// This behavior will cause [SnackBar] to be shown above other widgets in the
|
||||||
/// [Scaffold]. This includes being displayed above a [BottomNavigationBar]
|
/// [Scaffold]. This includes being displayed above a [BottomNavigationBar] or
|
||||||
/// and a [FloatingActionButton].
|
/// a [NavigationBar], and a [FloatingActionButton] when its location is on the
|
||||||
|
/// bottom. When the floating action button location is on the top, this behavior
|
||||||
|
/// will cause the [SnackBar] to be shown above other widgets in the [Scaffold]
|
||||||
|
/// except the floating action button.
|
||||||
///
|
///
|
||||||
/// See <https://material.io/design/components/snackbars.html> for more details.
|
/// See <https://material.io/design/components/snackbars.html> for more details.
|
||||||
floating,
|
floating,
|
||||||
|
@ -2071,6 +2071,59 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'${SnackBarBehavior.floating} should not align SnackBar with the top of FloatingActionButton '
|
||||||
|
'when Scaffold has a FloatingActionButton and floatingActionButtonLocation is set to a top position',
|
||||||
|
(WidgetTester tester) async {
|
||||||
|
Future<void> pumpApp({required FloatingActionButtonLocation fabLocation}) async {
|
||||||
|
return tester.pumpWidget(MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
child: const Icon(Icons.send),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
floatingActionButtonLocation: fabLocation,
|
||||||
|
body: Builder(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||||
|
content: const Text('I am a snack bar.'),
|
||||||
|
duration: const Duration(seconds: 2),
|
||||||
|
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
));
|
||||||
|
},
|
||||||
|
child: const Text('X'),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const List<FloatingActionButtonLocation> topLocations = <FloatingActionButtonLocation>[
|
||||||
|
FloatingActionButtonLocation.startTop,
|
||||||
|
FloatingActionButtonLocation.centerTop,
|
||||||
|
FloatingActionButtonLocation.endTop,
|
||||||
|
FloatingActionButtonLocation.miniStartTop,
|
||||||
|
FloatingActionButtonLocation.miniCenterTop,
|
||||||
|
FloatingActionButtonLocation.miniEndTop,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final FloatingActionButtonLocation location in topLocations) {
|
||||||
|
await pumpApp(fabLocation: location);
|
||||||
|
|
||||||
|
await tester.tap(find.text('X'));
|
||||||
|
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
|
||||||
|
|
||||||
|
final Offset snackBarBottomLeft = tester.getBottomLeft(find.byType(SnackBar));
|
||||||
|
|
||||||
|
expect(snackBarBottomLeft.dy, 600); // Device height is 600.
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
testWidgets(
|
testWidgets(
|
||||||
'${SnackBarBehavior.fixed} should align SnackBar with the top of BottomNavigationBar '
|
'${SnackBarBehavior.fixed} should align SnackBar with the top of BottomNavigationBar '
|
||||||
'when Scaffold has a BottomNavigationBar and FloatingActionButton',
|
'when Scaffold has a BottomNavigationBar and FloatingActionButton',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user