From 1e78c47bc4087dab56d013d594a68794de4c3856 Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Mon, 24 Dec 2018 15:22:13 -0800 Subject: [PATCH] Provide some more locations for the FAB. (#24736) Top left and top right for big FABs, and top left for mini FABs. --- .../src/material/floating_action_button.dart | 12 +- .../floating_action_button_location.dart | 167 ++++++++++++++---- .../flutter/lib/src/material/scaffold.dart | 7 +- .../floating_action_button_location_test.dart | 79 +++++++++ 4 files changed, 218 insertions(+), 47 deletions(-) diff --git a/packages/flutter/lib/src/material/floating_action_button.dart b/packages/flutter/lib/src/material/floating_action_button.dart index 061d678d02..bfe8c1e434 100644 --- a/packages/flutter/lib/src/material/floating_action_button.dart +++ b/packages/flutter/lib/src/material/floating_action_button.dart @@ -50,10 +50,10 @@ class _DefaultHeroTag { /// /// See also: /// -/// * [Scaffold] -/// * [RaisedButton] -/// * [FlatButton] -/// * +/// * [Scaffold], in which floating action buttons typically live. +/// * [RaisedButton], another kind of button that appears to float above the +/// content. +/// * class FloatingActionButton extends StatefulWidget { /// Creates a circular floating action button. /// @@ -192,7 +192,9 @@ class FloatingActionButton extends StatefulWidget { /// By default, floating action buttons are non-mini and have a height and /// width of 56.0 logical pixels. Mini floating action buttons have a height /// and width of 40.0 logical pixels with a layout width and height of 48.0 - /// logical pixels. + /// logical pixels. (The extra 4 pixels of padding on each side are added as a + /// result of the floating action button having [MaterialTapTargetSize.padded] + /// set on the underlying [RawMaterialButton.materialTapTargetSize].) final bool mini; /// The shape of the button's [Material]. diff --git a/packages/flutter/lib/src/material/floating_action_button_location.dart b/packages/flutter/lib/src/material/floating_action_button_location.dart index 3da2248ca2..7a744de6c8 100644 --- a/packages/flutter/lib/src/material/floating_action_button_location.dart +++ b/packages/flutter/lib/src/material/floating_action_button_location.dart @@ -51,10 +51,10 @@ abstract class FloatingActionButtonLocation { /// End-aligned [FloatingActionButton], floating at the bottom of the screen. /// /// This is the default alignment of [FloatingActionButton]s in Material applications. - static const FloatingActionButtonLocation endFloat = _EndFloatFabLocation(); + static const FloatingActionButtonLocation endFloat = _EndFloatFloatingActionButtonLocation(); /// Centered [FloatingActionButton], floating at the bottom of the screen. - static const FloatingActionButtonLocation centerFloat = _CenterFloatFabLocation(); + static const FloatingActionButtonLocation centerFloat = _CenterFloatFloatingActionButtonLocation(); /// End-aligned [FloatingActionButton], floating over the /// [Scaffold.bottomNavigationBar] so that the center of the floating @@ -80,6 +80,37 @@ abstract class FloatingActionButtonLocation { /// navigation bar. static const FloatingActionButtonLocation centerDocked = _CenterDockedFloatingActionButtonLocation(); + /// Start-aligned [FloatingActionButton], floating over the transition between + /// the [Scaffold.appBar] and the [Scaffold.body]. + /// + /// To align a floating action button with [FloatingActionButton.mini] set to + /// true with [CircleAvatar]s in the [ListTile.leading] slots of [ListTile]s + /// in a [ListView] in the [Scaffold.body], consider using [miniStartTop]. + /// + /// This is unlikely to be a useful location for apps that lack a top [AppBar] + /// or that use a [SliverAppBar] in the scaffold body itself. + static const FloatingActionButtonLocation startTop = _StartTopFloatingActionButtonLocation(); + + /// Start-aligned [FloatingActionButton], floating over the transition between + /// the [Scaffold.appBar] and the [Scaffold.body], optimized for mini floating + /// action buttons. + /// + /// This is intended to be used with [FloatingActionButton.mini] set to true, + /// so that the floating action button appears to align with [CircleAvatar]s + /// in the [ListTile.leading] slot of a [ListTile] in a [ListView] in the + /// [Scaffold.body]. + /// + /// This is unlikely to be a useful location for apps that lack a top [AppBar] + /// or that use a [SliverAppBar] in the scaffold body itself. + static const FloatingActionButtonLocation miniStartTop = _MiniStartTopFloatingActionButtonLocation(); + + /// End-aligned [FloatingActionButton], floating over the transition between + /// the [Scaffold.appBar] and the [Scaffold.body]. + /// + /// This is unlikely to be a useful location for apps that lack a top [AppBar] + /// or that use a [SliverAppBar] in the scaffold body itself. + static const FloatingActionButtonLocation endTop = _EndTopFloatingActionButtonLocation(); + /// Places the [FloatingActionButton] based on the [Scaffold]'s layout. /// /// This uses a [ScaffoldPrelayoutGeometry], which the [Scaffold] constructs @@ -93,8 +124,44 @@ abstract class FloatingActionButtonLocation { String toString() => '$runtimeType'; } -class _CenterFloatFabLocation extends FloatingActionButtonLocation { - const _CenterFloatFabLocation(); +double _leftOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) { + return kFloatingActionButtonMargin + + scaffoldGeometry.minInsets.left + - offset; +} + +double _rightOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) { + return scaffoldGeometry.scaffoldSize.width + - kFloatingActionButtonMargin + - scaffoldGeometry.minInsets.right + - scaffoldGeometry.floatingActionButtonSize.width + + offset; +} + +double _endOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) { + assert(scaffoldGeometry.textDirection != null); + switch (scaffoldGeometry.textDirection) { + case TextDirection.rtl: + return _leftOffset(scaffoldGeometry, offset: offset); + case TextDirection.ltr: + return _rightOffset(scaffoldGeometry, offset: offset); + } + return null; +} + +double _startOffset(ScaffoldPrelayoutGeometry scaffoldGeometry, { double offset = 0.0 }) { + assert(scaffoldGeometry.textDirection != null); + switch (scaffoldGeometry.textDirection) { + case TextDirection.rtl: + return _rightOffset(scaffoldGeometry, offset: offset); + case TextDirection.ltr: + return _leftOffset(scaffoldGeometry, offset: offset); + } + return null; +} + +class _CenterFloatFloatingActionButtonLocation extends FloatingActionButtonLocation { + const _CenterFloatFloatingActionButtonLocation(); @override Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { @@ -114,28 +181,18 @@ class _CenterFloatFabLocation extends FloatingActionButtonLocation { return Offset(fabX, fabY); } + + @override + String toString() => 'FloatingActionButtonLocation.centerFloat'; } -class _EndFloatFabLocation extends FloatingActionButtonLocation { - const _EndFloatFabLocation(); +class _EndFloatFloatingActionButtonLocation extends FloatingActionButtonLocation { + const _EndFloatFloatingActionButtonLocation(); @override Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { // Compute the x-axis offset. - double fabX; - assert(scaffoldGeometry.textDirection != null); - switch (scaffoldGeometry.textDirection) { - case TextDirection.rtl: - // In RTL, the end of the screen is the left. - final double endPadding = scaffoldGeometry.minInsets.left; - fabX = kFloatingActionButtonMargin + endPadding; - break; - case TextDirection.ltr: - // In LTR, the end of the screen is the right. - final double endPadding = scaffoldGeometry.minInsets.right; - fabX = scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width - kFloatingActionButtonMargin - endPadding; - break; - } + final double fabX = _endOffset(scaffoldGeometry); // Compute the y-axis offset. final double contentBottom = scaffoldGeometry.contentBottom; @@ -151,6 +208,9 @@ class _EndFloatFabLocation extends FloatingActionButtonLocation { return Offset(fabX, fabY); } + + @override + String toString() => 'FloatingActionButtonLocation.endFloat'; } // Provider of common logic for [FloatingActionButtonLocation]s that @@ -185,24 +245,12 @@ class _EndDockedFloatingActionButtonLocation extends _DockedFloatingActionButton @override Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { - // Compute the x-axis offset. - double fabX; - assert(scaffoldGeometry.textDirection != null); - switch (scaffoldGeometry.textDirection) { - case TextDirection.rtl: - // In RTL, the end of the screen is the left. - final double endPadding = scaffoldGeometry.minInsets.left; - fabX = kFloatingActionButtonMargin + endPadding; - break; - case TextDirection.ltr: - // In LTR, the end of the screen is the right. - final double endPadding = scaffoldGeometry.minInsets.right; - fabX = scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width - kFloatingActionButtonMargin - endPadding; - break; - } - // Return an offset with a docked Y coordinate. + final double fabX = _endOffset(scaffoldGeometry); return Offset(fabX, getDockedY(scaffoldGeometry)); } + + @override + String toString() => 'FloatingActionButtonLocation.endDocked'; } class _CenterDockedFloatingActionButtonLocation extends _DockedFloatingActionButtonLocation { @@ -213,6 +261,53 @@ class _CenterDockedFloatingActionButtonLocation extends _DockedFloatingActionBut final double fabX = (scaffoldGeometry.scaffoldSize.width - scaffoldGeometry.floatingActionButtonSize.width) / 2.0; return Offset(fabX, getDockedY(scaffoldGeometry)); } + + @override + String toString() => 'FloatingActionButtonLocation.centerDocked'; +} + +double _straddleAppBar(ScaffoldPrelayoutGeometry scaffoldGeometry) { + final double fabHalfHeight = scaffoldGeometry.floatingActionButtonSize.height / 2.0; + return scaffoldGeometry.contentTop - fabHalfHeight; +} + +class _StartTopFloatingActionButtonLocation extends FloatingActionButtonLocation { + const _StartTopFloatingActionButtonLocation(); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + return Offset(_startOffset(scaffoldGeometry), _straddleAppBar(scaffoldGeometry)); + } + + @override + String toString() => 'FloatingActionButtonLocation.startTop'; +} + +class _MiniStartTopFloatingActionButtonLocation extends FloatingActionButtonLocation { + const _MiniStartTopFloatingActionButtonLocation(); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + // We have to offset the FAB by four pixels because the FAB itself _adds_ + // four pixels in every direction in order to have a hit target area of 48 + // pixels in each dimension, despite being a circle of radius 40. + return Offset(_startOffset(scaffoldGeometry, offset: 4.0), _straddleAppBar(scaffoldGeometry)); + } + + @override + String toString() => 'FloatingActionButtonLocation.miniStartTop'; +} + +class _EndTopFloatingActionButtonLocation extends FloatingActionButtonLocation { + const _EndTopFloatingActionButtonLocation(); + + @override + Offset getOffset(ScaffoldPrelayoutGeometry scaffoldGeometry) { + return Offset(_endOffset(scaffoldGeometry), _straddleAppBar(scaffoldGeometry)); + } + + @override + String toString() => 'FloatingActionButtonLocation.endTop'; } /// Provider of animations to move the [FloatingActionButton] between [FloatingActionButtonLocation]s. diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart index 63b3c2defc..812ea2e6d6 100644 --- a/packages/flutter/lib/src/material/scaffold.dart +++ b/packages/flutter/lib/src/material/scaffold.dart @@ -701,11 +701,6 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr /// 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. -/// * [FloatingActionButtonLocation], which is used to place the -/// [floatingActionButton] within the [Scaffold]'s layout. -/// * [FloatingActionButtonAnimator], which is used to animate the -/// [floatingActionButton] from one [floatingActionButtonLocation] to -/// another. /// * [Drawer], which is a vertical panel that is typically displayed to the /// left of the body (and often hidden on phones) using the [drawer] /// property. @@ -719,7 +714,7 @@ class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTr /// using the [ScaffoldState.showBottomSheet] method, or modal, in which case /// it is shown using the [showModalBottomSheet] function. /// * [ScaffoldState], which is the state associated with this widget. -/// * +/// * class Scaffold extends StatefulWidget { /// Creates a visual scaffold for material design widgets. const Scaffold({ diff --git a/packages/flutter/test/material/floating_action_button_location_test.dart b/packages/flutter/test/material/floating_action_button_location_test.dart index b67bf7ac3e..6ea05a9963 100644 --- a/packages/flutter/test/material/floating_action_button_location_test.dart +++ b/packages/flutter/test/material/floating_action_button_location_test.dart @@ -171,6 +171,85 @@ void main() { ); expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 572.0)); }); + + testWidgets('Mini-start-top floating action button location', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar(), + floatingActionButton: FloatingActionButton(onPressed: () { }, mini: true), + floatingActionButtonLocation: FloatingActionButtonLocation.miniStartTop, + body: Column( + children: const [ + ListTile( + leading: CircleAvatar(), + ), + ], + ), + ), + ), + ); + expect(tester.getCenter(find.byType(FloatingActionButton)).dx, tester.getCenter(find.byType(CircleAvatar)).dx); + expect(tester.getCenter(find.byType(FloatingActionButton)).dy, kToolbarHeight); + }); + + testWidgets('Start-top floating action button location LTR', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar(), + floatingActionButton: const FloatingActionButton(onPressed: null), + floatingActionButtonLocation: FloatingActionButtonLocation.startTop, + ), + ), + ); + expect(tester.getRect(find.byType(FloatingActionButton)), Rect.fromLTWH(16.0, 28.0, 56.0, 56.0)); + }); + + testWidgets('End-top floating action button location RTL', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Directionality( + textDirection: TextDirection.rtl, + child: Scaffold( + appBar: AppBar(), + floatingActionButton: const FloatingActionButton(onPressed: null), + floatingActionButtonLocation: FloatingActionButtonLocation.endTop, + ), + ), + ), + ); + expect(tester.getRect(find.byType(FloatingActionButton)), Rect.fromLTWH(16.0, 28.0, 56.0, 56.0)); + }); + + testWidgets('Start-top floating action button location RTL', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Directionality( + textDirection: TextDirection.rtl, + child: Scaffold( + appBar: AppBar(), + floatingActionButton: const FloatingActionButton(onPressed: null), + floatingActionButtonLocation: FloatingActionButtonLocation.startTop, + ), + ), + ), + ); + expect(tester.getRect(find.byType(FloatingActionButton)), Rect.fromLTWH(800.0 - 56.0 - 16.0, 28.0, 56.0, 56.0)); + }); + + testWidgets('End-top floating action button location LTR', (WidgetTester tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + appBar: AppBar(), + floatingActionButton: const FloatingActionButton(onPressed: null), + floatingActionButtonLocation: FloatingActionButtonLocation.endTop, + ), + ), + ); + expect(tester.getRect(find.byType(FloatingActionButton)), Rect.fromLTWH(800.0 - 56.0 - 16.0, 28.0, 56.0, 56.0)); + }); }