How to achieve Cupertino Style Navigation Bar with a cupertino search field inside it

1.1k views Asked by At

I am developing an iOS application in flutter. I want a navigation bar which can be expandable. On expand there should be large title on left side and on collapse same title should be on top center. This thing is possible with CupertinoSliverNavigationBar but I want to add a search field under the large title which should only appear when navigation bar should be expanded and on scroll up, first search bar should be scrolled up and then CupertinoSliverNavigationBar. This is default behaviour in many iOS applications. Let me show an example

Default IOS Scrolling

enter image description here

You can notice in example when scroll up then first height of search bar gets decreases then navigation bar collapses and when scroll down then first navigation bar expands then height of search bar increases. This is what I achieved yet Achieved Result

This is my code

CustomScrollView(
                          physics:  const AlwaysScrollableScrollPhysics(),
                          controller: _scrollController,

                          slivers: <Widget> [
                            const CupertinoSliverNavigationBar(
                              largeTitle: Text('Products'),
                              stretch: true,
                              //backgroundColor: Colors.white,
                              border: Border(),

                              trailing: Icon(CupertinoIcons.add,color: CupertinoColors.systemBlue,size: 24,),
                            ),
                            SliverToBoxAdapter(
                              child: Padding(
                                padding: const EdgeInsets.symmetric(horizontal: 15),
                                child: CupertinoSearchTextField(
                                
                                  controller:  _controller,

                                  onSubmitted: (String value) {
                                  },
                                ),
                              ),
                            ),
                            SliverFillRemaining(
                              child: _controller.text.isNotEmpty?
                              paymentList(state.productDataSearch!,state):
                              paymentList(state.productData!,state),
                            ),
                          ],
                        ),

[1]: https://i.stack.imgur.com/nfARA.png

2

There are 2 answers

0
Umar Ghaffar On BEST ANSWER

I achieved this by using NotificationListener and change the height of textfield according to the position of scroll

     return NotificationListener<ScrollNotification>(
                              onNotification: (ScrollNotification scrollInfo) {

                                if (scrollInfo is ScrollUpdateNotification) {

                                  if (scrollInfo.metrics.pixels > previousScrollPosition) {
                                    //print("going up ${scrollInfo.metrics.pixels}");
                                    ///up
                                    if(isVisibleSearchBar > 0 && scrollInfo.metrics.pixels > 0){
                                      setState(() {
                                        isVisibleSearchBar = (40 - scrollInfo.metrics.pixels) >= 0?(40 - scrollInfo.metrics.pixels):0;
                                      });
                                    }
                                  }
                                  else if (scrollInfo.metrics.pixels <= previousScrollPosition) {
                                    //print("going down ${scrollInfo.metrics.pixels}");
                                    ///down
                                    if(isVisibleSearchBar < 40 && scrollInfo.metrics.pixels >= 0 && scrollInfo.metrics.pixels <= 40){
                                      setState(() {
                                        isVisibleSearchBar = (40 - scrollInfo.metrics.pixels) <= 40?(40 - scrollInfo.metrics.pixels):40;
                                      });
                                    }
                                  }
                                  setState(() {
                                    previousScrollPosition = scrollInfo.metrics.pixels;
                                  });
                                }
                                else if (scrollInfo is ScrollEndNotification) {
                                  print("on edn isVisibleSearchBar $isVisibleSearchBar");
                                  Future.delayed(Duration.zero, () {
                                    if(isVisibleSearchBar < 20 && isVisibleSearchBar > 0){

                                      setState(() {
                                        isVisibleSearchBar = 0;
                                        _scrollController.animateTo(60, duration: const Duration(milliseconds: 200), curve: Curves.ease);
                                      });

                                    }
                                    else if(isVisibleSearchBar >= 20 && isVisibleSearchBar <= 40){
                                      setState(() {
                                        isVisibleSearchBar = 40;
                                        _scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.ease);
                                      });
                                    }
                                  });
                                }
                                return true;
                              },
                              child: CustomScrollView(
                                physics:  const AlwaysScrollableScrollPhysics(),
                                controller: _scrollController,
                                anchor:0.06,

                                slivers: <Widget> [

                                  CupertinoSliverNavigationBar(
                                    largeTitle: Column(
                                      crossAxisAlignment: CrossAxisAlignment.start,
                                      children: [
                                        const Text('Products'),
                                        AnimatedContainer(

                                          duration: const Duration(milliseconds: 200),
                                          height: isVisibleSearchBar,
                                          child: Padding(
                                            padding: const EdgeInsets.only(right: 15,top: 3),
                                            child: CupertinoSearchTextField(
                                              onChanged: (val){
                                                print("client $val");
                                                if(val.isNotEmpty){
                                                  EasyDebounce.debounce('search_name_debounce', const Duration(milliseconds: 300), () {
                                                    productBloc.add(SearchPayment(val));
                                                    setState(() {});
                                                  });
                                                }
                                                else{
                                                  EasyDebounce.debounce('search_name_debounce', const Duration(milliseconds: 300), () {
                                                    productBloc.add(const SetInitialSearch());
                                                    setState(() {});
                                                  });
                                                }
                                              },
                                              itemSize:isVisibleSearchBar/2,
                                              prefixIcon: AnimatedOpacity(
                                                duration: const Duration(milliseconds: 200),
                                                opacity: isVisibleSearchBar/40 > 1?1:
                                                 isVisibleSearchBar/40 < 0?0:isVisibleSearchBar/40,
                                                child: const Icon(CupertinoIcons.search),
                                              ),
                                              controller:  _controller,
                                              onSubmitted: (String value) {
                                              },
                                            ),
                                          ),
                                        ),
                                      ],
                                    ),
                                    stretch: true,
                                    middle:  const Text('Products'),
                                    alwaysShowMiddle: false,
                                    backgroundColor: Colors.white,
                                    trailing: const Icon(CupertinoIcons.add,color: CupertinoColors.activeBlue,size: 24,),
                                  ),



                                  SliverToBoxAdapter(
                                    child: SafeArea(
                                      top: false,
                                      child: Scrollbar(
                                        child: _controller.text.isNotEmpty?
                                        paymentList(state.productDataSearch!,state):
                                        paymentList(state.productData!,state),
                                      ),
                                    ),
                                  ),
                                ],
                              ),
                            );
2
AudioBubble On

I know it's been a while, but I stumbled upon this too, and this answer has helped me a lot.

I've used some other code from GitHub for better CupertinoSliverNavigationBar, and adjusted the animations. So here's the modified version of the control:

"Real" cupertino navigation bar:

class SliverNavigationBar extends StatefulWidget {
  final ScrollController scrollController;
  final Widget? largeTitle;
  final Widget? leading;
  final bool? alwaysShowMiddle;
  final String? previousPageTitle;
  final Widget? middle;
  final Widget? trailing;
  final Color color;
  final Color darkColor;
  final bool? transitionBetweenRoutes;
  final double threshold;

  const SliverNavigationBar(
      {super.key,
      required this.scrollController,
      this.transitionBetweenRoutes,
      this.largeTitle,
      this.leading,
      this.alwaysShowMiddle = false,
      this.previousPageTitle,
      this.middle,
      this.trailing,
      this.threshold = 52,
      this.color = Colors.white,
      this.darkColor = Colors.black});

  @override
  State<SliverNavigationBar> createState() => _NavState();
}

class _NavState extends State<SliverNavigationBar> {
  bool _isCollapsed = false;

  @override
  void initState() {
    super.initState();
    widget.scrollController.addListener(() {
      if (widget.scrollController.offset >= widget.threshold && !_isCollapsed) {
        setState(() {
          _isCollapsed = true;
        });
      } else if (widget.scrollController.offset < widget.threshold && _isCollapsed) {
        setState(() {
          _isCollapsed = false;
        });
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final isDark = CupertinoTheme.maybeBrightnessOf(context) == Brightness.dark;

    return CupertinoSliverNavigationBar(
      transitionBetweenRoutes: widget.transitionBetweenRoutes ?? true,
      largeTitle: widget.largeTitle,
      leading: widget.leading,
      trailing: widget.trailing,
      alwaysShowMiddle: widget.alwaysShowMiddle ?? false,
      previousPageTitle: widget.previousPageTitle,
      middle: widget.middle,
      stretch: true,
      backgroundColor: _isCollapsed
          ? isDark
              ? const Color.fromRGBO(45, 45, 45, 0.5)
              : Colors.white.withOpacity(0.5)
          : const SpecialColor(),
      border: Border(
        bottom: BorderSide(
          color: _isCollapsed
              ? isDark
                  ? Colors.white.withOpacity(0.5)
                  : Colors.black.withOpacity(0.5)
              : const SpecialColor(),
          width: 0.0, // 0.0 means one physical pixel
        ),
      ),
    );
  }
}

// SpecialColor to remove CupertinoSliverNavigationBar blur effect
class SpecialColor extends Color {
  const SpecialColor() : super(0x00000000);

  @override
  int get alpha => 0xFF;
}

Searchable navigation bar (child):

class SearchableSliverNavigationBar extends StatefulWidget {
  final Widget? largeTitle;
  final Widget? leading;
  final bool? alwaysShowMiddle;
  final String? previousPageTitle;
  final Widget? middle;
  final Widget? trailing;
  final Color color;
  final Color darkColor;
  final bool? transitionBetweenRoutes;
  final TextEditingController searchController;
  final List<Widget>? children;
  final Function(String)? onChanged;
  final Function(String)? onSubmitted;

  const SearchableSliverNavigationBar(
      {super.key,
      required this.searchController,
      this.children,
      this.onChanged,
      this.onSubmitted,
      this.transitionBetweenRoutes,
      this.largeTitle,
      this.leading,
      this.alwaysShowMiddle = false,
      this.previousPageTitle,
      this.middle,
      this.trailing,
      this.color = Colors.white,
      this.darkColor = Colors.black});

  @override
  State<SearchableSliverNavigationBar> createState() => _NavState();
}

class _NavState extends State<SearchableSliverNavigationBar> {
  final scrollController = ScrollController(initialScrollOffset: 40);
  double previousScrollPosition = 0, isVisibleSearchBar = 0;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
        child: NotificationListener<ScrollNotification>(
      onNotification: (ScrollNotification scrollInfo) {
        if (scrollInfo is ScrollUpdateNotification) {
          if (scrollInfo.metrics.pixels > previousScrollPosition) {
            if (isVisibleSearchBar > 0 && scrollInfo.metrics.pixels > 0) {
              setState(() {
                isVisibleSearchBar = (40 - scrollInfo.metrics.pixels) >= 0 ? (40 - scrollInfo.metrics.pixels) : 0;
              });
            }
          } else if (scrollInfo.metrics.pixels <= previousScrollPosition) {
            if (isVisibleSearchBar < 40 && scrollInfo.metrics.pixels >= 0 && scrollInfo.metrics.pixels <= 40) {
              setState(() {
                isVisibleSearchBar = (40 - scrollInfo.metrics.pixels) <= 40 ? (40 - scrollInfo.metrics.pixels) : 40;
              });
            }
          }
          setState(() {
            previousScrollPosition = scrollInfo.metrics.pixels;
          });
        } else if (scrollInfo is ScrollEndNotification) {
          Future.delayed(Duration.zero, () {
            if (isVisibleSearchBar < 30 && isVisibleSearchBar > 0) {
              setState(() {
                scrollController.animateTo(40, duration: const Duration(milliseconds: 200), curve: Curves.ease);
              });
            } else if (isVisibleSearchBar >= 30 && isVisibleSearchBar <= 40) {
              setState(() {
                scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.ease);
              });
            }
          });
        }
        return true;
      },
      child: CustomScrollView(
        physics: const AlwaysScrollableScrollPhysics(),
        controller: scrollController,
        anchor: 0.055,
        slivers: <Widget>[
          SliverNavigationBar(
            transitionBetweenRoutes: widget.transitionBetweenRoutes,
            leading: widget.leading,
            previousPageTitle: widget.previousPageTitle,
            threshold: 97,
            middle: widget.middle ?? widget.largeTitle,
            largeTitle: Column(
              children: [
                Align(alignment: Alignment.centerLeft, child: widget.largeTitle),
                Container(
                  margin: const EdgeInsets.only(top: 5),
                  height: isVisibleSearchBar,
                  child: Padding(
                    padding: const EdgeInsets.only(right: 15, top: 3),
                    child: CupertinoSearchTextField(
                      onChanged: widget.onChanged,
                      placeholderStyle: TextStyle(
                          fontSize: lerpDouble(13, 17, ((isVisibleSearchBar - 30) / 10).clamp(0.0, 1.0)),
                          color: CupertinoDynamicColor.withBrightness(
                              color: const Color.fromARGB(153, 60, 60, 67)
                                  .withAlpha((((isVisibleSearchBar - 30) / 10).clamp(0.0, 1.0) * 153).round()),
                              darkColor: const Color.fromARGB(153, 235, 235, 245)
                                  .withAlpha((((isVisibleSearchBar - 30) / 10).clamp(0.0, 1.0) * 153).round()))),
                      prefixIcon: AnimatedOpacity(
                        duration: const Duration(milliseconds: 1),
                        opacity: ((isVisibleSearchBar - 30) / 10).clamp(0.0, 1.0),
                        child: Transform.scale(
                            scale: lerpDouble(0.7, 1.0, ((isVisibleSearchBar - 30) / 10).clamp(0.0, 1.0)),
                            child: const Icon(CupertinoIcons.search)),
                      ),
                      controller: widget.searchController,
                      onSubmitted: widget.onSubmitted,
                    ),
                  ),
                ),
              ],
            ),
            scrollController: scrollController,
            alwaysShowMiddle: false,
            trailing: widget.trailing,
          ),
          SliverFillRemaining(
              hasScrollBody: false,
              child: Container(
                  padding: const EdgeInsets.only(bottom: 60),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.center,
                    mainAxisSize: MainAxisSize.max,
                    children: widget.children ?? [],
                  ))),
        ],
      ),
    ));
  }
}

Use it like:

final searchController = TextEditingController();

// ...
SearchableSliverNavigationBar(
      largeTitle: Text('Sample page'),
      searchController: searchController,
      trailing: Icon(CupertinoIcons.gear),
      children: [
           // Whatever, will be put in a column
        ],
    )

https://imgur.com/oTTdUJr