Как создать панель навигации в стиле Купертино с полем поиска в Купертино внутри нее

Я разрабатываю приложение для iOS во флаттере. Мне нужна панель навигации, которую можно расширять. При развертывании слева должен быть большой заголовок, а при сворачивании тот же заголовок должен быть вверху по центру. Это возможно, но я хочу добавить поле поиска под большим заголовком, которое должно появляться только тогда, когда панель навигации должна быть расширена и при прокрутке вверх сначала должна быть прокручена панель поиска, а затемCupertinoSliverNavigationBar. Это поведение по умолчанию во многих приложениях iOS. Позвольте мне показать пример

Прокрутка iOS по умолчанию

В примере вы можете заметить, что при прокрутке вверх первая высота панели поиска уменьшается, затем панель навигации сворачивается, а при прокрутке вниз первая панель навигации расширяется, а затем увеличивается высота панели поиска. Это то, чего я добился, но достигнутый результат

Это мой код

      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),
                            ),
                          ],
                        ),

2 ответа

Я добился этого, используя NotificationListener и изменив высоту текстового поля в зависимости от положения прокрутки.

           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),
                                      ),
                                    ),
                                  ),
                                ],
                              ),
                            );

Я знаю, что прошло много времени, но я тоже наткнулся на это, и этот ответ мне очень помог.

Я использовал другой код с GitHub для улучшения CupertinoSliverNavigationBar и настроил анимацию. Итак, вот модифицированная версия элемента управления:

«Настоящая» навигационная панель Купертино:

      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;
}

Панель навигации с возможностью поиска (дочерняя):

      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 ?? [],
                  ))),
        ],
      ),
    ));
  }
}

Используйте его как:

      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