Skip to content

Suggestion: CupertinoSliverNavigationBar allow forcing condensed title #59525

Closed

Description

@cpboyd

Use case

See: iOS 13 Settings app search, App Store Search, or News app's Following tab search.

While searching and viewing results, the title is hidden.

My thoughts were something like this:

  Widget _buildSliverBar(BuildContext context) {
    return CupertinoSliverNavigationBar(
      middle: _isSearching
          ? TextField(
              autofocus: true,
              controller: _filter,
              decoration: InputDecoration(
                  hintText: 'Search by name...', border: InputBorder.none),
            )
          : null,
      largeTitle: _isSearching ? null : _buildTitle(),
      trailing: IconButton(
        icon: Icon(_isSearching ? Icons.clear : Icons.search),
        onPressed: () {
          setState(() {
            _isSearching = !_isSearching;
            if (!_isSearching) {
              _resetTitle();
            }
          });
        },
      ),
    );
  }

Alternatives: The built-in iOS apps that I mentioned probably actually behave by hiding the title bar and having a separate pinned search bar, e.g. SliverPersistentHeader. SliverPersistentHeader, however, doesn't seem to be very easily extensible.

SliverAppBar has a bottom option for easily appending a widget as well as allowing flexibleSpace (analogous to largeTitle) to be set to null, while placing widgets in title (analogous to middle).

Proposal

Proposal:

Simulator Screen Shot - iPhone SE (2nd generation) - 2020-06-15 at 23 33 49

Apple News app

IMG_0512
IMG_0513
IMG_0514

Activity

  1. added
    a: fidelityMatching the OEM platforms better
    p: cupertino_uicupertino_ui package in flutter/packages
    frameworkflutter/packages/flutter repository. See also f: labels.
    c: proposalA detailed proposal for a change to Flutter
    on Jun 16, 2020
  2. cpboyd commented on Jun 26, 2020

    @cpboyd
    Author

    Perhaps a more flexible way to implement this would be to publicly expose the large bool and allow the user to specify false to force a condensed title?

  3. cpboyd commented on Jun 26, 2020

    @cpboyd
    Author

    As an example: cpboyd@972b591

    I made some basic necessary changes to getting this to work, including those related to calculating the components.

    large basically allows you to specify false to force a condensed title.

  4. changed the title [-]Suggestion: CupertinoSliverNavigationBar allow null largeTitle to hide[/-] [+]Suggestion: CupertinoSliverNavigationBar allow forcing condensed title[/+] on Jun 26, 2020
  5. Piinks commented on Aug 22, 2024

    @Piinks
    Contributor

    The native Messages has a good example of this. In the all messages page, the navigation bar is always collapsed. CupertinoSliverNavigationBar should have this option too. 👍

  6. victorsanni commented on Oct 14, 2024

    @victorsanni
    Contributor

    I've been investigating this behavior in stock iOS apps. So far, it looks like when the search bar is tapped/focused, it rises (or autoscrolls) to the collapsed navigation bar. The search bar becomes the middle, the 'cancel' button becomes the trailing, while there is no leading:

    collapsed.search.bar.mov

    I wonder if after #18103 is completed (and there is a CupertinoSliverNavigationBar.search constructor), we can internally bake in that on tap/on focused behavior and modify the collapsed navigation bar as needed.

  7. victorsanni commented on Nov 7, 2024

    @victorsanni
    Contributor

    Playing around with it a little more, I'm increasingly thinking that it is a hero animation between the route with the CupertinoSliverNavigationBar.search and a new route with a CupertinoNavigationBar (with a CupertinoSearchTextField middle and a CupertinoButton trailing). In this case, how do we bake this transition into the framework?

  8. added
    P3Issues that are less important to the Flutter project
    on Nov 7, 2024
  9. victorsanni commented on Nov 11, 2024

    @victorsanni
    Contributor

    Made progress on the forward transition animation, but still having difficulties with the reverse:

    Flutter

    flutter.current.searchable.hero.mp4

    Native

    native.searchable.hero.mp4
    Sample code
    import 'package:flutter/cupertino.dart';
    
    /// Flutter code sample for [CupertinoSliverNavigationBar].
    
    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 StatefulWidget {
      const SliverNavBarExample({super.key});
    
      @override
      State<SliverNavBarExample> createState() => _SliverNavBarExampleState();
    }
    
    class _SliverNavBarExampleState extends State<SliverNavBarExample> {
      @override
      Widget build(BuildContext context) {
        return CupertinoPageScaffold(
          child: CustomScrollView(
            slivers: <Widget>[
              CupertinoSliverNavigationBar(
                stretch: true,
                transitionBetweenRoutes: false,
                leading: const Icon(CupertinoIcons.person_2),
                largeTitle: const Text('Large Title'),
                trailing: const Icon(CupertinoIcons.add_circled),
                bottom: _NavigationBarSearchField(
                  onTap: () {
                    Navigator.of(context).push(
                      PageRouteBuilder(
                        pageBuilder: (context, animation1, animation2) =>
                            const NextPage(),
                        transitionsBuilder:
                            (context, animation, secondaryAnimation, child) {
                          var tween =
                              Tween(begin: const Offset(0.0, 0.1), end: Offset.zero)
                                  .chain(CurveTween(curve: Curves.ease));
                          return SlideTransition(
                            position: animation.drive(tween),
                            child: child,
                          );
                        },
                        transitionDuration: const Duration(milliseconds: 500),
                        reverseTransitionDuration:
                            const Duration(milliseconds: 500),
                      ),
                    );
                  },
                ),
              ),
              const ItemList(),
            ],
          ),
        );
      }
    }
    
    class NextPage extends StatelessWidget {
      const NextPage({super.key});
    
      @override
      Widget build(BuildContext context) {
        return CupertinoPageScaffold(
          navigationBar: CupertinoNavigationBar(
            transitionBetweenRoutes: false,
            automaticallyImplyLeading: false,
            middle: _NavigationBarSearchField(onTap: () {}),
            trailing: CupertinoButton(
              padding: const EdgeInsets.all(0.0),
              child: const Text('Cancel'),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
          ),
          child: const CustomScrollView(
            slivers: <Widget>[ItemList()],
          ),
        );
      }
    }
    
    class _NavigationBarSearchField extends StatelessWidget
        implements PreferredSizeWidget {
      const _NavigationBarSearchField({required this.onTap});
    
      static const double searchFieldHeight = 35.0;
      final VoidCallback onTap;
    
      @override
      Widget build(BuildContext context) {
        return Hero(
          tag: 'hero-search',
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 0.0),
            child: SizedBox(
                height: searchFieldHeight,
                child: CupertinoSearchTextField(
                  onTap: onTap,
                )),
          ),
        );
      }
    
      @override
      Size get preferredSize => const Size.fromHeight(searchFieldHeight);
    }
    
    // A placeholder SliverList of 50 items.
    class ItemList extends StatelessWidget {
      const ItemList({
        super.key,
        this.itemCount = 50,
      });
    
      final int itemCount;
    
      @override
      Widget build(BuildContext context) {
        return SliverList(
          delegate: SliverChildBuilderDelegate(
            (BuildContext context, int index) {
              return CupertinoFormRow(
                child: CupertinoListTile(
                  title: Text('Item $index'),
                ),
              );
            },
            childCount: itemCount,
          ),
        );
      }
    }
    
    
  10. github-actions commented on Feb 21, 2025

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  11. locked as resolved and limited conversation to collaborators on Feb 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P3Issues that are less important to the Flutter projecta: fidelityMatching the OEM platforms betterc: proposalA detailed proposal for a change to Flutterframeworkflutter/packages/flutter repository. See also f: labels.p: cupertino_uicupertino_ui package in flutter/packagesteam-designOwned by Design Languages teamtriaged-designTriaged by Design Languages team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions