Unverified Commit fd7a72ee authored by Kate Lovett's avatar Kate Lovett Committed by GitHub

Ensure FloatingActionButtonLocations are always within safe interactive areas (#60248)

parent a4fa61b4
......@@ -413,14 +413,26 @@ abstract class StandardFabLocation extends FloatingActionButtonLocation {
}
/// Mixin for a "top" floating action button location, such as [FloatingActionButtonLocation.startTop].
/// Mixin for a "top" floating action button location, such as
/// [FloatingActionButtonLocation.startTop].
///
/// The [adjustment], typically [kMiniButtonOffsetAdjustment], is ignored in the
/// Y axis of "top" positions. For "top" positions, the X offset is adjusted to
/// move closer to the edge of the screen. This is so that a minified floating
/// action button appears to align with [CircleAvatar]s in the
/// [ListTile.leading] slot of a [ListTile] in a [ListView] in the
/// [Scaffold.body].
mixin FabTopOffsetY on StandardFabLocation {
/// Calculates y-offset for [FloatingActionButtonLocation]s floating over
/// the transition between the [Scaffold.appBar] and the [Scaffold.body].
@override
double getOffsetY(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) {
final double fabHalfHeight = scaffoldGeometry.floatingActionButtonSize.height / 2.0;
return scaffoldGeometry.contentTop - fabHalfHeight;
if (scaffoldGeometry.contentTop > scaffoldGeometry.minViewPadding.top) {
final double fabHalfHeight = scaffoldGeometry.floatingActionButtonSize.height / 2.0;
return scaffoldGeometry.contentTop - fabHalfHeight;
}
// Otherwise, ensure we are placed within the bounds of a safe area.
return scaffoldGeometry.minViewPadding.top;
}
}
......@@ -434,8 +446,12 @@ mixin FabFloatOffsetY on StandardFabLocation {
final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height;
final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height;
final double snackBarHeight = scaffoldGeometry.snackBarSize.height;
final double safeMargin = math.max(
kFloatingActionButtonMargin,
scaffoldGeometry.minViewPadding.bottom,
);
double fabY = contentBottom - fabHeight - kFloatingActionButtonMargin;
double fabY = contentBottom - fabHeight - safeMargin;
if (snackBarHeight > 0.0)
fabY = math.min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin);
if (bottomSheetHeight > 0.0)
......@@ -453,19 +469,21 @@ mixin FabDockedOffsetY on StandardFabLocation {
@override
double getOffsetY(ScaffoldPrelayoutGeometry scaffoldGeometry, double adjustment) {
final double contentBottom = scaffoldGeometry.contentBottom;
final double contentMargin = scaffoldGeometry.scaffoldSize.height - contentBottom;
final double bottomViewPadding = scaffoldGeometry.minViewPadding.bottom;
final double bottomSheetHeight = scaffoldGeometry.bottomSheetSize.height;
final double fabHeight = scaffoldGeometry.floatingActionButtonSize.height;
final double snackBarHeight = scaffoldGeometry.snackBarSize.height;
final double safeMargin = bottomViewPadding > contentMargin ? bottomViewPadding : 0.0;
double fabY = contentBottom - fabHeight / 2.0;
double fabY = contentBottom - fabHeight / 2.0 - safeMargin;
// The FAB should sit with a margin between it and the snack bar.
if (snackBarHeight > 0.0)
fabY = math.min(fabY, contentBottom - snackBarHeight - fabHeight - kFloatingActionButtonMargin);
// The FAB should sit with its center in front of the top of the bottom sheet.
if (bottomSheetHeight > 0.0)
fabY = math.min(fabY, contentBottom - bottomSheetHeight - fabHeight / 2.0);
final double maxFabY = scaffoldGeometry.scaffoldSize.height - fabHeight;
final double maxFabY = scaffoldGeometry.scaffoldSize.height - fabHeight - safeMargin;
return math.min(maxFabY, fabY);
}
}
......
......@@ -80,6 +80,7 @@ class ScaffoldPrelayoutGeometry {
@required this.contentTop,
@required this.floatingActionButtonSize,
@required this.minInsets,
@required this.minViewPadding,
@required this.scaffoldSize,
@required this.snackBarSize,
@required this.textDirection,
......@@ -134,6 +135,16 @@ class ScaffoldPrelayoutGeometry {
/// will be 0.0.
final EdgeInsets minInsets;
/// The minimum padding to inset interactive elements to be within a safe,
/// un-obscured space.
///
/// This value reflects the [MediaQuery.viewPadding] of the [Scaffold]'s
/// [BuildContext] when [Scaffold.resizeToAvoidBottomInset] is false or and
/// the [MediaQuery.viewInsets] > 0.0. This helps distinguish between
/// different types of obstructions on the screen, such as software keyboards
/// and physical device notches.
final EdgeInsets minViewPadding;
/// The [Size] of the whole [Scaffold].
///
/// If the [Size] of the [Scaffold]'s contents is modified by values such as
......@@ -389,6 +400,7 @@ class _BodyBuilder extends StatelessWidget {
class _ScaffoldLayout extends MultiChildLayoutDelegate {
_ScaffoldLayout({
@required this.minInsets,
@required this.minViewPadding,
@required this.textDirection,
@required this.geometryNotifier,
// for floating action button
......@@ -410,6 +422,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
final bool extendBody;
final bool extendBodyBehindAppBar;
final EdgeInsets minInsets;
final EdgeInsets minViewPadding;
final TextDirection textDirection;
final _ScaffoldGeometryNotifier geometryNotifier;
......@@ -536,6 +549,7 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
scaffoldSize: size,
snackBarSize: snackBarSize,
textDirection: textDirection,
minViewPadding: minViewPadding,
);
final Offset currentFabOffset = currentFloatingActionButtonLocation.getOffset(currentGeometry);
final Offset previousFabOffset = previousFloatingActionButtonLocation.getOffset(currentGeometry);
......@@ -2497,6 +2511,12 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
bottom: _resizeToAvoidBottomInset ? mediaQuery.viewInsets.bottom : 0.0,
);
// The minimum viewPadding for interactive elements positioned by the
// Scaffold to keep within safe interactive areas.
final EdgeInsets minViewPadding = mediaQuery.viewPadding.copyWith(
bottom: _resizeToAvoidBottomInset && mediaQuery.viewInsets.bottom != 0.0 ? 0.0 : null,
);
// extendBody locked when keyboard is open
final bool _extendBody = minInsets.bottom <= 0 && widget.extendBody;
......@@ -2514,6 +2534,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
extendBody: _extendBody,
extendBodyBehindAppBar: widget.extendBodyBehindAppBar,
minInsets: minInsets,
minViewPadding: minViewPadding,
currentFloatingActionButtonLocation: _floatingActionButtonLocation,
floatingActionButtonMoveAnimationProgress: _floatingActionButtonMoveController.value,
floatingActionButtonMotionAnimator: _floatingActionButtonAnimator,
......
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