diff --git a/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.1_test.dart b/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.1_test.dart index 3fd0b843d38..25caf27d40a 100644 --- a/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.1_test.dart +++ b/examples/api/test/cupertino/nav_bar/cupertino_sliver_nav_bar.1_test.dart @@ -39,8 +39,8 @@ void main() { // 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); + expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 96.0); + expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 132.0); await tester.fling(find.text('Drag me up'), bottomDragUp, 50.0); await tester.pumpAndSettle(); @@ -48,8 +48,8 @@ void main() { // 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); + expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 96.0); + expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 96.0); await tester.fling(find.text('Drag me up'), titleDragUp, 50.0); await tester.pumpAndSettle(); @@ -60,7 +60,7 @@ void main() { tester.getBottomLeft(find.text('Family').first).dy, 36.0 + 8.0, ); // Static part + _kNavBarBottomPadding. - expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 52.0); + expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 44.0); }); testWidgets('Search field is always shown in bottom always mode', (WidgetTester tester) async { @@ -75,8 +75,8 @@ void main() { // 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); + expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 96.0); + expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 132.0); await tester.fling(find.text('Drag me up'), titleDragUp, 50.0); await tester.pumpAndSettle(); @@ -87,8 +87,8 @@ void main() { 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); + expect(tester.getTopLeft(find.byType(CupertinoSearchTextField)).dy, 44.0); + expect(tester.getBottomLeft(find.byType(CupertinoSearchTextField)).dy, 80.0); }); testWidgets('Opens the search view when the search field is tapped', (WidgetTester tester) async { diff --git a/packages/flutter/lib/src/cupertino/nav_bar.dart b/packages/flutter/lib/src/cupertino/nav_bar.dart index c4b6647cf9d..555f67e6661 100644 --- a/packages/flutter/lib/src/cupertino/nav_bar.dart +++ b/packages/flutter/lib/src/cupertino/nav_bar.dart @@ -66,12 +66,18 @@ const double _kNavBarBackButtonTapWidth = 50.0; /// The width of the 'Cancel' button if the search field in a /// [CupertinoSliverNavigationBar.search] is active. -const double _kSearchFieldCancelButtonWidth = 65.0; +/// +/// Eyeballed on an iPhone 15 simulator running iOS 17.5. +const double _kSearchFieldCancelButtonWidth = 67.0; /// The duration of the animation when the search field in /// [CupertinoSliverNavigationBar.search] is tapped. const Duration _kNavBarSearchDuration = Duration(milliseconds: 300); +/// The curve of the animation when the search field in +/// [CupertinoSliverNavigationBar.search] is tapped. +const Curve _kNavBarSearchCurve = Curves.easeInOut; + /// Title text transfer fade. const Duration _kNavBarTitleFadeDuration = Duration(milliseconds: 150); @@ -1070,13 +1076,15 @@ class CupertinoSliverNavigationBar extends StatefulWidget { /// * [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. + /// Modes that determine how to display the navigation bar's [bottom], or the + /// search field in a [CupertinoSliverNavigationBar.search]. /// - /// Defaults to [NavigationBarBottomMode.automatic] if this is null and a [bottom] is provided. + /// If null, defaults to [NavigationBarBottomMode.automatic] if either a + /// [bottom] is provided or this is a [CupertinoSliverNavigationBar.search]. final NavigationBarBottomMode? bottomMode; /// Called when the search field in [CupertinoSliverNavigationBar.search] - /// is tapped, toggling the search state between active and inactive. + /// is tapped, toggling between an active and an inactive search state. final ValueChanged? onSearchableBottomTap; /// True if the navigation bar's background color has no transparency. @@ -1119,6 +1127,7 @@ class _CupertinoSliverNavigationBarState extends State persistentHeightAnimation; late Animation largeTitleHeightAnimation; bool searchIsActive = false; @@ -1148,6 +1157,7 @@ class _CupertinoSliverNavigationBarState extends State persistentHeightTween = Tween( begin: _kNavBarPersistentHeight, end: 0.0, @@ -1211,17 +1222,20 @@ class _CupertinoSliverNavigationBarState extends State[ Expanded(child: searchField ?? const SizedBox.shrink()), AnimatedBuilder( @@ -2322,7 +2342,7 @@ class _NavigationBarSearchField extends StatelessWidget implements PreferredSize const _NavigationBarSearchField({required this.searchField}); static const double verticalPadding = 8.0; - static const double searchFieldHeight = 35.0; + static const double searchFieldHeight = 36.0; final Widget searchField; @override @@ -2331,9 +2351,10 @@ class _NavigationBarSearchField extends StatelessWidget implements PreferredSize child: FocusableActionDetector( descendantsAreFocusable: false, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: _kNavBarEdgePadding, - vertical: verticalPadding, + padding: const EdgeInsetsDirectional.only( + start: _kNavBarEdgePadding, + end: _kNavBarEdgePadding, + bottom: verticalPadding, ), child: SizedBox(height: searchFieldHeight, child: searchField), ), @@ -2342,7 +2363,7 @@ class _NavigationBarSearchField extends StatelessWidget implements PreferredSize } @override - Size get preferredSize => const Size.fromHeight(searchFieldHeight + verticalPadding * 2); + Size get preferredSize => const Size.fromHeight(searchFieldHeight + verticalPadding); } /// This should always be the first child of Hero widgets. diff --git a/packages/flutter/lib/src/cupertino/search_field.dart b/packages/flutter/lib/src/cupertino/search_field.dart index 80aefa1376e..a6e4fbefeb0 100644 --- a/packages/flutter/lib/src/cupertino/search_field.dart +++ b/packages/flutter/lib/src/cupertino/search_field.dart @@ -118,9 +118,9 @@ class CupertinoSearchTextField extends StatefulWidget { this.padding = const EdgeInsetsDirectional.fromSTEB(5.5, 8, 5.5, 8), this.itemColor = CupertinoColors.secondaryLabel, this.itemSize = 20.0, - this.prefixInsets = const EdgeInsetsDirectional.fromSTEB(6, 0, 0, 3), + this.prefixInsets = const EdgeInsetsDirectional.fromSTEB(6, 8, 0, 8), this.prefixIcon = const Icon(CupertinoIcons.search), - this.suffixInsets = const EdgeInsetsDirectional.fromSTEB(0, 0, 5, 2), + this.suffixInsets = const EdgeInsetsDirectional.fromSTEB(0, 8, 5, 8), this.suffixIcon = const Icon(CupertinoIcons.xmark_circle_fill), this.suffixMode = OverlayVisibilityMode.editing, this.onSuffixTap, @@ -462,6 +462,18 @@ class _CupertinoSearchTextFieldState extends State wit } } + // Animate the top padding so that the contents of the search field + // move upwards when the search text field is resized on scroll. + EdgeInsetsGeometry _animatedInsets(BuildContext context, EdgeInsetsGeometry insets) { + final EdgeInsets currentInsets = insets.resolve(Directionality.of(context)); + final EdgeInsetsGeometry? animatedInsets = EdgeInsetsGeometry.lerp( + insets, + currentInsets.copyWith(top: currentInsets.top / 2), + _fadeExtent, + ); + return animatedInsets ?? insets; + } + @override Widget build(BuildContext context) { final String placeholder = @@ -489,19 +501,10 @@ class _CupertinoSearchTextFieldState extends State wit size: scaledIconSize, ); - // Animate the top padding so that the placeholder and editable text - // move when the search text field is resized on scroll. - final EdgeInsets currentInsets = widget.padding.resolve(Directionality.of(context)); - final EdgeInsetsGeometry? padding = EdgeInsetsGeometry.lerp( - widget.padding, - widget.padding.resolve(Directionality.of(context)).copyWith(top: currentInsets.top / 2), - _fadeExtent, - ); - final Widget prefix = Opacity( opacity: 1.0 - _fadeExtent, child: Padding( - padding: widget.prefixInsets, + padding: _animatedInsets(context, widget.prefixInsets), child: IconTheme(data: iconThemeData, child: widget.prefixIcon), ), ); @@ -509,7 +512,7 @@ class _CupertinoSearchTextFieldState extends State wit final Widget suffix = Opacity( opacity: 1.0 - _fadeExtent, child: Padding( - padding: widget.suffixInsets, + padding: _animatedInsets(context, widget.suffixInsets), child: CupertinoButton( onPressed: widget.onSuffixTap ?? _defaultOnSuffixTap, minSize: 0, @@ -536,7 +539,7 @@ class _CupertinoSearchTextFieldState extends State wit suffixMode: widget.suffixMode, placeholder: placeholder, placeholderStyle: placeholderStyle, - padding: padding ?? widget.padding, + padding: _animatedInsets(context, widget.padding), onChanged: widget.onChanged, onSubmitted: widget.onSubmitted, focusNode: widget.focusNode, diff --git a/packages/flutter/test/cupertino/nav_bar_test.dart b/packages/flutter/test/cupertino/nav_bar_test.dart index 4ea12e7588f..4931efbd22f 100644 --- a/packages/flutter/test/cupertino/nav_bar_test.dart +++ b/packages/flutter/test/cupertino/nav_bar_test.dart @@ -2608,6 +2608,69 @@ void main() { expect(find.widgetWithText(CupertinoButton, 'Cancel'), findsOneWidget); }); + testWidgets('CupertinoSliverNavigationBar.search golden tests', (WidgetTester tester) async { + await tester.binding.setSurfaceSize(const Size(390, 850)); + addTearDown(() => tester.binding.setSurfaceSize(null)); + await tester.pumpWidget( + const CupertinoApp( + home: RepaintBoundary( + child: CustomScrollView( + slivers: [ + CupertinoSliverNavigationBar.search( + largeTitle: Text('Large title'), + searchField: CupertinoSearchTextField(), + ), + SliverFillRemaining(child: SizedBox(height: 300.0)), + ], + ), + ), + ), + ); + + await expectLater( + find.byType(CupertinoSliverNavigationBar), + matchesGoldenFile('nav_bar.search.inactive.png'), + ); + + // Tap the search field. + await tester.tap(find.byType(CupertinoSearchTextField), warnIfMissed: false); + await tester.pump(); + // Pump halfway through the animation. + await tester.pump(const Duration(milliseconds: 150)); + + await expectLater( + find.byType(CupertinoSliverNavigationBar), + matchesGoldenFile('nav_bar.search.transition_forward.png'), + ); + + // Pump to the end of the animation. + await tester.pump(const Duration(milliseconds: 300)); + + await expectLater( + find.byType(CupertinoSliverNavigationBar), + matchesGoldenFile('nav_bar.search.active.png'), + ); + + // Tap the 'Cancel' button to exit the search view. + await tester.tap(find.widgetWithText(CupertinoButton, 'Cancel')); + await tester.pump(); + // Pump halfway through the animation. + await tester.pump(const Duration(milliseconds: 150)); + + await expectLater( + find.byType(CupertinoSliverNavigationBar), + matchesGoldenFile('nav_bar.search.transition_backward.png'), + ); + + // Pump for the duration of the search field animation. + await tester.pump(const Duration(milliseconds: 300)); + + await expectLater( + find.byType(CupertinoSliverNavigationBar), + matchesGoldenFile('nav_bar.search.inactive.png'), + ); + }); + testWidgets('onSearchableBottomTap callback', (WidgetTester tester) async { const Color activeSearchColor = Color(0x0000000A); const Color inactiveSearchColor = Color(0x0000000B); diff --git a/packages/flutter/test/cupertino/search_field_test.dart b/packages/flutter/test/cupertino/search_field_test.dart index 0e2ee08585c..f209d3c8795 100644 --- a/packages/flutter/test/cupertino/search_field_test.dart +++ b/packages/flutter/test/cupertino/search_field_test.dart @@ -83,7 +83,7 @@ void main() { expect( tester.getTopLeft(find.text('initial')) - tester.getTopLeft(find.byType(CupertinoSearchTextField)), - const Offset(31.5, 8.0), + const Offset(31.5, 9.5), ); }); @@ -253,6 +253,32 @@ void main() { expect(find.byIcon(CupertinoIcons.xmark_circle_fill), findsNothing); }); + testWidgets('Default prefix and suffix insets are aligned', (WidgetTester tester) async { + await tester.pumpWidget(const CupertinoApp(home: Center(child: CupertinoSearchTextField()))); + + expect(find.byIcon(CupertinoIcons.search), findsOneWidget); + expect(find.byIcon(CupertinoIcons.xmark_circle_fill), findsNothing); + + await tester.enterText(find.byType(CupertinoSearchTextField), 'text input'); + await tester.pump(); + + expect(find.text('text input'), findsOneWidget); + expect(find.byIcon(CupertinoIcons.search), findsOneWidget); + expect(find.byIcon(CupertinoIcons.xmark_circle_fill), findsOneWidget); + + expect(tester.getTopLeft(find.byIcon(CupertinoIcons.search)), const Offset(6.0, 290.0)); + expect( + tester.getTopLeft(find.byIcon(CupertinoIcons.xmark_circle_fill)), + const Offset(775.0, 290.0), + ); + + expect(tester.getBottomRight(find.byIcon(CupertinoIcons.search)), const Offset(26.0, 310.0)); + expect( + tester.getBottomRight(find.byIcon(CupertinoIcons.xmark_circle_fill)), + const Offset(795.0, 310.0), + ); + }); + testWidgets('clear button shows with right visibility mode', (WidgetTester tester) async { TextEditingController controller = TextEditingController(); addTearDown(controller.dispose);