Unverified Commit 33777817 authored by Ayush Bherwani's avatar Ayush Bherwani Committed by GitHub

[AppBar] adds toolbarHeight property to customize AppBar height (#59405)

parent 4d2ddb91
...@@ -28,18 +28,20 @@ import 'theme.dart'; ...@@ -28,18 +28,20 @@ import 'theme.dart';
const double _kLeadingWidth = kToolbarHeight; // So the leading button is square. const double _kLeadingWidth = kToolbarHeight; // So the leading button is square.
const double _kMaxTitleTextScaleFactor = 1.34; // TODO(perc): Add link to Material spec when available, https://github.com/flutter/flutter/issues/58769. const double _kMaxTitleTextScaleFactor = 1.34; // TODO(perc): Add link to Material spec when available, https://github.com/flutter/flutter/issues/58769.
// Bottom justify the kToolbarHeight child which may overflow the top. // Bottom justify the toolbarHeight child which may overflow the top.
class _ToolbarContainerLayout extends SingleChildLayoutDelegate { class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
const _ToolbarContainerLayout(); const _ToolbarContainerLayout(this.toolbarHeight);
final double toolbarHeight;
@override @override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) { BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
return constraints.tighten(height: kToolbarHeight); return constraints.tighten(height: toolbarHeight);
} }
@override @override
Size getSize(BoxConstraints constraints) { Size getSize(BoxConstraints constraints) {
return Size(constraints.maxWidth, kToolbarHeight); return Size(constraints.maxWidth, toolbarHeight);
} }
@override @override
...@@ -48,7 +50,8 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate { ...@@ -48,7 +50,8 @@ class _ToolbarContainerLayout extends SingleChildLayoutDelegate {
} }
@override @override
bool shouldRelayout(_ToolbarContainerLayout oldDelegate) => false; bool shouldRelayout(_ToolbarContainerLayout oldDelegate) =>
toolbarHeight != oldDelegate.toolbarHeight;
} }
// TODO(eseidel): Toolbar needs to change size based on orientation: // TODO(eseidel): Toolbar needs to change size based on orientation:
...@@ -204,13 +207,14 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -204,13 +207,14 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
this.titleSpacing = NavigationToolbar.kMiddleSpacing, this.titleSpacing = NavigationToolbar.kMiddleSpacing,
this.toolbarOpacity = 1.0, this.toolbarOpacity = 1.0,
this.bottomOpacity = 1.0, this.bottomOpacity = 1.0,
this.toolbarHeight,
}) : assert(automaticallyImplyLeading != null), }) : assert(automaticallyImplyLeading != null),
assert(elevation == null || elevation >= 0.0), assert(elevation == null || elevation >= 0.0),
assert(primary != null), assert(primary != null),
assert(titleSpacing != null), assert(titleSpacing != null),
assert(toolbarOpacity != null), assert(toolbarOpacity != null),
assert(bottomOpacity != null), assert(bottomOpacity != null),
preferredSize = Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)), preferredSize = Size.fromHeight(toolbarHeight ?? kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
super(key: key); super(key: key);
/// A widget to display before the [title]. /// A widget to display before the [title].
...@@ -219,7 +223,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -219,7 +223,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// ///
/// Becomes the leading component of the [NavigationToolBar] built /// Becomes the leading component of the [NavigationToolBar] built
/// by this widget. The [leading] widget's width and height are constrained to /// by this widget. The [leading] widget's width and height are constrained to
/// be no bigger than toolbar's height, which is [kToolbarHeight]. /// be no bigger than [kToolbarHeight] and [toolbarHeight] respectively.
/// ///
/// If this is null and [automaticallyImplyLeading] is set to true, the /// If this is null and [automaticallyImplyLeading] is set to true, the
/// [AppBar] will imply an appropriate widget. For example, if the [AppBar] is /// [AppBar] will imply an appropriate widget. For example, if the [AppBar] is
...@@ -276,12 +280,12 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -276,12 +280,12 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// The [title]'s width is constrained to fit within the remaining space /// The [title]'s width is constrained to fit within the remaining space
/// between the toolbar's [leading] and [actions] widgets. Its height is /// between the toolbar's [leading] and [actions] widgets. Its height is
/// _not_ constrained. The [title] is vertically centered and clipped to fit /// _not_ constrained. The [title] is vertically centered and clipped to fit
/// within the toolbar, whose height is [kToolbarHeight]. Typically this /// within the toolbar, whose height is [toolbarHeight]. Typically this
/// isn't noticeable because a simple [Text] [title] will fit within the /// isn't noticeable because a simple [Text] [title] will fit within the
/// toolbar by default. On the other hand, it is noticeable when a /// toolbar by default. On the other hand, it is noticeable when a
/// widget with an intrinsic height that is greater than [kToolbarHeight] /// widget with an intrinsic height that is greater than [toolbarHeight]
/// is used as the [title]. For example, when the height of an Image used /// is used as the [title]. For example, when the height of an Image used
/// as the [title] exceeds [kToolbarHeight], it will be centered and /// as the [title] exceeds [toolbarHeight], it will be centered and
/// clipped (top and bottom), which may be undesirable. In cases like this /// clipped (top and bottom), which may be undesirable. In cases like this
/// the height of the [title] widget can be constrained. For example: /// the height of the [title] widget can be constrained. For example:
/// ///
...@@ -290,9 +294,10 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -290,9 +294,10 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// home: Scaffold( /// home: Scaffold(
/// appBar: AppBar( /// appBar: AppBar(
/// title: SizedBox( /// title: SizedBox(
/// height: kToolbarHeight, /// height: toolbarHeight,
/// child: child: Image.asset(logoAsset), /// child: child: Image.asset(logoAsset),
/// ), /// ),
/// toolbarHeight: toolbarHeight,
/// ), /// ),
/// ) /// )
/// ``` /// ```
...@@ -306,7 +311,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -306,7 +311,7 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// ///
/// The [actions] become the trailing component of the [NavigationToolBar] built /// The [actions] become the trailing component of the [NavigationToolBar] built
/// by this widget. The height of each action is constrained to be no bigger /// by this widget. The height of each action is constrained to be no bigger
/// than the toolbar's height, which is [kToolbarHeight]. /// than the [toolbarHeight].
final List<Widget> actions; final List<Widget> actions;
/// This widget is stacked behind the toolbar and the tab bar. It's height will /// This widget is stacked behind the toolbar and the tab bar. It's height will
...@@ -433,13 +438,18 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget { ...@@ -433,13 +438,18 @@ class AppBar extends StatefulWidget implements PreferredSizeWidget {
/// bar is scrolled. /// bar is scrolled.
final double bottomOpacity; final double bottomOpacity;
/// A size whose height is the sum of [kToolbarHeight] and the [bottom] widget's /// A size whose height is the sum of [toolbarHeight] and the [bottom] widget's
/// preferred height. /// preferred height.
/// ///
/// [Scaffold] uses this size to set its app bar's height. /// [Scaffold] uses this size to set its app bar's height.
@override @override
final Size preferredSize; final Size preferredSize;
/// Defines the height of the toolbar component of an [AppBar].
///
/// By default, the value of `toolbarHeight` is [kToolbarHeight].
final double toolbarHeight;
bool _getEffectiveCenterTitle(ThemeData theme) { bool _getEffectiveCenterTitle(ThemeData theme) {
if (centerTitle != null) if (centerTitle != null)
return centerTitle; return centerTitle;
...@@ -489,6 +499,8 @@ class _AppBarState extends State<AppBar> { ...@@ -489,6 +499,8 @@ class _AppBarState extends State<AppBar> {
final bool canPop = parentRoute?.canPop ?? false; final bool canPop = parentRoute?.canPop ?? false;
final bool useCloseButton = parentRoute is PageRoute<dynamic> && parentRoute.fullscreenDialog; final bool useCloseButton = parentRoute is PageRoute<dynamic> && parentRoute.fullscreenDialog;
final double toolbarHeight = widget.toolbarHeight ?? kToolbarHeight;
IconThemeData overallIconTheme = widget.iconTheme IconThemeData overallIconTheme = widget.iconTheme
?? appBarTheme.iconTheme ?? appBarTheme.iconTheme
?? theme.primaryIconTheme; ?? theme.primaryIconTheme;
...@@ -618,11 +630,11 @@ class _AppBarState extends State<AppBar> { ...@@ -618,11 +630,11 @@ class _AppBarState extends State<AppBar> {
middleSpacing: widget.titleSpacing, middleSpacing: widget.titleSpacing,
); );
// If the toolbar is allocated less than kToolbarHeight make it // If the toolbar is allocated less than toolbarHeight make it
// appear to scroll upwards within its shrinking container. // appear to scroll upwards within its shrinking container.
Widget appBar = ClipRect( Widget appBar = ClipRect(
child: CustomSingleChildLayout( child: CustomSingleChildLayout(
delegate: const _ToolbarContainerLayout(), delegate: _ToolbarContainerLayout(toolbarHeight),
child: IconTheme.merge( child: IconTheme.merge(
data: overallIconTheme, data: overallIconTheme,
child: DefaultTextStyle( child: DefaultTextStyle(
...@@ -638,7 +650,7 @@ class _AppBarState extends State<AppBar> { ...@@ -638,7 +650,7 @@ class _AppBarState extends State<AppBar> {
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: kToolbarHeight), constraints: BoxConstraints(maxHeight: toolbarHeight),
child: appBar, child: appBar,
), ),
), ),
...@@ -788,6 +800,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -788,6 +800,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@required this.snapConfiguration, @required this.snapConfiguration,
@required this.stretchConfiguration, @required this.stretchConfiguration,
@required this.shape, @required this.shape,
@required this.toolbarHeight,
}) : assert(primary || topPadding == 0.0), }) : assert(primary || topPadding == 0.0),
_bottomHeight = bottom?.preferredSize?.height ?? 0.0; _bottomHeight = bottom?.preferredSize?.height ?? 0.0;
...@@ -815,6 +828,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -815,6 +828,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
final bool floating; final bool floating;
final bool pinned; final bool pinned;
final ShapeBorder shape; final ShapeBorder shape;
final double toolbarHeight;
final double _bottomHeight; final double _bottomHeight;
...@@ -822,7 +836,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -822,7 +836,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
double get minExtent => collapsedHeight; double get minExtent => collapsedHeight;
@override @override
double get maxExtent => math.max(topPadding + (expandedHeight ?? kToolbarHeight + _bottomHeight), minExtent); double get maxExtent => math.max(topPadding + (expandedHeight ?? (toolbarHeight ?? kToolbarHeight) + _bottomHeight), minExtent);
@override @override
final FloatingHeaderSnapConfiguration snapConfiguration; final FloatingHeaderSnapConfiguration snapConfiguration;
...@@ -833,12 +847,12 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -833,12 +847,12 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
@override @override
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
final double visibleMainHeight = maxExtent - shrinkOffset - topPadding; final double visibleMainHeight = maxExtent - shrinkOffset - topPadding;
final double extraToolbarHeight = math.max(minExtent - _bottomHeight - topPadding - kToolbarHeight, 0.0); final double extraToolbarHeight = math.max(minExtent - _bottomHeight - topPadding - (toolbarHeight ?? kToolbarHeight), 0.0);
final double visibleToolbarHeight = visibleMainHeight - _bottomHeight - extraToolbarHeight; final double visibleToolbarHeight = visibleMainHeight - _bottomHeight - extraToolbarHeight;
final bool isPinnedWithOpacityFade = pinned && floating && bottom != null && extraToolbarHeight == 0.0; final bool isPinnedWithOpacityFade = pinned && floating && bottom != null && extraToolbarHeight == 0.0;
final double toolbarOpacity = !pinned || isPinnedWithOpacityFade final double toolbarOpacity = !pinned || isPinnedWithOpacityFade
? (visibleToolbarHeight / kToolbarHeight).clamp(0.0, 1.0) as double ? (visibleToolbarHeight / (toolbarHeight ?? kToolbarHeight)).clamp(0.0, 1.0) as double
: 1.0; : 1.0;
final Widget appBar = FlexibleSpaceBar.createSettings( final Widget appBar = FlexibleSpaceBar.createSettings(
...@@ -869,6 +883,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -869,6 +883,7 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
shape: shape, shape: shape,
toolbarOpacity: toolbarOpacity, toolbarOpacity: toolbarOpacity,
bottomOpacity: pinned ? 1.0 : ((visibleMainHeight / _bottomHeight).clamp(0.0, 1.0) as double), bottomOpacity: pinned ? 1.0 : ((visibleMainHeight / _bottomHeight).clamp(0.0, 1.0) as double),
toolbarHeight: toolbarHeight,
), ),
); );
return floating ? _FloatingAppBar(child: appBar) : appBar; return floating ? _FloatingAppBar(child: appBar) : appBar;
...@@ -899,7 +914,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { ...@@ -899,7 +914,8 @@ class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|| floating != oldDelegate.floating || floating != oldDelegate.floating
|| snapConfiguration != oldDelegate.snapConfiguration || snapConfiguration != oldDelegate.snapConfiguration
|| stretchConfiguration != oldDelegate.stretchConfiguration || stretchConfiguration != oldDelegate.stretchConfiguration
|| forceElevated != oldDelegate.forceElevated; || forceElevated != oldDelegate.forceElevated
|| toolbarHeight != oldDelegate.toolbarHeight;
} }
@override @override
...@@ -1020,6 +1036,7 @@ class SliverAppBar extends StatefulWidget { ...@@ -1020,6 +1036,7 @@ class SliverAppBar extends StatefulWidget {
this.stretchTriggerOffset = 100.0, this.stretchTriggerOffset = 100.0,
this.onStretchTrigger, this.onStretchTrigger,
this.shape, this.shape,
this.toolbarHeight = kToolbarHeight,
}) : assert(automaticallyImplyLeading != null), }) : assert(automaticallyImplyLeading != null),
assert(forceElevated != null), assert(forceElevated != null),
assert(primary != null), assert(primary != null),
...@@ -1028,9 +1045,10 @@ class SliverAppBar extends StatefulWidget { ...@@ -1028,9 +1045,10 @@ class SliverAppBar extends StatefulWidget {
assert(pinned != null), assert(pinned != null),
assert(snap != null), assert(snap != null),
assert(stretch != null), assert(stretch != null),
assert(toolbarHeight != null),
assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'), assert(floating || !snap, 'The "snap" argument only makes sense for floating app bars.'),
assert(stretchTriggerOffset > 0.0), assert(stretchTriggerOffset > 0.0),
assert(collapsedHeight == null || collapsedHeight > kToolbarHeight, 'The "collapsedHeight" argument has to be larger than [kToolbarHeight].'), assert(collapsedHeight == null || collapsedHeight > toolbarHeight, 'The "collapsedHeight" argument has to be larger than [toolbarHeight].'),
super(key: key); super(key: key);
/// A widget to display before the [title]. /// A widget to display before the [title].
...@@ -1198,7 +1216,7 @@ class SliverAppBar extends StatefulWidget { ...@@ -1198,7 +1216,7 @@ class SliverAppBar extends StatefulWidget {
/// Defines the height of the app bar when it is collapsed. /// Defines the height of the app bar when it is collapsed.
/// ///
/// By default, the collapsed height is [kToolbarHeight]. If [bottom] widget /// By default, the collapsed height is [toolbarHeight]. If [bottom] widget
/// is specified, then its [bottom.preferredSize.height] is added to the /// is specified, then its [bottom.preferredSize.height] is added to the
/// height. If [primary] is true, then the [MediaQuery] top padding, /// height. If [primary] is true, then the [MediaQuery] top padding,
/// [MediaQueryData.padding.top], is added as well. /// [MediaQueryData.padding.top], is added as well.
...@@ -1312,6 +1330,11 @@ class SliverAppBar extends StatefulWidget { ...@@ -1312,6 +1330,11 @@ class SliverAppBar extends StatefulWidget {
/// offset specified by [stretchTriggerOffset]. /// offset specified by [stretchTriggerOffset].
final AsyncCallback onStretchTrigger; final AsyncCallback onStretchTrigger;
/// Defines the height of the toolbar component of an [AppBar].
///
/// By default, the value of `toolbarHeight` is [kToolbarHeight].
final double toolbarHeight;
@override @override
_SliverAppBarState createState() => _SliverAppBarState(); _SliverAppBarState createState() => _SliverAppBarState();
} }
...@@ -1368,7 +1391,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -1368,7 +1391,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0; final double topPadding = widget.primary ? MediaQuery.of(context).padding.top : 0.0;
final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null) final double collapsedHeight = (widget.pinned && widget.floating && widget.bottom != null)
? (widget.collapsedHeight ?? 0.0) + bottomHeight + topPadding ? (widget.collapsedHeight ?? 0.0) + bottomHeight + topPadding
: (widget.collapsedHeight ?? kToolbarHeight) + bottomHeight + topPadding; : (widget.collapsedHeight ?? widget.toolbarHeight) + bottomHeight + topPadding;
return MediaQuery.removePadding( return MediaQuery.removePadding(
context: context, context: context,
...@@ -1403,6 +1426,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix ...@@ -1403,6 +1426,7 @@ class _SliverAppBarState extends State<SliverAppBar> with TickerProviderStateMix
shape: widget.shape, shape: widget.shape,
snapConfiguration: _snapConfiguration, snapConfiguration: _snapConfiguration,
stretchConfiguration: _stretchConfiguration, stretchConfiguration: _stretchConfiguration,
toolbarHeight: widget.toolbarHeight,
), ),
), ),
); );
......
...@@ -18,6 +18,7 @@ Widget buildSliverAppBarApp({ ...@@ -18,6 +18,7 @@ Widget buildSliverAppBarApp({
double collapsedHeight, double collapsedHeight,
double expandedHeight, double expandedHeight,
bool snap = false, bool snap = false,
double toolbarHeight = kToolbarHeight,
}) { }) {
return Localizations( return Localizations(
locale: const Locale('en', 'US'), locale: const Locale('en', 'US'),
...@@ -41,6 +42,7 @@ Widget buildSliverAppBarApp({ ...@@ -41,6 +42,7 @@ Widget buildSliverAppBarApp({
pinned: pinned, pinned: pinned,
collapsedHeight: collapsedHeight, collapsedHeight: collapsedHeight,
expandedHeight: expandedHeight, expandedHeight: expandedHeight,
toolbarHeight: toolbarHeight,
snap: snap, snap: snap,
bottom: TabBar( bottom: TabBar(
tabs: <String>['A','B','C'].map<Widget>((String t) => Tab(text: 'TAB $t')).toList(), tabs: <String>['A','B','C'].map<Widget>((String t) => Tab(text: 'TAB $t')).toList(),
...@@ -1940,4 +1942,73 @@ void main() { ...@@ -1940,4 +1942,73 @@ void main() {
expect(tester.getRect(appBarTitle), const Rect.fromLTRB(200, -12, 800.0 - 200.0, 68)); expect(tester.getRect(appBarTitle), const Rect.fromLTRB(200, -12, 800.0 - 200.0, 68));
expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy); expect(tester.getCenter(appBarTitle).dy, tester.getCenter(toolbar).dy);
}); });
testWidgets('AppBar respects toolbarHeight', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Title'),
toolbarHeight: 48,
),
body: Container(),
),
)
);
expect(appBarHeight(tester), 48);
});
testWidgets('SliverAppBar default collapsedHeight with respect to toolbarHeight', (WidgetTester tester) async {
const double toolbarHeight = 100.0;
await tester.pumpWidget(buildSliverAppBarApp(
floating: false,
pinned: false,
toolbarHeight: toolbarHeight,
));
final ScrollController controller = primaryScrollController(tester);
final double initialTabBarHeight = tabBarHeight(tester);
// Scroll the not-pinned appbar out of view, to its collapsed height.
controller.jumpTo(300.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsNothing);
// By default, the collapsedHeight is toolbarHeight + bottom.preferredSize.height,
// in this case initialTabBarHeight.
expect(appBarHeight(tester), toolbarHeight + initialTabBarHeight);
});
testWidgets('SliverAppBar collapsedHeight with toolbarHeight', (WidgetTester tester) async {
const double toolbarHeight = 100.0;
const double collapsedHeight = 150.0;
await tester.pumpWidget(buildSliverAppBarApp(
floating: false,
pinned: false,
toolbarHeight: toolbarHeight,
collapsedHeight: collapsedHeight
));
final ScrollController controller = primaryScrollController(tester);
final double initialTabBarHeight = tabBarHeight(tester);
// Scroll the not-pinned appbar out of view, to its collapsed height.
controller.jumpTo(300.0);
await tester.pump();
expect(find.byType(SliverAppBar), findsNothing);
expect(appBarHeight(tester), collapsedHeight + initialTabBarHeight);
});
test('SliverApp toolbarHeight cannot be null', () {
try{
SliverAppBar(
toolbarHeight: null,
);
} on AssertionError catch (error) {
expect(error.toString(), contains('toolbarHeight != null'));
expect(error.toString(), contains('is not true'));
}
});
} }
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