mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add bottom to CupertinoSliverNavigationBar (#155841)
Fixes [Support search box inside large title nav bar](https://github.com/flutter/flutter/issues/18103) Part of [Support segmented controls in nav bars and double row nav bars](https://github.com/flutter/flutter/issues/10469) ## None mode (Current default) https://github.com/user-attachments/assets/d798314e-940f-4311-9a9a-fe999c65f280 ## Always mode https://github.com/user-attachments/assets/950a85aa-8ca2-42ea-bf8b-3cb8f95e616e ## Automatic mode https://github.com/user-attachments/assets/c7c7240b-d493-4036-a987-30f61d02bac3 ## With CupertinoSlidingSegmentedControl https://github.com/user-attachments/assets/59f4aec4-8d9c-4223-915b-97b73cb25dc8 <details> <summary>Sample Code</summary> ```dart // 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 'package:flutter/cupertino.dart'; /// Flutter code sample for [CupertinoSliverNavigationBar]. void main() => runApp(const SliverNavBarApp()); class SliverNavBarApp extends StatelessWidget { const SliverNavBarApp({super.key}); @override Widget build(BuildContext context) { return const CupertinoApp( theme: CupertinoThemeData(brightness: Brightness.light), home: SliverNavBarExample(), ); } } class SliverNavBarExample extends StatelessWidget { const SliverNavBarExample({super.key}); @override Widget build(BuildContext context) { return const CupertinoPageScaffold( // A ScrollView that creates custom scroll effects using slivers. child: SafeArea( child: CustomScrollView( // A list of sliver widgets. slivers: <Widget>[ CupertinoSliverNavigationBar( leading: SizedBox( width: 100, child: Row( children: [ Icon(CupertinoIcons.back), Text( 'Lists', style: TextStyle(color: CupertinoColors.activeBlue), ), ], ), ), trailing: Icon(CupertinoIcons.plus), largeTitle: Text('iPhone'), // Change to desired mode. drawerMode: NavigationDrawerMode.none, drawer: Padding( padding: EdgeInsets.fromLTRB(8.0, 0.0, 8.0, 16.0), child: CupertinoSearchTextField(), ), ), SliverFillRemaining( child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Text('Drag me up', textAlign: TextAlign.center), ], ), ), ], ), ), ); } } ``` </details>
This commit is contained in:
parent
e08ad36dd9
commit
293ae2e5ab
@ -0,0 +1,119 @@
|
||||
// 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 'package:flutter/cupertino.dart';
|
||||
|
||||
/// Flutter code sample for [CupertinoSliverNavigationBar].
|
||||
|
||||
void main() => runApp(const SliverNavBarApp());
|
||||
|
||||
class SliverNavBarApp extends StatelessWidget {
|
||||
const SliverNavBarApp({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const CupertinoApp(
|
||||
theme: CupertinoThemeData(brightness: Brightness.light),
|
||||
home: SliverNavBarExample(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SliverNavBarExample extends StatelessWidget {
|
||||
const SliverNavBarExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
const CupertinoSliverNavigationBar(
|
||||
leading: Icon(CupertinoIcons.person_2),
|
||||
largeTitle: Text('Contacts'),
|
||||
trailing: Icon(CupertinoIcons.add_circled),
|
||||
),
|
||||
SliverFillRemaining(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
const Text('Drag me up', textAlign: TextAlign.center),
|
||||
CupertinoButton.filled(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute<Widget>(
|
||||
builder: (BuildContext context) {
|
||||
return const NextPage();
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Bottom Automatic mode'),
|
||||
),
|
||||
CupertinoButton.filled(
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute<Widget>(
|
||||
builder: (BuildContext context) {
|
||||
return const NextPage(
|
||||
bottomMode: NavigationBarBottomMode.always,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Bottom Always mode'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NextPage extends StatelessWidget {
|
||||
const NextPage({super.key, this.bottomMode = NavigationBarBottomMode.automatic});
|
||||
|
||||
final NavigationBarBottomMode bottomMode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Brightness brightness = CupertinoTheme.brightnessOf(context);
|
||||
return CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
CupertinoSliverNavigationBar.search(
|
||||
backgroundColor: CupertinoColors.systemYellow,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: brightness == Brightness.light
|
||||
? CupertinoColors.black
|
||||
: CupertinoColors.white,
|
||||
),
|
||||
),
|
||||
middle: const Text('Contacts Group'),
|
||||
largeTitle: const Text('Family'),
|
||||
bottomMode: bottomMode,
|
||||
),
|
||||
const SliverFillRemaining(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: <Widget>[
|
||||
Text('Drag me up', textAlign: TextAlign.center),
|
||||
Text('Tap on the leading button to navigate back',
|
||||
textAlign: TextAlign.center),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
// 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 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_api_samples/cupertino/nav_bar/cupertino_sliver_nav_bar.1.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
const Offset titleDragUp = Offset(0.0, -100.0);
|
||||
const Offset bottomDragUp = Offset(0.0, -50.0);
|
||||
|
||||
void main() {
|
||||
testWidgets('Collapse and expand CupertinoSliverNavigationBar changes title position', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.SliverNavBarApp(),
|
||||
);
|
||||
|
||||
// Large title is visible and at lower position.
|
||||
expect(tester.getBottomLeft(find.text('Contacts').first).dy, 88.0);
|
||||
await tester.fling(find.text('Drag me up'), titleDragUp, 500.0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Large title is hidden and at higher position.
|
||||
expect(tester.getBottomLeft(find.text('Contacts').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
|
||||
});
|
||||
|
||||
testWidgets('Search field is hidden in bottom automatic mode', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.SliverNavBarApp(),
|
||||
);
|
||||
|
||||
// Navigate to a page with bottom automatic mode.
|
||||
final Finder nextButton = find.text('Bottom Automatic mode');
|
||||
expect(nextButton, findsOneWidget);
|
||||
await tester.tap(nextButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Middle, large title, and search field are visible.
|
||||
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
|
||||
expect(tester.getBottomLeft(find.text('Family').first).dy, 88.0);
|
||||
expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 104.0);
|
||||
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 139.0);
|
||||
|
||||
await tester.fling(find.text('Drag me up'), bottomDragUp, 50.0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Search field is hidden, but large title and middle title are visible.
|
||||
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
|
||||
expect(tester.getBottomLeft(find.text('Family').first).dy, 88.0);
|
||||
expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 104.0);
|
||||
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 104.0);
|
||||
|
||||
await tester.fling(find.text('Drag me up'), titleDragUp, 50.0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Large title and search field are hidden and middle title is visible.
|
||||
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
|
||||
expect(tester.getBottomLeft(find.text('Family').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
|
||||
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 52.0);
|
||||
});
|
||||
|
||||
testWidgets('Search field is always shown in bottom always mode', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.SliverNavBarApp(),
|
||||
);
|
||||
|
||||
// Navigate to a page with bottom always mode.
|
||||
final Finder nextButton = find.text('Bottom Always mode');
|
||||
expect(nextButton, findsOneWidget);
|
||||
await tester.tap(nextButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Middle, large title, and search field are visible.
|
||||
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
|
||||
expect(tester.getBottomLeft(find.text('Family').first).dy, 88.0);
|
||||
expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 104.0);
|
||||
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 139.0);
|
||||
|
||||
await tester.fling(find.text('Drag me up'), titleDragUp, 50.0);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Large title is hidden, but search field and middle title are visible.
|
||||
expect(tester.getBottomLeft(find.text('Contacts Group').first).dy, 30.5);
|
||||
expect(tester.getBottomLeft(find.text('Family').first).dy, 36.0 + 8.0); // Static part + _kNavBarBottomPadding.
|
||||
expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 52.0);
|
||||
expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 87.0);
|
||||
});
|
||||
|
||||
testWidgets('CupertinoSliverNavigationBar with previous route has back button', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.SliverNavBarApp(),
|
||||
);
|
||||
|
||||
// Navigate to the first page.
|
||||
final Finder nextButton1 = find.text('Bottom Automatic mode');
|
||||
expect(nextButton1, findsOneWidget);
|
||||
await tester.tap(nextButton1);
|
||||
await tester.pumpAndSettle();
|
||||
expect(nextButton1, findsNothing);
|
||||
|
||||
// Go back to the previous page.
|
||||
final Finder backButton1 = find.byType(CupertinoButton);
|
||||
expect(backButton1, findsOneWidget);
|
||||
await tester.tap(backButton1);
|
||||
await tester.pumpAndSettle();
|
||||
expect(nextButton1, findsOneWidget);
|
||||
|
||||
// Navigate to the second page.
|
||||
final Finder nextButton2 = find.text('Bottom Always mode');
|
||||
expect(nextButton2, findsOneWidget);
|
||||
await tester.tap(nextButton2);
|
||||
await tester.pumpAndSettle();
|
||||
expect(nextButton2, findsNothing);
|
||||
|
||||
// Go back to the previous page.
|
||||
final Finder backButton2 = find.byType(CupertinoButton);
|
||||
expect(backButton2, findsOneWidget);
|
||||
await tester.tap(backButton2);
|
||||
await tester.pumpAndSettle();
|
||||
expect(nextButton2, findsOneWidget);
|
||||
});
|
||||
}
|
||||
@ -19,8 +19,25 @@ import 'constants.dart';
|
||||
import 'icons.dart';
|
||||
import 'page_scaffold.dart';
|
||||
import 'route.dart';
|
||||
import 'search_field.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
/// Modes that determine how to display the navigation bar's bottom in relation to scroll events.
|
||||
enum NavigationBarBottomMode {
|
||||
/// Enable hiding the bottom in response to scrolling.
|
||||
///
|
||||
/// As scrolling starts, the large title stays pinned while the bottom resizes
|
||||
/// until it is completely consumed. Then, the large title scrolls under the
|
||||
/// persistent navigation bar.
|
||||
automatic,
|
||||
|
||||
/// Always display the bottom regardless of the scroll activity.
|
||||
///
|
||||
/// When scrolled, the bottom stays pinned while the large title scrolls under
|
||||
/// the persistent navigation bar.
|
||||
always,
|
||||
}
|
||||
|
||||
/// Standard iOS navigation bar height without the status bar.
|
||||
///
|
||||
/// This height is constant and independent of accessibility as it is in iOS.
|
||||
@ -667,6 +684,13 @@ class _CupertinoNavigationBarState extends State<CupertinoNavigationBar> {
|
||||
/// ** See code in examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// {@tool dartpad}
|
||||
/// This example shows how to add a bottom (typically a
|
||||
/// [CupertinoSearchTextField]) to a [CupertinoSliverNavigationBar].
|
||||
///
|
||||
/// ** See code in examples/api/lib/cupertino/nav_bar/cupertino_sliver_nav_bar.1.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [CupertinoNavigationBar], an iOS navigation bar for use on non-scrolling
|
||||
@ -697,13 +721,52 @@ class CupertinoSliverNavigationBar extends StatefulWidget {
|
||||
this.transitionBetweenRoutes = true,
|
||||
this.heroTag = _defaultHeroTag,
|
||||
this.stretch = false,
|
||||
this.bottom,
|
||||
this.bottomMode,
|
||||
}) : assert(
|
||||
automaticallyImplyTitle || largeTitle != null,
|
||||
'No largeTitle has been provided but automaticallyImplyTitle is also '
|
||||
'false. Either provide a largeTitle or set automaticallyImplyTitle to '
|
||||
'true.',
|
||||
),
|
||||
assert(
|
||||
bottomMode == null || bottom != null,
|
||||
'A bottomMode was provided without a corresponding bottom.',
|
||||
);
|
||||
|
||||
/// Create a navigation bar for scrolling lists with [bottom] set to a
|
||||
/// [CupertinoSearchTextField] with padding.
|
||||
///
|
||||
/// If [automaticallyImplyTitle] is false, then the [largeTitle] argument is
|
||||
/// required.
|
||||
const CupertinoSliverNavigationBar.search({
|
||||
super.key,
|
||||
this.largeTitle,
|
||||
this.leading,
|
||||
this.automaticallyImplyLeading = true,
|
||||
this.automaticallyImplyTitle = true,
|
||||
this.alwaysShowMiddle = true,
|
||||
this.previousPageTitle,
|
||||
this.middle,
|
||||
this.trailing,
|
||||
this.border = _kDefaultNavBarBorder,
|
||||
this.backgroundColor,
|
||||
this.automaticBackgroundVisibility = true,
|
||||
this.enableBackgroundFilterBlur = true,
|
||||
this.brightness,
|
||||
this.padding,
|
||||
this.transitionBetweenRoutes = true,
|
||||
this.heroTag = _defaultHeroTag,
|
||||
this.stretch = false,
|
||||
this.bottomMode = NavigationBarBottomMode.automatic,
|
||||
}) : assert(
|
||||
automaticallyImplyTitle || largeTitle != null,
|
||||
'No largeTitle has been provided but automaticallyImplyTitle is also '
|
||||
'false. Either provide a largeTitle or set automaticallyImplyTitle to '
|
||||
'true.',
|
||||
),
|
||||
bottom = const _NavigationBarSearchField();
|
||||
|
||||
/// The navigation bar's title.
|
||||
///
|
||||
/// This text will appear in the top static navigation bar when collapsed and
|
||||
@ -794,6 +857,22 @@ class CupertinoSliverNavigationBar extends StatefulWidget {
|
||||
/// {@macro flutter.cupertino.CupertinoNavigationBar.heroTag}
|
||||
final Object heroTag;
|
||||
|
||||
/// A widget to place at the bottom of the large title or static navigation
|
||||
/// bar if there is no large title.
|
||||
///
|
||||
/// Only widgets that implement [PreferredSizeWidget] can be used at the
|
||||
/// bottom of a navigation bar.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [PreferredSize], which can be used to give an arbitrary widget a preferred size.
|
||||
final PreferredSizeWidget? bottom;
|
||||
|
||||
/// Modes that determine how to display the navigation bar's [bottom] and scrolling behavior.
|
||||
///
|
||||
/// Defaults to [NavigationBarBottomMode.automatic] if this is null and a [bottom] is provided.
|
||||
final NavigationBarBottomMode? bottomMode;
|
||||
|
||||
/// True if the navigation bar's background color has no transparency.
|
||||
bool get opaque => backgroundColor?.alpha == 0xFF;
|
||||
|
||||
@ -860,6 +939,9 @@ class _CupertinoSliverNavigationBarState extends State<CupertinoSliverNavigation
|
||||
alwaysShowMiddle: widget.alwaysShowMiddle && widget.middle != null,
|
||||
stretchConfiguration: widget.stretch ? OverScrollHeaderStretchConfiguration() : null,
|
||||
enableBackgroundFilterBlur: widget.enableBackgroundFilterBlur,
|
||||
bottom: widget.bottom ?? const SizedBox.shrink(),
|
||||
bottomMode: widget.bottomMode ?? NavigationBarBottomMode.automatic,
|
||||
bottomHeight: widget.bottom != null ? widget.bottom!.preferredSize.height : 0.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -884,6 +966,9 @@ class _LargeTitleNavigationBarSliverDelegate
|
||||
required this.alwaysShowMiddle,
|
||||
required this.stretchConfiguration,
|
||||
required this.enableBackgroundFilterBlur,
|
||||
required this.bottom,
|
||||
required this.bottomMode,
|
||||
required this.bottomHeight,
|
||||
});
|
||||
|
||||
final _NavigationBarStaticComponentsKeys keys;
|
||||
@ -900,12 +985,15 @@ class _LargeTitleNavigationBarSliverDelegate
|
||||
final double persistentHeight;
|
||||
final bool alwaysShowMiddle;
|
||||
final bool enableBackgroundFilterBlur;
|
||||
final Widget bottom;
|
||||
final NavigationBarBottomMode bottomMode;
|
||||
final double bottomHeight;
|
||||
|
||||
@override
|
||||
double get minExtent => persistentHeight;
|
||||
double get minExtent => persistentHeight + (bottomMode == NavigationBarBottomMode.always ? bottomHeight : 0.0);
|
||||
|
||||
@override
|
||||
double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension;
|
||||
double get maxExtent => persistentHeight + _kNavBarLargeTitleHeightExtension + bottomHeight;
|
||||
|
||||
@override
|
||||
OverScrollHeaderStretchConfiguration? stretchConfiguration;
|
||||
@ -914,6 +1002,10 @@ class _LargeTitleNavigationBarSliverDelegate
|
||||
Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
final double largeTitleThreshold = maxExtent - minExtent - _kNavBarShowLargeTitleThreshold;
|
||||
final bool showLargeTitle = shrinkOffset < largeTitleThreshold;
|
||||
|
||||
// Calculate how much the bottom should shrink.
|
||||
final double bottomShrinkFactor = clampDouble(shrinkOffset / bottomHeight, 0, 1);
|
||||
|
||||
final double shrinkAnimationValue = clampDouble(
|
||||
(shrinkOffset - largeTitleThreshold - _kNavBarScrollUnderAnimationExtent) / _kNavBarScrollUnderAnimationExtent,
|
||||
0,
|
||||
@ -947,50 +1039,68 @@ class _LargeTitleNavigationBarSliverDelegate
|
||||
enableBackgroundFilterBlur: enableBackgroundFilterBlur,
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
top: persistentHeight,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
bottom: 0.0,
|
||||
child: ClipRect(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: _kNavBarEdgePadding,
|
||||
bottom: _kNavBarBottomPadding
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: AnimatedOpacity(
|
||||
opacity: showLargeTitle ? 1.0 : 0.0,
|
||||
duration: _kNavBarTitleFadeDuration,
|
||||
child: Semantics(
|
||||
header: true,
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navLargeTitleTextStyle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: _LargeTitle(
|
||||
child: components.largeTitle,
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
Positioned(
|
||||
top: persistentHeight,
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
bottom: bottomMode == NavigationBarBottomMode.automatic
|
||||
? bottomHeight * (1.0 - bottomShrinkFactor)
|
||||
: 0.0,
|
||||
child: ClipRect(
|
||||
child: Padding(
|
||||
padding: const EdgeInsetsDirectional.only(
|
||||
start: _kNavBarEdgePadding,
|
||||
bottom: _kNavBarBottomPadding
|
||||
),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
bottom: false,
|
||||
child: AnimatedOpacity(
|
||||
opacity: showLargeTitle ? 1.0 : 0.0,
|
||||
duration: _kNavBarTitleFadeDuration,
|
||||
child: Semantics(
|
||||
header: true,
|
||||
child: DefaultTextStyle(
|
||||
style: CupertinoTheme.of(context)
|
||||
.textTheme
|
||||
.navLargeTitleTextStyle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
child: _LargeTitle(
|
||||
child: components.largeTitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
top: 0.0,
|
||||
child: persistentNavigationBar,
|
||||
),
|
||||
if (bottomMode == NavigationBarBottomMode.automatic)
|
||||
Positioned(
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
bottom: 0.0,
|
||||
child: SizedBox(
|
||||
height: bottomHeight * (1.0 - bottomShrinkFactor),
|
||||
child: bottom,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0.0,
|
||||
right: 0.0,
|
||||
top: 0.0,
|
||||
child: persistentNavigationBar,
|
||||
),
|
||||
if (bottomMode == NavigationBarBottomMode.always) SizedBox(height: bottomHeight, child: bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -1038,7 +1148,10 @@ class _LargeTitleNavigationBarSliverDelegate
|
||||
|| persistentHeight != oldDelegate.persistentHeight
|
||||
|| alwaysShowMiddle != oldDelegate.alwaysShowMiddle
|
||||
|| heroTag != oldDelegate.heroTag
|
||||
|| enableBackgroundFilterBlur != oldDelegate.enableBackgroundFilterBlur;
|
||||
|| enableBackgroundFilterBlur != oldDelegate.enableBackgroundFilterBlur
|
||||
|| bottom != oldDelegate.bottom
|
||||
|| bottomMode != oldDelegate.bottomMode
|
||||
|| bottomHeight != oldDelegate.bottomHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@ -2633,3 +2746,24 @@ Widget _navBarHeroFlightShuttleBuilder(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NavigationBarSearchField extends StatelessWidget implements PreferredSizeWidget {
|
||||
const _NavigationBarSearchField();
|
||||
|
||||
static const double padding = 8.0;
|
||||
static const double searchFieldHeight = 35.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: padding, vertical: padding),
|
||||
child: SizedBox(
|
||||
height: searchFieldHeight,
|
||||
child: CupertinoSearchTextField()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => const Size.fromHeight(searchFieldHeight + padding * 2);
|
||||
}
|
||||
|
||||
@ -1888,6 +1888,204 @@ void main() {
|
||||
expect(largeTitleFinder.hitTestable(), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('NavigationBarBottomMode.automatic mode for bottom', (WidgetTester tester) async {
|
||||
const double persistentHeight = 44.0;
|
||||
const double largeTitleHeight = 44.0;
|
||||
const double bottomHeight = 10.0;
|
||||
final ScrollController controller = ScrollController();
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: <Widget>[
|
||||
const CupertinoSliverNavigationBar(
|
||||
largeTitle: Text('Large title'),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(bottomHeight),
|
||||
child: Placeholder(),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 1200.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(controller.offset, 0.0);
|
||||
|
||||
final Finder largeTitleFinder = find.ancestor(
|
||||
of: find.text('Large title').first,
|
||||
matching: find.byType(Padding),
|
||||
).first;
|
||||
final Finder bottomFinder = find.byType(Placeholder);
|
||||
|
||||
// The persistent navigation bar, large title, and search field are all
|
||||
// visible.
|
||||
expect(tester.getTopLeft(largeTitleFinder).dy, persistentHeight);
|
||||
expect(tester.getBottomLeft(largeTitleFinder).dy, persistentHeight + largeTitleHeight);
|
||||
expect(tester.getTopLeft(bottomFinder).dy, 96.0);
|
||||
expect(tester.getBottomLeft(bottomFinder).dy, 96.0 + bottomHeight);
|
||||
|
||||
// Scroll the length of the navigation bar search text field.
|
||||
controller.jumpTo(bottomHeight);
|
||||
await tester.pump();
|
||||
|
||||
// The search field is hidden, but the large title remains visible.
|
||||
expect(tester.getBottomLeft(largeTitleFinder).dy, persistentHeight + largeTitleHeight);
|
||||
expect(tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
|
||||
|
||||
// Scroll until the large title scrolls under the persistent navigation bar.
|
||||
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -400.0), 10.0);
|
||||
await tester.pump();
|
||||
|
||||
// The large title and search field are both hidden.
|
||||
expect(tester.getBottomLeft(largeTitleFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
|
||||
expect(tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
|
||||
|
||||
controller.dispose();
|
||||
});
|
||||
|
||||
testWidgets('NavigationBarBottomMode.always mode for bottom', (WidgetTester tester) async {
|
||||
const double persistentHeight = 44.0;
|
||||
const double largeTitleHeight = 44.0;
|
||||
const double bottomHeight = 10.0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
const CupertinoSliverNavigationBar(
|
||||
largeTitle: Text('Large title'),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(bottomHeight),
|
||||
child: Placeholder(),
|
||||
),
|
||||
bottomMode: NavigationBarBottomMode.always,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 1200.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder largeTitleFinder = find.ancestor(
|
||||
of: find.text('Large title').first,
|
||||
matching: find.byType(Padding),
|
||||
).first;
|
||||
final Finder bottomFinder = find.byType(Placeholder);
|
||||
|
||||
// The persistent navigation bar, large title, and search field are all
|
||||
// visible.
|
||||
expect(tester.getTopLeft(largeTitleFinder).dy, persistentHeight);
|
||||
expect(tester.getBottomLeft(largeTitleFinder).dy, persistentHeight + largeTitleHeight);
|
||||
expect(tester.getTopLeft(bottomFinder).dy, 96.0);
|
||||
expect(tester.getBottomLeft(bottomFinder).dy, 96.0 + bottomHeight);
|
||||
|
||||
// Scroll until the large title scrolls under the persistent navigation bar.
|
||||
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, -400.0), 10.0);
|
||||
await tester.pump();
|
||||
|
||||
// Only the large title is hidden.
|
||||
expect(tester.getBottomLeft(largeTitleFinder).dy - tester.getTopLeft(bottomFinder).dy, 0.0);
|
||||
expect(tester.getTopLeft(bottomFinder).dy, persistentHeight);
|
||||
expect(tester.getBottomLeft(bottomFinder).dy, persistentHeight + bottomHeight);
|
||||
});
|
||||
|
||||
testWidgets('Disallow providing a bottomMode without a corresponding bottom', (WidgetTester tester) async {
|
||||
expect(
|
||||
() => const CupertinoSliverNavigationBar(
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(10.0),
|
||||
child: Placeholder(),
|
||||
),
|
||||
bottomMode: NavigationBarBottomMode.automatic,
|
||||
),
|
||||
returnsNormally,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => const CupertinoSliverNavigationBar(
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(10.0),
|
||||
child: Placeholder(),
|
||||
),
|
||||
),
|
||||
returnsNormally,
|
||||
);
|
||||
|
||||
expect(
|
||||
() => CupertinoSliverNavigationBar(
|
||||
bottomMode: NavigationBarBottomMode.automatic,
|
||||
),
|
||||
throwsA(isA<AssertionError>().having(
|
||||
(AssertionError e) => e.message,
|
||||
'message',
|
||||
contains('A bottomMode was provided without a corresponding bottom.'),
|
||||
)),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('Overscroll when stretched does not resize bottom in automatic mode', (WidgetTester tester) async {
|
||||
const double bottomHeight = 10.0;
|
||||
const double bottomDisplacement = 96.0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
CupertinoApp(
|
||||
home: CupertinoPageScaffold(
|
||||
child: CustomScrollView(
|
||||
slivers: <Widget>[
|
||||
const CupertinoSliverNavigationBar(
|
||||
stretch: true,
|
||||
largeTitle: Text('Large title'),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(bottomHeight),
|
||||
child: Placeholder(),
|
||||
),
|
||||
bottomMode: NavigationBarBottomMode.automatic,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: Container(
|
||||
height: 1200.0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final Finder bottomFinder = find.byType(Placeholder);
|
||||
expect(tester.getTopLeft(bottomFinder).dy, bottomDisplacement);
|
||||
expect(
|
||||
tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy,
|
||||
bottomHeight,
|
||||
);
|
||||
|
||||
// Overscroll to stretch the navigation bar.
|
||||
await tester.fling(find.byType(CustomScrollView), const Offset(0.0, 50.0), 10.0);
|
||||
await tester.pump();
|
||||
|
||||
// The bottom stretches without resizing.
|
||||
expect(tester.getTopLeft(bottomFinder).dy, greaterThan(bottomDisplacement));
|
||||
expect(
|
||||
tester.getBottomLeft(bottomFinder).dy - tester.getTopLeft(bottomFinder).dy,
|
||||
bottomHeight,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class _ExpectStyles extends StatelessWidget {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user