Skip to content

Commit 7b6af17

Browse files
authored
Reland - Fix floating SnackBar throws when FAB is on the top (flutter#131475)
## Description This PR is a reland of flutter#129274 with a fix and new test related to the revert (flutter#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 flutter#51465, Flutter `Scaffold` makes it valid to show a FAB on the top of the `Scaffold`. ## Related Issue fixes flutter#128150 ## Tests Adds 2 tests.
1 parent 3396ec7 commit 7b6af17

File tree

3 files changed

+157
-6
lines changed

3 files changed

+157
-6
lines changed

packages/flutter/lib/src/material/scaffold.dart

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1142,7 +1142,29 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
11421142
}
11431143

11441144
final double snackBarYOffsetBase;
1145-
if (floatingActionButtonRect.size != Size.zero && isSnackBarFloating) {
1145+
final bool showAboveFab = switch (currentFloatingActionButtonLocation) {
1146+
FloatingActionButtonLocation.startTop
1147+
|| FloatingActionButtonLocation.centerTop
1148+
|| FloatingActionButtonLocation.endTop
1149+
|| FloatingActionButtonLocation.miniStartTop
1150+
|| FloatingActionButtonLocation.miniCenterTop
1151+
|| FloatingActionButtonLocation.miniEndTop => false,
1152+
FloatingActionButtonLocation.startDocked
1153+
|| FloatingActionButtonLocation.startFloat
1154+
|| FloatingActionButtonLocation.centerDocked
1155+
|| FloatingActionButtonLocation.centerFloat
1156+
|| FloatingActionButtonLocation.endContained
1157+
|| FloatingActionButtonLocation.endDocked
1158+
|| FloatingActionButtonLocation.endFloat
1159+
|| FloatingActionButtonLocation.miniStartDocked
1160+
|| FloatingActionButtonLocation.miniStartFloat
1161+
|| FloatingActionButtonLocation.miniCenterDocked
1162+
|| FloatingActionButtonLocation.miniCenterFloat
1163+
|| FloatingActionButtonLocation.miniEndDocked
1164+
|| FloatingActionButtonLocation.miniEndFloat => true,
1165+
FloatingActionButtonLocation() => true,
1166+
};
1167+
if (floatingActionButtonRect.size != Size.zero && isSnackBarFloating && showAboveFab) {
11461168
snackBarYOffsetBase = floatingActionButtonRect.top;
11471169
} else {
11481170
// SnackBarBehavior.fixed applies a SafeArea automatically.

packages/flutter/lib/src/material/snack_bar_theme.dart

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,17 @@ enum SnackBarBehavior {
1717
/// Fixes the [SnackBar] at the bottom of the [Scaffold].
1818
///
1919
/// The exception is that the [SnackBar] will be shown above a
20-
/// [BottomNavigationBar]. Additionally, the [SnackBar] will cause other
21-
/// non-fixed widgets inside [Scaffold] to be pushed above (for example, the
22-
/// [FloatingActionButton]).
20+
/// [BottomNavigationBar] or a [NavigationBar]. Additionally, the [SnackBar]
21+
/// will cause other non-fixed widgets inside [Scaffold] to be pushed above
22+
/// (for example, the [FloatingActionButton]).
2323
fixed,
2424

2525
/// This behavior will cause [SnackBar] to be shown above other widgets in the
26-
/// [Scaffold]. This includes being displayed above a [BottomNavigationBar]
27-
/// and a [FloatingActionButton].
26+
/// [Scaffold]. This includes being displayed above a [BottomNavigationBar] or
27+
/// a [NavigationBar], and a [FloatingActionButton] when its location is on the
28+
/// bottom. When the floating action button location is on the top, this behavior
29+
/// will cause the [SnackBar] to be shown above other widgets in the [Scaffold]
30+
/// except the floating action button.
2831
///
2932
/// See <https://material.io/design/components/snackbars.html> for more details.
3033
floating,

packages/flutter/test/material/snack_bar_test.dart

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2071,6 +2071,127 @@ void main() {
20712071
},
20722072
);
20732073

2074+
testWidgets(
2075+
'${SnackBarBehavior.floating} should not align SnackBar with the top of FloatingActionButton '
2076+
'when Scaffold has a FloatingActionButton and floatingActionButtonLocation is set to a top position',
2077+
(WidgetTester tester) async {
2078+
Future<void> pumpApp({required FloatingActionButtonLocation fabLocation}) async {
2079+
return tester.pumpWidget(MaterialApp(
2080+
home: Scaffold(
2081+
floatingActionButton: FloatingActionButton(
2082+
child: const Icon(Icons.send),
2083+
onPressed: () {},
2084+
),
2085+
floatingActionButtonLocation: fabLocation,
2086+
body: Builder(
2087+
builder: (BuildContext context) {
2088+
return GestureDetector(
2089+
onTap: () {
2090+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
2091+
content: const Text('I am a snack bar.'),
2092+
duration: const Duration(seconds: 2),
2093+
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
2094+
behavior: SnackBarBehavior.floating,
2095+
));
2096+
},
2097+
child: const Text('X'),
2098+
);
2099+
},
2100+
),
2101+
),
2102+
));
2103+
}
2104+
2105+
const List<FloatingActionButtonLocation> topLocations = <FloatingActionButtonLocation>[
2106+
FloatingActionButtonLocation.startTop,
2107+
FloatingActionButtonLocation.centerTop,
2108+
FloatingActionButtonLocation.endTop,
2109+
FloatingActionButtonLocation.miniStartTop,
2110+
FloatingActionButtonLocation.miniCenterTop,
2111+
FloatingActionButtonLocation.miniEndTop,
2112+
];
2113+
2114+
for (final FloatingActionButtonLocation location in topLocations) {
2115+
await pumpApp(fabLocation: location);
2116+
2117+
await tester.tap(find.text('X'));
2118+
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
2119+
2120+
final Offset snackBarBottomLeft = tester.getBottomLeft(find.byType(SnackBar));
2121+
2122+
expect(snackBarBottomLeft.dy, 600); // Device height is 600.
2123+
}
2124+
},
2125+
);
2126+
2127+
testWidgets(
2128+
'${SnackBarBehavior.floating} should align SnackBar with the top of FloatingActionButton '
2129+
'when Scaffold has a FloatingActionButton and floatingActionButtonLocation is not set to a top position',
2130+
(WidgetTester tester) async {
2131+
Future<void> pumpApp({required FloatingActionButtonLocation fabLocation}) async {
2132+
return tester.pumpWidget(MaterialApp(
2133+
home: Scaffold(
2134+
floatingActionButton: FloatingActionButton(
2135+
child: const Icon(Icons.send),
2136+
onPressed: () {},
2137+
),
2138+
floatingActionButtonLocation: fabLocation,
2139+
body: Builder(
2140+
builder: (BuildContext context) {
2141+
return GestureDetector(
2142+
onTap: () {
2143+
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
2144+
content: const Text('I am a snack bar.'),
2145+
duration: const Duration(seconds: 2),
2146+
action: SnackBarAction(label: 'ACTION', onPressed: () {}),
2147+
behavior: SnackBarBehavior.floating,
2148+
));
2149+
},
2150+
child: const Text('X'),
2151+
);
2152+
},
2153+
),
2154+
),
2155+
));
2156+
}
2157+
2158+
const List<FloatingActionButtonLocation> nonTopLocations = <FloatingActionButtonLocation>[
2159+
FloatingActionButtonLocation.startDocked,
2160+
FloatingActionButtonLocation.startFloat,
2161+
FloatingActionButtonLocation.centerDocked,
2162+
FloatingActionButtonLocation.centerFloat,
2163+
FloatingActionButtonLocation.endContained,
2164+
FloatingActionButtonLocation.endDocked,
2165+
FloatingActionButtonLocation.endFloat,
2166+
FloatingActionButtonLocation.miniStartDocked,
2167+
FloatingActionButtonLocation.miniStartFloat,
2168+
FloatingActionButtonLocation.miniCenterDocked,
2169+
FloatingActionButtonLocation.miniCenterFloat,
2170+
FloatingActionButtonLocation.miniEndDocked,
2171+
FloatingActionButtonLocation.miniEndFloat,
2172+
// Regression test related to https://github.com/flutter/flutter/pull/131303.
2173+
_CustomFloatingActionButtonLocation(),
2174+
];
2175+
2176+
2177+
for (final FloatingActionButtonLocation location in nonTopLocations) {
2178+
await pumpApp(fabLocation: location);
2179+
2180+
await tester.tap(find.text('X'));
2181+
await tester.pumpAndSettle(); // Have the SnackBar fully animate out.
2182+
2183+
final Offset snackBarBottomLeft = tester.getBottomLeft(find.byType(SnackBar));
2184+
final Offset floatingActionButtonTopLeft = tester.getTopLeft(
2185+
find.byType(FloatingActionButton),
2186+
);
2187+
2188+
// Since padding between the SnackBar and the FAB is created by the SnackBar,
2189+
// the bottom offset of the SnackBar should be equal to the top offset of the FAB
2190+
expect(snackBarBottomLeft.dy, floatingActionButtonTopLeft.dy);
2191+
}
2192+
},
2193+
);
2194+
20742195
testWidgets(
20752196
'${SnackBarBehavior.fixed} should align SnackBar with the top of BottomNavigationBar '
20762197
'when Scaffold has a BottomNavigationBar and FloatingActionButton',
@@ -3749,3 +3870,8 @@ class _TestMaterialStateColor extends MaterialStateColor {
37493870
return const Color(_colorRed);
37503871
}
37513872
}
3873+
3874+
class _CustomFloatingActionButtonLocation extends StandardFabLocation
3875+
with FabEndOffsetX, FabFloatOffsetY {
3876+
const _CustomFloatingActionButtonLocation();
3877+
}

0 commit comments

Comments
 (0)