material_floating_search_bar_2 0.5.0 material_floating_search_bar_2: ^0.5.0 copied to clipboard
A Flutter implementation of an expandable and animated floating search bar, also known as persistent search.
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:implicitly_animated_reorderable_list_2/implicitly_animated_reorderable_list_2.dart';
import 'package:implicitly_animated_reorderable_list_2/transitions.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:material_floating_search_bar_2/material_floating_search_bar_2.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<SearchModel>(
create: (_) => SearchModel(),
child: const Home(),
),
),
),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
debugShowCheckedModeBanner: false,
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const Home(),
);
}
}
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
final FloatingSearchBarController 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
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
drawer: Drawer(
child: Container(
width: 200,
),
),
body: buildSearchBar(),
);
}
Widget buildSearchBar() {
final List<FloatingSearchBarAction> actions = <FloatingSearchBarAction>[
FloatingSearchBarAction(
child: CircularButton(
icon: const Icon(Icons.place),
onPressed: () {},
),
),
FloatingSearchBarAction.searchToClear(
showIfClosed: false,
),
];
final bool isPortrait =
MediaQuery.of(context).orientation == Orientation.portrait;
return Consumer<SearchModel>(
builder: (BuildContext context, SearchModel model, _) =>
FloatingSearchBar(
automaticallyImplyBackButton: false,
controller: controller,
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: (BuildContext context, _) => buildExpandableBody(model),
body: buildBody(),
),
);
}
Widget buildBody() {
return Column(
children: <Widget>[
Expanded(
child: IndexedStack(
index: min(index, 2),
children: const <Widget>[
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: (BuildContext context, Animation<double> animation,
Place item, _) {
return SizeFadeTransition(
animation: animation,
child: buildItem(context, item),
);
},
updateItemBuilder:
(BuildContext context, Animation<double> animation, Place item) {
return FadeTransition(
opacity: animation,
child: buildItem(context, item),
);
},
areItemsTheSame: (Place a, Place b) => a == b,
),
),
);
}
Widget buildItem(BuildContext context, Place place) {
final ThemeData theme = Theme.of(context);
final TextTheme textTheme = theme.textTheme;
final SearchModel model = Provider.of<SearchModel>(context, listen: false);
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
InkWell(
onTap: () {
FloatingSearchBar.of(context)?.close();
Future<void>.delayed(
const Duration(milliseconds: 500),
() => model.clear(),
);
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: <Widget>[
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: <Widget>[
Text(
place.name,
style: textTheme.titleMedium,
),
const SizedBox(height: 2),
Text(
place.level2Address,
style: textTheme.bodyMedium
?.copyWith(color: Colors.grey.shade600),
),
],
),
),
],
),
),
),
if (model.suggestions.isNotEmpty && place != model.suggestions.last)
const Divider(height: 0),
],
);
}
Widget buildBottomNavigationBar() {
return BottomNavigationBar(
onTap: (int 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>[
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
void dispose() {
controller.dispose();
super.dispose();
}
}
class Map extends StatelessWidget {
const Map({super.key});
@override
Widget build(BuildContext context) {
return Stack(
fit: StackFit.expand,
children: <Widget>[
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: <Widget>[
Builder(
builder: (BuildContext context) => FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute<dynamic>(
builder: (BuildContext context) => const 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({
super.key,
});
@override
State<SearchBar> createState() => _SearchBarState();
}
class _SearchBarState extends State<SearchBar> {
final FloatingSearchBarController controller = FloatingSearchBarController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: FloatingSearchBar(
contextMenuBuilder:
(BuildContext context, EditableTextState editableTextState) {
final List<ContextMenuButtonItem> buttonItems =
editableTextState.contextMenuButtonItems;
return AdaptiveTextSelectionToolbar.buttonItems(
anchors: editableTextState.contextMenuAnchors,
buttonItems: buttonItems,
);
},
controller: controller,
title: const Text(
'Aschaffenburg',
),
hint: 'Search for a place',
builder: (BuildContext context, _) {
return Container();
},
),
);
}
}
class SomeScrollableContent extends StatelessWidget {
const SomeScrollableContent({super.key});
@override
Widget build(BuildContext context) {
return FloatingSearchBarScrollNotifier(
child: ListView.separated(
padding: const EdgeInsets.only(top: kToolbarHeight),
itemCount: 100,
separatorBuilder: (BuildContext context, int index) => const Divider(),
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
),
);
}
}
class FloatingSearchAppBarExample extends StatelessWidget {
const FloatingSearchAppBarExample({super.key});
@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: (BuildContext context, int index) => const Divider(),
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text('Item $index'),
);
},
),
);
}
}