Flutter浮动搜索栏插件material_floating_search_bar的使用
Flutter浮动搜索栏插件material_floating_search_bar的使用
Material Floating Search Bar 是一个 Flutter 实现的可扩展的浮动搜索栏,类似于广泛应用于 Google 应用程序中的持久搜索。
点击 这里 查看完整的示例。
安装
在 pubspec.yaml
文件中添加依赖:
dependencies:
material_floating_search_bar: ^0.3.7
从命令行安装包:
flutter packages get
如果您喜欢这个包,请考虑在 GitHub 和 pub.dev 上给它点赞 ❤️。
使用
FloatingSearchBar
应该放置在您的主内容上方,并允许其填充所有可用空间。
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
// 这由搜索栏本身处理。
resizeToAvoidBottomInset: false,
body: Stack(
fit: StackFit.expand,
children: [
buildMap(),
buildBottomNavigationBar(),
buildFloatingSearchBar(),
],
),
);
}
Widget buildFloatingSearchBar() {
final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
return FloatingSearchBar(
hint: '搜索...',
scrollPadding: const EdgeInsets.only(top: 16, bottom: 56),
transitionDuration: const Duration(milliseconds: 800),
transitionCurve: Curves.easeInOut,
physics: const BouncingScrollPhysics(),
axisAlignment: isPortrait ? 0.0 : -1.0,
openAxisAlignment: 0.0,
width: isPortrait ? 600 : 500,
debounceDelay: const Duration(milliseconds: 500),
onQueryChanged: (query) {
// 调用您的模型、小部件或控制器。
},
// 指定一个自定义过渡来在打开和关闭状态之间进行动画。
transition: CircularFloatingSearchBarTransition(),
actions: [
FloatingSearchBarAction(
showIfOpened: false,
child: CircularButton(
icon: const Icon(Icons.place),
onPressed: () {},
),
),
FloatingSearchBarAction.searchToClear(
showIfClosed: false,
),
],
builder: (context, transition) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Material(
color: Colors.white,
elevation: 4.0,
child: Column(
mainAxisSize: MainAxisSize.min,
children: Colors.accents.map((color) {
return Container(height: 112, color: color);
}).toList(),
),
),
);
},
);
}
带滚动视图的使用
默认情况下,builder
返回的小部件不允许具有无限高度。这是为了使搜索栏能够在用户点击子项区域下方时关闭(例如,当列表项不足以填满整个屏幕时)。
因此,应在所有滚动视图上将 shrinkWrap
设置为 true
并将 physics
设置为 NeverScrollableScrollPhysics
。对于列,应将 mainAxisSize
设置为 MainAxisSize.min
。
如果您不希望这种行为,可以将 isScrollControlled
标志设置为 true
。然后您可以使用展开小部件,如 Scrollables
,但要注意搜索栏可能无法检测到背景区域的点击。
自定义
有许多定制选项:
字段 | 描述 |
---|---|
body |
显示在 FloatingSearchBar 下的小部件。 |
accentColor |
用于进度指示器等元素的颜色。 |
backgroundColor |
卡片的颜色。 |
shadowColor |
当 elevation > 0 时绘制的阴影颜色。 |
iconColor |
覆盖主题图标颜色,以便轻松调整所有 actions 和 leadingActions 的图标颜色。 |
backdropColor |
打开时填充可用空间的颜色。 |
margins |
其父组件边缘的内边距。 |
padding |
卡片的内边距。 |
insets |
leadingActions 、输入字段和 actions 之间的内边距。 |
height |
卡片的高度。 |
elevation |
卡片的阴影。 |
width |
FloatingSearchBar 的宽度。 |
openWidth |
打开时 FloatingSearchBar 的宽度。 |
axisAlignment |
可用宽度大于 maxWidth 时,FloatingSearchBar 的对齐方式。 |
openAxisAlignment |
可用宽度大于 openMaxWidth 时,FloatingSearchBar 的对齐方式。 |
border |
卡片的边框。 |
borderRadius |
卡片的圆角半径。 |
hintStyle |
TextField 中提示文本的样式。 |
queryStyle |
TextField 中输入文本的样式。 |
clearQueryOnClose |
关闭时是否清除当前查询。 |
automaticallyImplyDrawerHamburger |
是否显示汉堡菜单。 |
closeOnBackdropTap |
点击背景时是否关闭 FloatingSearchBar 。 |
automaticallyImplyBackButton |
是否自动显示返回按钮。 |
progress |
LinearProgressIndicator 的进度。 |
transitionDuration |
打开和关闭状态之间动画的持续时间。 |
transitionCurve |
打开和关闭状态之间动画的曲线。 |
debounceDelay |
用户停止输入后调用 onQueryChanged 回调的延迟。 |
title |
TextField 关闭时显示的小部件。 |
hint |
TextField 提示文本的值。 |
actions |
在 TextField 后面显示的一组小部件。 |
leadingActions |
在 TextField 前面显示的一组小部件。 |
onQueryChanged |
TextField 输入变化时的回调。 |
onSubmitted |
用户提交查询时的回调。 |
onFocusChanged |
FloatingSearchBar 获得或失去焦点时的回调。 |
transition |
打开和关闭状态之间使用的过渡。 |
builder |
此 FloatingSearchBar 的主体构建器。 |
controller |
控制此 FloatingSearchBar 的控制器。 |
isScrollControlled |
此 FloatingSearchBar 的主体是否使用自己的滚动视图。 |
initiallyHidden |
初始是否隐藏搜索栏。 |
过渡效果
目前有三种类型的过渡效果:
过渡效果 | 描述 |
---|---|
CircularFloatingSearchBarTransition |
将其子节点剪裁在一个扩展的圆形中。 |
ExpandingFloatingSearchBarTransition |
用 FloatingSearchBar 的背景填充所有可用空间。 |
SlideFadeFloatingSearchBarTransition |
垂直滑动并淡出其子节点。 |
您可以轻松地通过扩展 FloatingSearchBarTransition
来创建自己的自定义过渡效果。
滚动
浮动搜索栏的一个常见行为是在用户向下滚动 Scrollable
时消失,并在向上滚动时重新出现。这可以通过将您的小部件传递给 FloatingSearchBar
的 body
字段轻松实现。这样 FloatingSearchBar
可以监听 ScrollNotifications
。为了使 FloatingSearchBar
不与小部件树中的其他 Scrollable
交互,应该在应与 FloatingSearchBar
交互的每个 Scrollable
上包装一个 FloatingSearchBarScrollNotifier
。
class Home extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return FloatingSearchBar(
// 你的页面或者简单的 Scaffold...
body: IndexedStack(
children: [
MyAwesomePage(),
],
),
);
}
}
class MyAwesomePage extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
/// 包装你的 Scrollable 在 FloatingSearchBarScrollNotifier 中,
/// 表示 FloatingSearchBar 应该对这个 Scrollable 的滚动事件做出反应。
return FloatingSearchBarScrollNotifier(
child: ListView.builder(
itemCount: 42,
itemBuilder: (_, index) => Item('Item $index'),
),
);
}
}
FloatingSearchBarController
FloatingSearchBarController
可用于控制 FloatingSearchBar
。
方法 | 描述 |
---|---|
open() |
展开 FloatingSearchBar 。 |
close() |
关闭 FloatingSearchBar 。 |
show() |
当之前被隐藏时显示 FloatingSearchBar 。 |
hide() |
视觉上隐藏 FloatingSearchBar (滑出屏幕)。 |
query |
设置 InputField 中查询的输入。 |
clear() |
清除查询。 |
Floating Search App Bar
有时,FloatingSearchBar
对于您的使用场景可能不是最合适的搜索方法。因此,还有一个 FloatingSearchAppBar
。它是一个具有简单搜索集成的正常 AppBar
,非常类似于标准的 FloatingSearchBar
。
除了大多数来自 FloatingSearchBar
的字段外,FloatingSearchAppBar
还有以下附加字段:
字段 | 描述 |
---|---|
colorOnScroll |
Scrollable 在 body 中滚动时(即 Scrollable 不在顶部)的条形颜色。 |
liftOnScrollElevation |
Scrollable 在 body 中滚动时(即 Scrollable 不在顶部)的条形阴影。 |
alwaysOpened |
是否始终处于打开状态。 |
hideKeyboardOnDownScroll |
如果 Scrollable 在 body 中滚动,则隐藏键盘并在用户滚动到顶部时再次显示。 |
完整示例
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:implicitly_animated_reorderable_list/implicitly_animated_reorderable_list.dart';
import 'package:implicitly_animated_reorderable_list/transitions.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:material_floating_search_bar/material_floating_search_bar.dart';
import 'package:provider/provider.dart';
import 'place.dart';
import 'search_model.dart';
void main() {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.white,
),
);
runApp(
MaterialApp(
title: 'Material Floating Search Bar Example',
debugShowCheckedModeBanner: false,
theme: ThemeData.light().copyWith(
iconTheme: const IconThemeData(
color: Color(0xFF4d4d4d),
),
floatingActionButtonTheme: const FloatingActionButtonThemeData(
elevation: 4,
),
),
home: Directionality(
textDirection: TextDirection.ltr,
child: ChangeNotifierProvider(
create: (_) => SearchModel(),
child: const Home(),
),
),
),
);
}
class MyApp extends StatelessWidget {
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Home(),
);
}
}
class Home extends StatefulWidget {
const Home({Key? key}) : super(key: key);
[@override](/user/override)
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
final controller = FloatingSearchBarController();
int _index = 0;
int get index => _index;
set index(int value) {
_index = min(value, 2);
_index == 2 ? controller.hide() : controller.show();
setState(() {});
}
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
drawer: Drawer(
child: Container(
width: 200,
),
),
body: buildSearchBar(),
);
}
Widget buildSearchBar() {
final actions = [
FloatingSearchBarAction(
showIfOpened: false,
child: CircularButton(
icon: const Icon(Icons.place),
onPressed: () {},
),
),
FloatingSearchBarAction.searchToClear(
showIfClosed: false,
),
];
final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
return Consumer<SearchModel>(
builder: (context, model, _) => FloatingSearchBar(
automaticallyImplyBackButton: false,
controller: controller,
clearQueryOnClose: true,
hint: '搜索...',
iconColor: Colors.grey,
transitionDuration: const Duration(milliseconds: 800),
transitionCurve: Curves.easeInOutCubic,
physics: const BouncingScrollPhysics(),
axisAlignment: isPortrait ? 0.0 : -1.0,
openAxisAlignment: 0.0,
actions: actions,
progress: model.isLoading,
debounceDelay: const Duration(milliseconds: 500),
onQueryChanged: model.onQueryChanged,
onKeyEvent: (KeyEvent keyEvent) {
if (keyEvent.logicalKey == LogicalKeyboardKey.escape) {
controller.query = "";
controller.close();
}
},
scrollPadding: EdgeInsets.zero,
transition: CircularFloatingSearchBarTransition(spacing: 16),
builder: (context, _) => buildExpandableBody(model),
body: buildBody(),
),
);
}
Widget buildBody() {
return Column(
children: [
Expanded(
child: IndexedStack(
index: min(index, 2),
children: const [
Map(),
SomeScrollableContent(),
FloatingSearchAppBarExample(),
],
),
),
buildBottomNavigationBar(),
],
);
}
Widget buildExpandableBody(SearchModel model) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16),
child: Material(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: ImplicitlyAnimatedList<Place>(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
items: model.suggestions,
insertDuration: const Duration(milliseconds: 700),
itemBuilder: (context, animation, item, i) {
return SizeFadeTransition(
animation: animation,
child: buildItem(context, item),
);
},
updateItemBuilder: (context, animation, item) {
return FadeTransition(
opacity: animation,
child: buildItem(context, item),
);
},
areItemsTheSame: (a, b) => a == b,
),
),
);
}
Widget buildItem(BuildContext context, Place place) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final model = Provider.of<SearchModel>(context, listen: false);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () {
FloatingSearchBar.of(context)?.close();
Future.delayed(
const Duration(milliseconds: 500),
() => model.clear(),
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
SizedBox(
width: 36,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: model.suggestions == history
? const Icon(Icons.history, key: Key('history'))
: const Icon(Icons.place, key: Key('place')),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
place.name,
style: textTheme.subtitle1,
),
const SizedBox(height: 2),
Text(
place.level2Address,
style: textTheme.bodyText2?.copyWith(color: Colors.grey.shade600),
),
],
),
),
],
),
),
),
if (model.suggestions.isNotEmpty && place != model.suggestions.last)
const Divider(height: 0),
],
);
}
Widget buildBottomNavigationBar() {
return BottomNavigationBar(
onTap: (value) => index = value,
currentIndex: index,
elevation: 16,
type: BottomNavigationBarType.fixed,
showUnselectedLabels: true,
backgroundColor: Colors.white,
selectedItemColor: Colors.blue,
selectedFontSize: 11.5,
unselectedFontSize: 11.5,
unselectedItemColor: const Color(0xFF4d4d4d),
items: const [
BottomNavigationBarItem(
icon: Icon(MdiIcons.homeVariantOutline),
label: 'Explore',
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.homeCityOutline),
label: 'Commute',
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.bookmarkOutline),
label: 'Saved',
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.plusCircleOutline),
label: 'Contribute',
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.bellOutline),
label: 'Updates',
),
],
);
}
[@override](/user/override)
void dispose() {
controller.dispose();
super.dispose();
}
}
class Map extends StatelessWidget {
const Map({Key? key}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: [
buildMap(),
buildFabs(),
],
);
}
Widget buildFabs() {
return Align(
alignment: AlignmentDirectional.bottomEnd,
child: Padding(
padding: const EdgeInsetsDirectional.only(bottom: 16, end: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Builder(
builder: (context) => FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SearchBar(),
),
);
},
backgroundColor: Colors.white,
child: const Icon(Icons.gps_fixed, color: Color(0xFF4d4d4d)),
),
),
const SizedBox(height: 16),
FloatingActionButton(
onPressed: () {},
heroTag: "öslkföl",
backgroundColor: Colors.blue,
child: const Icon(Icons.directions),
),
],
),
),
);
}
Widget buildMap() {
return Image.asset(
'assets/map.jpg',
fit: BoxFit.cover,
);
}
}
class SearchBar extends StatefulWidget {
const SearchBar({
Key? key,
}) : super(key: key);
[@override](/user/override)
_SearchBarState createState() => _SearchBarState();
}
class _SearchBarState extends State<SearchBar> {
final FloatingSearchBarController controller = FloatingSearchBarController();
[@override](/user/override)
void dispose() {
controller.dispose();
super.dispose();
}
[@override](/user/override)
Widget build(BuildContext context) {
return Scaffold(
body: FloatingSearchBar(
controller: controller,
title: Text(
"Aschaffenburg",
),
hint: '搜索一个地点',
builder: (context, _) {
return Container();
},
),
);
}
}
class SomeScrollableContent extends StatelessWidget {
const SomeScrollableContent({Key? key}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) {
return FloatingSearchBarScrollNotifier(
child: ListView.separated(
padding: const EdgeInsets.only(top: kToolbarHeight),
itemCount: 100,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
),
);
}
}
class FloatingSearchAppBarExample extends StatelessWidget {
const FloatingSearchAppBarExample({Key? key}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) {
return FloatingSearchAppBar(
title: const Text('Title'),
transitionDuration: const Duration(milliseconds: 800),
color: Colors.greenAccent.shade100,
colorOnScroll: Colors.greenAccent.shade200,
body: ListView.separated(
padding: EdgeInsets.zero,
itemCount: 100,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
),
);
}
}
更多关于Flutter浮动搜索栏插件material_floating_search_bar的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter浮动搜索栏插件material_floating_search_bar的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是如何在Flutter项目中使用material_floating_search_bar
插件的一个示例代码。这个插件允许你在应用中实现一个浮动搜索栏,非常适合需要搜索功能的界面。
首先,确保你的pubspec.yaml
文件中已经添加了material_floating_search_bar
的依赖:
dependencies:
flutter:
sdk: flutter
material_floating_search_bar: ^4.2.0 # 请检查最新版本号
然后运行flutter pub get
来安装依赖。
下面是一个简单的示例,展示如何使用material_floating_search_bar
插件:
import 'package:flutter/material.dart';
import 'package:material_floating_search_bar/material_floating_search_bar.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Floating Search Bar Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
@override
_HomeScreenState createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
final List<String> suggestions = List.generate(20, (i) => "Item ${i + 1}");
String? searchQuery = "";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Floating Search Bar Demo"),
),
body: FloatingSearchBar(
// 搜索栏的控制器
controller: searchQueryController,
// 搜索栏的提示文本
hint: "Search...",
// 搜索栏的初始值
initialValue: searchQuery,
// 搜索栏变化时的回调
onChanged: (value) {
setState(() {
searchQuery = value;
});
// 这里可以处理搜索逻辑,例如过滤列表
},
// 搜索栏提交时的回调
onSubmitted: (value) {
setState(() {
searchQuery = value;
});
// 这里可以处理搜索结果的提交逻辑
},
// 搜索栏的搜索结果列表
leadingActions: [
FloatingSearchBarAction(
icon: Icons.clear,
onPressed: () => setState(() {
searchQueryController.clear();
searchQuery = "";
}),
),
],
trailingActions: [
FloatingSearchBarAction.searchToClear(
showIfOpened: true,
),
],
// 搜索栏下方的主体内容
builder: (context, scrollController) => CustomScrollView(
controller: scrollController,
slivers: <Widget>[
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (searchQuery == null || searchQuery!.isEmpty) {
return ListTile(
title: Text(suggestions[index]),
);
} else {
if (suggestions[index].toLowerCase().contains(searchQuery!.toLowerCase())) {
return ListTile(
title: Text(suggestions[index]),
);
}
}
return null; // 不匹配的项不会被渲染
},
childCount: suggestions.length,
),
),
],
),
// 搜索栏的过渡动画
transitionDuration: const Duration(milliseconds: 300),
transitionCurve: Curves.easeInOut,
// 搜索栏打开时的背景颜色
openAxisAlignment: 0.5,
physics: const BouncingScrollPhysics(),
),
);
}
// 搜索栏的控制器
final TextEditingController searchQueryController = TextEditingController();
@override
void dispose() {
searchQueryController.dispose();
super.dispose();
}
}
在这个示例中,我们创建了一个简单的搜索界面,其中FloatingSearchBar
用于处理搜索栏的显示和交互。搜索栏会根据用户输入的内容动态过滤列表中的项目。我们还添加了清除搜索栏的按钮以及搜索图标。
注意:
searchQueryController
用于管理搜索栏的文本输入。onChanged
和onSubmitted
回调用于处理搜索文本的变化和提交。builder
参数用于构建搜索栏下方的主体内容,这里我们使用了CustomScrollView
来显示一个列表。
希望这个示例对你有所帮助!