material_floating_search_bar 0.2.0-dev+1 material_floating_search_bar: ^0.2.0-dev+1 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/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:provider/provider.dart';
import 'package:material_floating_search_bar/material_floating_search_bar.dart';
import 'place.dart';
import 'search_model.dart';
void main() {
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
systemNavigationBarColor: Colors.white,
),
);
runApp(MaterialFloatingSearchBarExample());
}
class MaterialFloatingSearchBarExample extends StatelessWidget {
@override
Widget build(BuildContext context) {
return 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: ChangeNotifierProvider(
create: (_) => SearchModel(),
child: const Home(),
),
);
}
}
class Home extends StatefulWidget {
const Home({Key key}) : super(key: key);
@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
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, _) {
return FloatingSearchBar(
controller: controller,
clearQueryOnClose: true,
hint: 'Search for a new Place...',
iconColor: Colors.grey,
transitionDuration: const Duration(milliseconds: 800),
transitionCurve: Curves.easeInOutCubic,
physics: const BouncingScrollPhysics(),
axisAlignment: isPortrait ? 0.0 : -1.0,
openAxisAlignment: 0.0,
maxWidth: isPortrait ? 600 : 500,
actions: actions,
progress: model.isLoading,
debounceDelay: const Duration(milliseconds: 500),
onQueryChanged: model.onQueryChanged,
transition: CircularFloatingSearchBarTransition(),
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 Material(
color: Colors.white,
elevation: 4.0,
borderRadius: BorderRadius.circular(8),
child: ImplicitlyAnimatedList<Place>(
shrinkWrap: true,
items: model.suggestions.take(6).toList(),
physics: const NeverScrollableScrollPhysics(),
areItemsTheSame: (a, b) => a == b,
itemBuilder: (context, animation, place, i) {
return SizeFadeTransition(
animation: animation,
child: buildItem(context, place),
);
},
updateItemBuilder: (context, animation, place) {
return FadeTransition(
opacity: animation,
child: buildItem(context, place),
);
},
),
);
}
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),
title: Text('Explore'),
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.homeCityOutline),
title: Text('Commute'),
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.bookmarkOutline),
title: Text('Saved'),
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.plusCircleOutline),
title: Text('Contribute'),
),
BottomNavigationBarItem(
icon: Icon(MdiIcons.bellOutline),
title: Text('Updates'),
),
],
);
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
}
class Map extends StatelessWidget {
const Map({Key key}) : super(key: key);
@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: [
FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.white,
child: const Icon(Icons.gps_fixed, color: Color(0xFF4d4d4d)),
),
const SizedBox(height: 16),
FloatingActionButton(
onPressed: () {},
backgroundColor: Colors.blue,
child: const Icon(Icons.directions),
),
],
),
),
);
}
Widget buildMap() {
return Image.asset(
'assets/map.jpg',
fit: BoxFit.cover,
);
}
}
class SomeScrollableContent extends StatelessWidget {
const SomeScrollableContent({Key key}) : super(key: key);
@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
Widget build(BuildContext context) {
return FloatingSearchAppBar(
title: Text('Title'),
transitionDuration: 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'),
);
},
),
);
}
}