diff --git a/dev/tools/gen_defaults/bin/gen_defaults.dart b/dev/tools/gen_defaults/bin/gen_defaults.dart index 21b76bf6cb1c..06a173f9a8a5 100644 --- a/dev/tools/gen_defaults/bin/gen_defaults.dart +++ b/dev/tools/gen_defaults/bin/gen_defaults.dart @@ -20,6 +20,7 @@ import 'dart:io'; import 'package:gen_defaults/action_chip_template.dart'; import 'package:gen_defaults/app_bar_template.dart'; import 'package:gen_defaults/banner_template.dart'; +import 'package:gen_defaults/bottom_app_bar.dart'; import 'package:gen_defaults/bottom_sheet_template.dart'; import 'package:gen_defaults/button_template.dart'; import 'package:gen_defaults/card_template.dart'; @@ -119,6 +120,7 @@ Future main(List args) async { ActionChipTemplate('Chip', '$materialLib/chip.dart', tokens).updateFile(); ActionChipTemplate('ActionChip', '$materialLib/action_chip.dart', tokens).updateFile(); AppBarTemplate('AppBar', '$materialLib/app_bar.dart', tokens).updateFile(); + BottomAppBarTemplate('BottomAppBar', '$materialLib/bottom_app_bar.dart', tokens).updateFile(); BannerTemplate('Banner', '$materialLib/banner.dart', tokens).updateFile(); BottomSheetTemplate('BottomSheet', '$materialLib/bottom_sheet.dart', tokens).updateFile(); ButtonTemplate('md.comp.elevated-button', 'ElevatedButton', '$materialLib/elevated_button.dart', tokens).updateFile(); diff --git a/dev/tools/gen_defaults/lib/bottom_app_bar.dart b/dev/tools/gen_defaults/lib/bottom_app_bar.dart new file mode 100644 index 000000000000..8c8f78a673df --- /dev/null +++ b/dev/tools/gen_defaults/lib/bottom_app_bar.dart @@ -0,0 +1,32 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'template.dart'; + +class BottomAppBarTemplate extends TokenTemplate { + const BottomAppBarTemplate(super.blockName, super.fileName, super.tokens); + + @override + String generate() => ''' +// Generated version ${tokens["version"]} +class _${blockName}DefaultsM3 extends BottomAppBarTheme { + const _${blockName}DefaultsM3(this.context) + : super( + elevation: ${elevation('md.comp.bottom-app-bar.container')}, + height: ${tokens['md.comp.bottom-app-bar.container.height']}, + ); + + final BuildContext context; + + @override + Color? get color => ${componentColor('md.comp.bottom-app-bar.container')}; + + @override + Color? get surfaceTintColor => ${componentColor('md.comp.bottom-app-bar.container.surface-tint-layer')}; + + @override + NotchedShape? get shape => const AutomaticNotchedShape(${shape('md.comp.bottom-app-bar.container')}); +} +'''; +} diff --git a/examples/api/lib/material/bottom_app_bar/bottom_app_bar.2.dart b/examples/api/lib/material/bottom_app_bar/bottom_app_bar.2.dart new file mode 100644 index 000000000000..2e48077bec8d --- /dev/null +++ b/examples/api/lib/material/bottom_app_bar/bottom_app_bar.2.dart @@ -0,0 +1,193 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Flutter code sample for BottomAppBar with Material 3 + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +void main() { + runApp(const BottomAppBarDemo()); +} + +class BottomAppBarDemo extends StatefulWidget { + const BottomAppBarDemo({super.key}); + + @override + State createState() => _BottomAppBarDemoState(); +} + +class _BottomAppBarDemoState extends State { + static const List colors = [ + Colors.yellow, + Colors.orange, + Colors.pink, + Colors.purple, + Colors.cyan, + ]; + + static final List items = List.generate( + colors.length, + (int index) => Container(color: colors[index], height: 150.0), + ).reversed.toList(); + + late ScrollController _controller; + bool _showFab = true; + bool _isElevated = true; + bool _isVisible = true; + + FloatingActionButtonLocation get _fabLocation => _isVisible + ? FloatingActionButtonLocation.endContained + : FloatingActionButtonLocation.endFloat; + + void _listen() { + final ScrollDirection direction = _controller.position.userScrollDirection; + if (direction == ScrollDirection.forward) { + _show(); + } else if (direction == ScrollDirection.reverse) { + _hide(); + } + } + + void _show() { + if (!_isVisible) { + setState(() => _isVisible = true); + } + } + + void _hide() { + if (_isVisible) { + setState(() => _isVisible = false); + } + } + + void _onShowFabChanged(bool value) { + setState(() { + _showFab = value; + }); + } + + void _onElevatedChanged(bool value) { + setState(() { + _isElevated = value; + }); + } + + void _addNewItem() { + setState(() { + items.insert( + 0, + Container(color: colors[items.length % 5], height: 150.0), + ); + }); + } + + @override + void initState() { + super.initState(); + _controller = ScrollController(); + _controller.addListener(_listen); + } + + @override + void dispose() { + _controller.removeListener(_listen); + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + theme: ThemeData(useMaterial3: true), + home: Scaffold( + appBar: AppBar( + title: const Text('Bottom App Bar Demo'), + ), + body: Column( + children: [ + SwitchListTile( + title: const Text('Floating Action Button'), + value: _showFab, + onChanged: _onShowFabChanged, + ), + SwitchListTile( + title: const Text('Bottom App Bar Elevation'), + value: _isElevated, + onChanged: _onElevatedChanged, + ), + Expanded( + child: ListView( + controller: _controller, + children: items.toList(), + ), + ), + ], + ), + floatingActionButton: _showFab + ? FloatingActionButton( + onPressed: _addNewItem, + tooltip: 'Add New Item', + elevation: _isVisible ? 0.0 : null, + child: const Icon(Icons.add), + ) + : null, + floatingActionButtonLocation: _fabLocation, + bottomNavigationBar: _DemoBottomAppBar(isElevated: _isElevated, isVisible: _isVisible), + ), + ); + } +} + +class _DemoBottomAppBar extends StatelessWidget { + const _DemoBottomAppBar({ + required this.isElevated, + required this.isVisible, + }); + + final bool isElevated; + final bool isVisible; + + @override + Widget build(BuildContext context) { + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + height: isVisible ? 80.0 : 0, + child: BottomAppBar( + elevation: isElevated ? null : 0.0, + child: Row( + children: [ + IconButton( + tooltip: 'Open popup menu', + icon: const Icon(Icons.more_vert), + onPressed: () { + final SnackBar snackBar = SnackBar( + content: const Text('Yay! A SnackBar!'), + action: SnackBarAction( + label: 'Undo', + onPressed: () {}, + ), + ); + + // Find the ScaffoldMessenger in the widget tree + // and use it to show a SnackBar. + ScaffoldMessenger.of(context).showSnackBar(snackBar); + }, + ), + IconButton( + tooltip: 'Search', + icon: const Icon(Icons.search), + onPressed: () {}, + ), + IconButton( + tooltip: 'Favorite', + icon: const Icon(Icons.favorite), + onPressed: () {}, + ), + ], + ), + ), + ); + } +} diff --git a/packages/flutter/lib/src/material/bottom_app_bar.dart b/packages/flutter/lib/src/material/bottom_app_bar.dart index 150732455ec7..100b76fc9f3f 100644 --- a/packages/flutter/lib/src/material/bottom_app_bar.dart +++ b/packages/flutter/lib/src/material/bottom_app_bar.dart @@ -14,9 +14,7 @@ import 'theme.dart'; // Examples can assume: // late Widget bottomAppBarContents; -/// A container that is typically used with [Scaffold.bottomNavigationBar], and -/// can have a notch along the top that makes room for an overlapping -/// [FloatingActionButton]. +/// A container that is typically used with [Scaffold.bottomNavigationBar]. /// /// Typically used with a [Scaffold] and a [FloatingActionButton]. /// @@ -40,6 +38,15 @@ import 'theme.dart'; /// ** See code in examples/api/lib/material/bottom_app_bar/bottom_app_bar.1.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This example shows Material 3 [BottomAppBar] with its expected look and behaviors. +/// +/// This also includes an optional [FloatingActionButton], which illustrates +/// the [FloatingActionButtonLocation.endContained]. +/// +/// ** See code in examples/api/lib/material/bottom_app_bar/bottom_app_bar.2.dart ** +/// {@end-tool} +/// /// See also: /// /// * [NotchedShape] which calculates the notch for a notched [BottomAppBar]. @@ -62,6 +69,8 @@ class BottomAppBar extends StatefulWidget { this.clipBehavior = Clip.none, this.notchMargin = 4.0, this.child, + this.surfaceTintColor, + this.height, }) : assert(elevation == null || elevation >= 0.0), assert(notchMargin != null), assert(clipBehavior != null); @@ -88,8 +97,8 @@ class BottomAppBar extends StatefulWidget { /// value is non-negative. /// /// If this property is null then [BottomAppBarTheme.elevation] of - /// [ThemeData.bottomAppBarTheme] is used. If that's null, the default value - /// is 8. + /// [ThemeData.bottomAppBarTheme] is used. If that's null and + /// [ThemeData.useMaterial3] is true, than the default value is 3 else is 8. final double? elevation; /// The notch that is made for the floating action button. @@ -110,6 +119,23 @@ class BottomAppBar extends StatefulWidget { /// Not used if [shape] is null. final double notchMargin; + /// The color used as an overlay on [color] to indicate elevation. + /// + /// If this is null, no overlay will be applied. Otherwise the + /// color will be composited on top of [color] with an opacity related + /// to [elevation] and used to paint the background of the [BottomAppBar]. + /// + /// The default is null. + /// + /// See [Material.surfaceTintColor] for more details on how this overlay is applied. + final Color? surfaceTintColor; + + /// The double value used to indicate the height of the [BottomAppBar]. + /// + /// If this is null, the default value is the minimum in relation to the content, + /// unless [ThemeData.useMaterial3] is true, in which case it defaults to 80.0. + final double? height; + @override State createState() => _BottomAppBarState(); } @@ -117,7 +143,6 @@ class BottomAppBar extends StatefulWidget { class _BottomAppBarState extends State { late ValueListenable geometryListenable; final GlobalKey materialKey = GlobalKey(); - static const double _defaultElevation = 8.0; @override void didChangeDependencies() { @@ -127,9 +152,13 @@ class _BottomAppBarState extends State { @override Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + final bool isMaterial3 = theme.useMaterial3; final BottomAppBarTheme babTheme = BottomAppBarTheme.of(context); + final BottomAppBarTheme defaults = isMaterial3 ? _BottomAppBarDefaultsM3(context) : _BottomAppBarDefaultsM2(context); + final bool hasFab = Scaffold.of(context).hasFloatingActionButton; - final NotchedShape? notchedShape = widget.shape ?? babTheme.shape; + final NotchedShape? notchedShape = widget.shape ?? babTheme.shape ?? defaults.shape; final CustomClipper clipper = notchedShape != null && hasFab ? _BottomAppBarClipper( geometry: geometryListenable, @@ -138,20 +167,33 @@ class _BottomAppBarState extends State { notchMargin: widget.notchMargin, ) : const ShapeBorderClipper(shape: RoundedRectangleBorder()); - final double elevation = widget.elevation ?? babTheme.elevation ?? _defaultElevation; - final Color color = widget.color ?? babTheme.color ?? Theme.of(context).bottomAppBarColor; - final Color effectiveColor = ElevationOverlay.applyOverlay(context, color, elevation); - return PhysicalShape( - clipper: clipper, - elevation: elevation, - color: effectiveColor, - clipBehavior: widget.clipBehavior, - child: Material( - key: materialKey, - type: MaterialType.transparency, - child: widget.child == null - ? null - : SafeArea(child: widget.child!), + final double elevation = widget.elevation ?? babTheme.elevation ?? defaults.elevation!; + final double? height = widget.height ?? babTheme.height ?? defaults.height; + final Color color = widget.color ?? babTheme.color ?? defaults.color!; + final Color surfaceTintColor = widget.surfaceTintColor ?? babTheme.surfaceTintColor ?? defaults.surfaceTintColor!; + final Color effectiveColor = isMaterial3 ? color : ElevationOverlay.applyOverlay(context, color, elevation); + + final Widget? child = isMaterial3 ? Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0), + child: widget.child, + ) : widget.child; + + return SizedBox( + height: height, + child: PhysicalShape( + clipper: clipper, + elevation: elevation, + color: effectiveColor, + clipBehavior: widget.clipBehavior, + child: Material( + key: materialKey, + type: isMaterial3 ? MaterialType.canvas : MaterialType.transparency, + elevation: elevation, + surfaceTintColor: surfaceTintColor, + child: child == null + ? null + : SafeArea(child: child), + ), ), ); } @@ -203,3 +245,49 @@ class _BottomAppBarClipper extends CustomClipper { || oldClipper.notchMargin != notchMargin; } } + +class _BottomAppBarDefaultsM2 extends BottomAppBarTheme { + const _BottomAppBarDefaultsM2(this.context) + : super( + elevation: 8.0, + ); + + final BuildContext context; + + @override + Color? get color => Theme.of(context).bottomAppBarColor; + + @override + Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint; +} + +// BEGIN GENERATED TOKEN PROPERTIES - BottomAppBar + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Token database version: v0_101 + +// Generated version v0_101 +class _BottomAppBarDefaultsM3 extends BottomAppBarTheme { + const _BottomAppBarDefaultsM3(this.context) + : super( + elevation: 3.0, + height: 80.0, + ); + + final BuildContext context; + + @override + Color? get color => Theme.of(context).colorScheme.surface; + + @override + Color? get surfaceTintColor => Theme.of(context).colorScheme.surfaceTint; + + @override + NotchedShape? get shape => const AutomaticNotchedShape(RoundedRectangleBorder()); +} + +// END GENERATED TOKEN PROPERTIES - BottomAppBar diff --git a/packages/flutter/lib/src/material/bottom_app_bar_theme.dart b/packages/flutter/lib/src/material/bottom_app_bar_theme.dart index df76ca8c52ba..fae3cbc4c0fd 100644 --- a/packages/flutter/lib/src/material/bottom_app_bar_theme.dart +++ b/packages/flutter/lib/src/material/bottom_app_bar_theme.dart @@ -32,6 +32,8 @@ class BottomAppBarTheme with Diagnosticable { this.color, this.elevation, this.shape, + this.height, + this.surfaceTintColor, }); /// Default value for [BottomAppBar.color]. @@ -45,17 +47,33 @@ class BottomAppBarTheme with Diagnosticable { /// Default value for [BottomAppBar.shape]. final NotchedShape? shape; + /// Default value for [BottomAppBar.height]. + /// + /// If null, [BottomAppBar] height will be the minimum on the non material 3. + final double? height; + + /// Default value for [BottomAppBar.surfaceTintColor]. + /// + /// If null, [BottomAppBar] will not display an overlay color. + /// + /// See [Material.surfaceTintColor] for more details. + final Color? surfaceTintColor; + /// Creates a copy of this object but with the given fields replaced with the /// new values. BottomAppBarTheme copyWith({ Color? color, double? elevation, NotchedShape? shape, + double? height, + Color? surfaceTintColor, }) { return BottomAppBarTheme( color: color ?? this.color, elevation: elevation ?? this.elevation, shape: shape ?? this.shape, + height: height ?? this.height, + surfaceTintColor: surfaceTintColor ?? this.surfaceTintColor, ); } @@ -75,6 +93,8 @@ class BottomAppBarTheme with Diagnosticable { color: Color.lerp(a?.color, b?.color, t), elevation: lerpDouble(a?.elevation, b?.elevation, t), shape: t < 0.5 ? a?.shape : b?.shape, + height: lerpDouble(a?.height, b?.height, t), + surfaceTintColor: Color.lerp(a?.color, b?.color, t), ); } @@ -83,6 +103,8 @@ class BottomAppBarTheme with Diagnosticable { color, elevation, shape, + height, + surfaceTintColor, ); @override @@ -96,7 +118,9 @@ class BottomAppBarTheme with Diagnosticable { return other is BottomAppBarTheme && other.color == color && other.elevation == elevation - && other.shape == shape; + && other.shape == shape + && other.height == height + && other.surfaceTintColor == surfaceTintColor; } @override @@ -105,5 +129,7 @@ class BottomAppBarTheme with Diagnosticable { properties.add(ColorProperty('color', color, defaultValue: null)); properties.add(DiagnosticsProperty('elevation', elevation, defaultValue: null)); properties.add(DiagnosticsProperty('shape', shape, defaultValue: null)); + properties.add(DiagnosticsProperty('height', height, defaultValue: null)); + properties.add(ColorProperty('surfaceTintColor', surfaceTintColor, defaultValue: null)); } } 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 d19fd54b53a7..862cce98bd1e 100644 --- a/packages/flutter/lib/src/material/floating_action_button_location.dart +++ b/packages/flutter/lib/src/material/floating_action_button_location.dart @@ -419,6 +419,16 @@ abstract class FloatingActionButtonLocation { /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_mini_end_docked.png) static const FloatingActionButtonLocation miniEndDocked = _MiniEndDockedFabLocation(); + /// End-aligned [FloatingActionButton], floating over the + /// [Scaffold.bottomNavigationBar] so that the floating + /// action button lines up with the center of the bottom navigation bar. + /// + /// This is unlikely to be a useful location for apps which has a [BottomNavigationBar] + /// or a non material 3 [BottomAppBar]. + /// + /// ![](https://flutter.github.io/assets-for-api-docs/assets/material/floating_action_button_location_end_contained.png) + static const FloatingActionButtonLocation endContained = _EndContainedFabLocation(); + /// Places the [FloatingActionButton] based on the [Scaffold]'s layout. /// /// This uses a [ScaffoldPrelayoutGeometry], which the [Scaffold] constructs @@ -609,6 +619,34 @@ mixin FabDockedOffsetY on StandardFabLocation { } } +/// Mixin for a "contained" floating action button location, such as [FloatingActionButtonLocation.endContained]. +mixin FabContainedOffsetY on StandardFabLocation { + /// Calculates y-offset for [FloatingActionButtonLocation]s floating over the + /// [Scaffold.bottomNavigationBar] so that the center of the floating + /// action button lines up with the center of the bottom navigation bar. + @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 fabHeight = scaffoldGeometry.floatingActionButtonSize.height; + final double bottomMinInset = scaffoldGeometry.minInsets.bottom; + + double safeMargin = 0.0; + if (contentMargin > bottomMinInset + fabHeight / 2.0) { + // If contentMargin is higher than bottomMinInset enough to display the + // FAB without clipping, don't provide a margin + safeMargin = 0.0; + } else { + safeMargin = bottomViewPadding; + } + + final double fabY = contentBottom - fabHeight / 2.0 - safeMargin; + final double maxFabY = scaffoldGeometry.scaffoldSize.height - fabHeight - safeMargin; + return math.min(maxFabY, fabY + contentMargin / 2); + } +} + /// Mixin for a "start" floating action button location, such as [FloatingActionButtonLocation.startTop]. mixin FabStartOffsetX on StandardFabLocation { /// Calculates x-offset for start-aligned [FloatingActionButtonLocation]s. @@ -798,6 +836,14 @@ class _MiniEndDockedFabLocation extends StandardFabLocation String toString() => 'FloatingActionButtonLocation.miniEndDocked'; } +class _EndContainedFabLocation extends StandardFabLocation + with FabEndOffsetX, FabContainedOffsetY { + const _EndContainedFabLocation(); + + @override + String toString() => 'FloatingActionButtonLocation.endContained'; +} + /// Provider of animations to move the [FloatingActionButton] between [FloatingActionButtonLocation]s. /// /// The [Scaffold] uses [Scaffold.floatingActionButtonAnimator] to define: diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 7503d03233dd..e264a7898230 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -1288,6 +1288,7 @@ class ThemeData with Diagnosticable { /// /// ### Components /// * Common buttons: [ElevatedButton], [FilledButton], [OutlinedButton], [TextButton], [IconButton] + /// * Bottom app bar: [BottomAppBar] /// * FAB: [FloatingActionButton] /// * Extended FAB: [FloatingActionButton.extended] /// * Cards: [Card] diff --git a/packages/flutter/test/material/bottom_app_bar_theme_test.dart b/packages/flutter/test/material/bottom_app_bar_theme_test.dart index 665d1b389d6c..1dd8ceb2dbb5 100644 --- a/packages/flutter/test/material/bottom_app_bar_theme_test.dart +++ b/packages/flutter/test/material/bottom_app_bar_theme_test.dart @@ -10,92 +10,215 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - testWidgets('BAB theme overrides color', (WidgetTester tester) async { - const Color themedColor = Colors.black87; - const BottomAppBarTheme theme = BottomAppBarTheme(color: themedColor); + group('Material 2 tests', () { + testWidgets('BAB theme overrides color', (WidgetTester tester) async { + const Color themedColor = Colors.black87; + const BottomAppBarTheme theme = BottomAppBarTheme(color: themedColor); - await tester.pumpWidget(_withTheme(theme)); + await tester.pumpWidget(_withTheme(theme)); - final PhysicalShape widget = _getBabRenderObject(tester); - expect(widget.color, themedColor); - }); + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, themedColor); + }); - testWidgets('BAB color - Widget', (WidgetTester tester) async { - const Color themeColor = Colors.white10; - const Color babThemeColor = Colors.black87; - const Color babColor = Colors.pink; - const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); + testWidgets('BAB color - Widget', (WidgetTester tester) async { + const Color themeColor = Colors.white10; + const Color babThemeColor = Colors.black87; + const Color babColor = Colors.pink; + const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); - await tester.pumpWidget(MaterialApp( - theme: ThemeData(bottomAppBarTheme: theme, bottomAppBarColor: themeColor), - home: const Scaffold(body: BottomAppBar(color: babColor)), - )); + await tester.pumpWidget(MaterialApp( + theme: ThemeData( + bottomAppBarTheme: theme, bottomAppBarColor: themeColor), + home: const Scaffold(body: BottomAppBar(color: babColor)), + )); - final PhysicalShape widget = _getBabRenderObject(tester); - expect(widget.color, babColor); - }); + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, babColor); + }); - testWidgets('BAB color - BabTheme', (WidgetTester tester) async { - const Color themeColor = Colors.white10; - const Color babThemeColor = Colors.black87; - const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); + testWidgets('BAB color - BabTheme', (WidgetTester tester) async { + const Color themeColor = Colors.white10; + const Color babThemeColor = Colors.black87; + const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); - await tester.pumpWidget(MaterialApp( - theme: ThemeData(bottomAppBarTheme: theme, bottomAppBarColor: themeColor), - home: const Scaffold(body: BottomAppBar()), - )); + await tester.pumpWidget(MaterialApp( + theme: ThemeData( + bottomAppBarTheme: theme, bottomAppBarColor: themeColor), + home: const Scaffold(body: BottomAppBar()), + )); - final PhysicalShape widget = _getBabRenderObject(tester); - expect(widget.color, babThemeColor); - }); + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, babThemeColor); + }); - testWidgets('BAB color - Theme', (WidgetTester tester) async { - const Color themeColor = Colors.white10; + testWidgets('BAB color - Theme', (WidgetTester tester) async { + const Color themeColor = Colors.white10; - await tester.pumpWidget(MaterialApp( - theme: ThemeData(bottomAppBarColor: themeColor), - home: const Scaffold(body: BottomAppBar()), - )); + await tester.pumpWidget(MaterialApp( + theme: ThemeData(bottomAppBarColor: themeColor), + home: const Scaffold(body: BottomAppBar()), + )); - final PhysicalShape widget = _getBabRenderObject(tester); - expect(widget.color, themeColor); - }); + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, themeColor); + }); - testWidgets('BAB color - Default', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp( - theme: ThemeData(), - home: const Scaffold(body: BottomAppBar()), - )); + testWidgets('BAB color - Default', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(), + home: const Scaffold(body: BottomAppBar()), + )); - final PhysicalShape widget = _getBabRenderObject(tester); + final PhysicalShape widget = _getBabRenderObject(tester); - expect(widget.color, Colors.white); - }); + expect(widget.color, Colors.white); + }); - testWidgets('BAB theme customizes shape', (WidgetTester tester) async { - const BottomAppBarTheme theme = BottomAppBarTheme( + testWidgets('BAB theme customizes shape', (WidgetTester tester) async { + const BottomAppBarTheme theme = BottomAppBarTheme( color: Colors.white30, shape: CircularNotchedRectangle(), elevation: 1.0, - ); + ); + + await tester.pumpWidget(_withTheme(theme)); + + await expectLater( + find.byKey(_painterKey), + matchesGoldenFile('bottom_app_bar_theme.custom_shape.png'), + ); + }); + + testWidgets( + 'BAB theme does not affect defaults', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Scaffold(body: BottomAppBar()), + )); - await tester.pumpWidget(_withTheme(theme)); + final PhysicalShape widget = _getBabRenderObject(tester); - await expectLater( - find.byKey(_painterKey), - matchesGoldenFile('bottom_app_bar_theme.custom_shape.png'), - ); + expect(widget.color, Colors.white); + expect(widget.elevation, equals(8.0)); + }); }); - testWidgets('BAB theme does not affect defaults', (WidgetTester tester) async { - await tester.pumpWidget(const MaterialApp( - home: Scaffold(body: BottomAppBar()), - )); + group('Material 3 tests', () { + Material getBabRenderObject(WidgetTester tester) { + return tester.widget( + find.descendant( + of: find.byType(BottomAppBar), + matching: find.byType(Material), + ), + ); + } + + testWidgets('BAB theme overrides color - M3', (WidgetTester tester) async { + const Color themedColor = Colors.black87; + const BottomAppBarTheme theme = BottomAppBarTheme( + color: themedColor, elevation: 0); + + await tester.pumpWidget(_withTheme(theme, true)); + + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, themedColor); + }); + + testWidgets('BAB color - Widget - M3', (WidgetTester tester) async { + const Color themeColor = Colors.white10; + const Color babThemeColor = Colors.black87; + const Color babColor = Colors.pink; + const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true, + bottomAppBarTheme: theme, + bottomAppBarColor: themeColor), + home: const Scaffold(body: BottomAppBar(color: babColor)), + )); + + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, babColor); + }); + + testWidgets('BAB color - BabTheme - M3', (WidgetTester tester) async { + const Color themeColor = Colors.white10; + const Color babThemeColor = Colors.black87; + const BottomAppBarTheme theme = BottomAppBarTheme(color: babThemeColor); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true, + bottomAppBarTheme: theme, + bottomAppBarColor: themeColor), + home: const Scaffold(body: BottomAppBar()), + )); + + final PhysicalShape widget = _getBabRenderObject(tester); + expect(widget.color, babThemeColor); + }); + + testWidgets( + 'BAB theme does not affect defaults - M3', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true), + home: const Scaffold(body: BottomAppBar()), + )); + + final PhysicalShape widget = _getBabRenderObject(tester); + + expect(widget.color, Colors.white); + expect(widget.elevation, equals(3.0)); + }); + + testWidgets('BAB theme overrides surfaceTintColor - M3', ( + WidgetTester tester) async { + const Color babThemeSurfaceTintColor = Colors.black87; + const BottomAppBarTheme theme = BottomAppBarTheme( + surfaceTintColor: babThemeSurfaceTintColor, elevation: 0); + + await tester.pumpWidget(_withTheme(theme, true)); + + final Material widget = getBabRenderObject(tester); + expect(widget.surfaceTintColor, babThemeSurfaceTintColor); + }); + + testWidgets( + 'BAB surfaceTintColor - Widget - M3', (WidgetTester tester) async { + const Color themeSurfaceTintColor = Colors.white10; + const Color babThemeSurfaceTintColor = Colors.black87; + const Color babSurfaceTintColor = Colors.pink; + const BottomAppBarTheme theme = BottomAppBarTheme( + surfaceTintColor: babThemeSurfaceTintColor); + + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true, + bottomAppBarTheme: theme, + bottomAppBarColor: themeSurfaceTintColor), + home: const Scaffold( + body: BottomAppBar(surfaceTintColor: babSurfaceTintColor)), + )); + + final Material widget = getBabRenderObject(tester); + expect(widget.surfaceTintColor, babSurfaceTintColor); + }); + + testWidgets( + 'BAB surfaceTintColor - BabTheme - M3', (WidgetTester tester) async { + const Color themeColor = Colors.white10; + const Color babThemeColor = Colors.black87; + const BottomAppBarTheme theme = BottomAppBarTheme( + surfaceTintColor: babThemeColor); - final PhysicalShape widget = _getBabRenderObject(tester); + await tester.pumpWidget(MaterialApp( + theme: ThemeData(useMaterial3: true, + bottomAppBarTheme: theme, + bottomAppBarColor: themeColor), + home: const Scaffold(body: BottomAppBar()), + )); - expect(widget.color, Colors.white); - expect(widget.elevation, equals(8.0)); + final Material widget = getBabRenderObject(tester); + expect(widget.surfaceTintColor, babThemeColor); + }); }); } @@ -110,9 +233,9 @@ PhysicalShape _getBabRenderObject(WidgetTester tester) { final Key _painterKey = UniqueKey(); -Widget _withTheme(BottomAppBarTheme theme) { +Widget _withTheme(BottomAppBarTheme theme, [bool useMaterial3 = false]) { return MaterialApp( - theme: ThemeData(bottomAppBarTheme: theme), + theme: ThemeData(useMaterial3: useMaterial3, bottomAppBarTheme: theme), home: Scaffold( floatingActionButton: const FloatingActionButton(onPressed: null), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, 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 a0147b9bbffe..1301d5f74a08 100644 --- a/packages/flutter/test/material/floating_action_button_location_test.dart +++ b/packages/flutter/test/material/floating_action_button_location_test.dart @@ -296,6 +296,21 @@ void main() { expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 572.0)); }); + testWidgets('Contained floating action button locations', (WidgetTester tester) async { + await tester.pumpWidget( + _buildFrame( + location: FloatingActionButtonLocation.endContained, + bab: const SizedBox(height: 100.0), + viewInsets: EdgeInsets.zero, + ), + ); + + // Scaffold 800x600, FAB is 56x56, BAB is 800x100, FAB's center is + // at the top of the BAB. + // Formula: scaffold height - BAB height + FAB height / 2 + BAB top & bottom margins. + expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(756.0, 550.0)); + }); + testWidgets('Mini-start-top floating action button location', (WidgetTester tester) async { await tester.pumpWidget( MaterialApp( @@ -430,6 +445,12 @@ void main() { expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _dockedOffsetY)); }); + testWidgets('endContained', (WidgetTester tester) async { + await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.endContained)); + + expect(tester.getCenter(find.byType(FloatingActionButton)), const Offset(_rightOffsetX, _containedOffsetY)); + }); + testWidgets('miniStartTop', (WidgetTester tester) async { await tester.pumpWidget(_singleFabScaffold(FloatingActionButtonLocation.miniStartTop)); @@ -1617,6 +1638,7 @@ const double _miniRightOffsetX = _rightOffsetX + kMiniButtonOffsetAdjustment; const double _topOffsetY = 56.0; const double _floatOffsetY = 500.0; const double _dockedOffsetY = 544.0; +const double _containedOffsetY = 544.0 + 56.0 / 2; const double _miniFloatOffsetY = _floatOffsetY + kMiniButtonOffsetAdjustment; Widget _singleFabScaffold(