Как создать панель навигации в стиле Купертино с полем поиска в Купертино внутри нее
Я разрабатываю приложение для 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
],
)