Unverified Commit 7b6af17f authored by Bruno Leroux's avatar Bruno Leroux Committed by GitHub

Reland - Fix floating SnackBar throws when FAB is on the top (#131475)

## Description

This PR is a reland of https://github.com/flutter/flutter/pull/129274 with a fix and new test related to the revert (https://github.com/flutter/flutter/pull/131303).

It 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).

![image](https://github.com/flutter/flutter/assets/840911/08fcee6c-b286-4749-ad0b-ba09e653bd94)

## 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 2 tests.
parent 3396ec7b
......@@ -1142,7 +1142,29 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
}
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() => true,
};
if (floatingActionButtonRect.size != Size.zero && isSnackBarFloating && showAboveFab) {
snackBarYOffsetBase = floatingActionButtonRect.top;
} else {
// SnackBarBehavior.fixed applies a SafeArea automatically.
......
......@@ -17,14 +17,17 @@ enum SnackBarBehavior {
/// Fixes the [SnackBar] at the bottom of the [Scaffold].
///
/// The exception is that the [SnackBar] will be shown above a
/// [BottomNavigationBar]. Additionally, the [SnackBar] will cause other
/// non-fixed widgets inside [Scaffold] to be pushed above (for example, the
/// [FloatingActionButton]).
/// [BottomNavigationBar] or a [NavigationBar]. Additionally, the [SnackBar]
/// will cause other non-fixed widgets inside [Scaffold] to be pushed above
/// (for example, the [FloatingActionButton]).
fixed,
/// This behavior will cause [SnackBar] to be shown above other widgets in the
/// [Scaffold]. This includes being displayed above a [BottomNavigationBar]
/// and a [FloatingActionButton].
/// [Scaffold]. This includes being displayed above a [BottomNavigationBar] or
/// 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.
floating,
......
......@@ -2071,6 +2071,127 @@ 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(
'${SnackBarBehavior.floating} should align SnackBar with the top of FloatingActionButton '
'when Scaffold has a FloatingActionButton and floatingActionButtonLocation is not 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> nonTopLocations = <FloatingActionButtonLocation>[
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,
// Regression test related to https://github.com/flutter/flutter/pull/131303.
_CustomFloatingActionButtonLocation(),
];
for (final FloatingActionButtonLocation location in nonTopLocations) {
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));
final Offset floatingActionButtonTopLeft = tester.getTopLeft(
find.byType(FloatingActionButton),
);
// Since padding between the SnackBar and the FAB is created by the SnackBar,
// the bottom offset of the SnackBar should be equal to the top offset of the FAB
expect(snackBarBottomLeft.dy, floatingActionButtonTopLeft.dy);
}
},
);
testWidgets(
'${SnackBarBehavior.fixed} should align SnackBar with the top of BottomNavigationBar '
'when Scaffold has a BottomNavigationBar and FloatingActionButton',
......@@ -3749,3 +3870,8 @@ class _TestMaterialStateColor extends MaterialStateColor {
return const Color(_colorRed);
}
}
class _CustomFloatingActionButtonLocation extends StandardFabLocation
with FabEndOffsetX, FabFloatOffsetY {
const _CustomFloatingActionButtonLocation();
}
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