Skip to content

Instantly share code, notes, and snippets.

@rodydavis
Created July 30, 2018 15:19
Show Gist options
  • Save rodydavis/932e9426b53468882717cf0f1b463a72 to your computer and use it in GitHub Desktop.
Save rodydavis/932e9426b53468882717cf0f1b463a72 to your computer and use it in GitHub Desktop.
// Copyright (c) 2017, Spencer. All rights reserved. Use of this source code
// is governed by a BSD-style license that can be found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
typedef AppBar AppBarCallback(BuildContext context);
typedef void TextFieldSubmitCallback(String value);
typedef void SetStateCallback(void fn());
class SearchBar {
/// Whether the search should take place "in the existing search bar", meaning whether it has the same background or a flipped one. Defaults to true.
final bool inBar;
bool isDarkTheme = false;
/// Whether the back button should be colored, if this is false the back button will be Colors.grey.shade400
final bool colorBackButton;
/// Whether or not the search bar should close on submit. Defaults to true.
final bool closeOnSubmit;
/// Whether the text field should be cleared when it is submitted
final bool clearOnSubmit;
/// A callback which should return an AppBar that is displayed until search is started. One of the actions in this AppBar should be a search button which you obtain from SearchBar.getSearchAction(). This will be called every time search is ended, etc. (like a build method on a widget)
final AppBarCallback buildDefaultAppBar;
/// A void callback which takes a string as an argument, this is fired every time the search is submitted. Do what you want with the result.
final TextFieldSubmitCallback onSubmitted;
/// Since this should be inside of a State class, just pass setState to this.
final SetStateCallback setState;
/// Whether or not the search bar should add a clear input button, defaults to true.
final bool showClearButton;
/// What the hintText on the search bar should be. Defaults to 'Search'.
String hintText;
/// The controller to be used in the textField.
TextEditingController controller;
/// Whether search is currently active.
bool _isSearching = false;
/// Whether the clear button should be active (fully colored) or inactive (greyed out)
bool _clearActive = false;
/// The last built default AppBar used for colors and such.
AppBar _defaultAppBar;
VoidCallback clearSubmitOnPressed;
ValueChanged<String> onChanged;
SearchBar(
{@required this.setState,
@required this.buildDefaultAppBar,
this.onSubmitted,
this.clearSubmitOnPressed,
this.controller,
this.hintText = 'Search',
this.inBar = true,
this.onChanged,
this.colorBackButton = true,
this.closeOnSubmit = true,
this.clearOnSubmit = true,
this.isDarkTheme,
this.showClearButton = true}) {
if (this.controller == null) {
this.controller = TextEditingController();
}
// Don't waste resources on listeners for the text controller if the dev
// doesn't want a clear button anyways in the search bar
if (!this.showClearButton) {
return;
}
this.controller.addListener(() {
if (this.controller.text.isEmpty) {
// If clear is already disabled, don't disable it
if (_clearActive) {
setState(() {
_clearActive = false;
});
}
return;
}
// If clear is already enabled, don't enable it
if (!_clearActive) {
setState(() {
_clearActive = true;
});
}
});
}
/// Whether search is currently active.
bool get isSearching => _isSearching;
/// Initializes the search bar.
///
/// This adds a route that listens for onRemove (and stops the search when that happens), and then calls [setState] to rebuild and start the search.
void beginSearch(context) {
ModalRoute.of(context).addLocalHistoryEntry(LocalHistoryEntry(onRemove: () {
setState(() {
_isSearching = false;
});
}));
setState(() {
_isSearching = true;
});
}
/// Builds, saves and returns the default app bar.
///
/// This calls the [buildDefaultAppBar] provided in the constructor, and saves it to [_defaultAppBar].
AppBar buildAppBar(BuildContext context) {
_defaultAppBar = buildDefaultAppBar(context);
return _defaultAppBar;
}
/// Builds the search bar!
///
/// The leading will always be a back button.
/// backgroundColor is determined by the value of inBar
/// title is always a [TextField] with the key 'SearchBarTextField', and various text stylings based on [inBar]. This is also where [onSubmitted] has its listener registered.
///
AppBar buildSearchBar(BuildContext context) {
ThemeData theme = Theme.of(context);
Color barColor = inBar ? _defaultAppBar.backgroundColor : theme.canvasColor;
// Don't provide a color (make it white) if it's in the bar, otherwise color it or set it to grey.
// Color buttonColor = inBar ? null : (colorBackButton ? _defaultAppBar.backgroundColor ?? theme.primaryColor ?? Colors.grey.shade400 : Colors.grey.shade400);
Color buttonColor = Colors.grey;
Color buttonDisabledColor =
inBar ? Color.fromRGBO(255, 255, 255, 0.25) : Colors.grey;
Color textColor = inBar ? Colors.white70 : Colors.black54;
return AppBar(
leading: BackButton(color: buttonColor),
backgroundColor: barColor,
title: Directionality(
textDirection: Directionality.of(context),
child: TextField(
key: Key('SearchBarTextField'),
keyboardType: TextInputType.text,
onChanged: onChanged,
style: TextStyle(
color: isDarkTheme ? null : textColor, fontSize: 16.0),
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(
color: isDarkTheme ? null : textColor, fontSize: 16.0),
border: null),
onSubmitted: (String val) async {
if (closeOnSubmit) {
await Navigator.maybePop(context);
}
if (clearOnSubmit) {
controller.clear();
}
onSubmitted(val);
},
autofocus: true,
controller: controller,
)),
actions: !showClearButton
? <Widget>[
IconButton(
icon: Icon(Icons.cancel),
color: Colors.grey,
onPressed: () async {
controller.clear();
await Navigator.maybePop(context);
clearSubmitOnPressed();
},
)
]
: <Widget>[
// Show an icon if clear is not active, so there's no ripple on tap
IconButton(
icon: Icon(Icons.clear,
color: _clearActive ? buttonColor : buttonDisabledColor),
disabledColor: buttonDisabledColor,
onPressed: !_clearActive
? null
: () {
controller.clear();
}),
],
);
}
/// Returns an [IconButton] suitable for an Action
///
/// Put this inside your [buildDefaultAppBar] method!
IconButton getSearchAction(BuildContext context) {
return IconButton(
icon: Icon(Icons.search),
onPressed: () {
beginSearch(context);
});
}
/// Returns an AppBar based on the value of [_isSearching]
AppBar build(BuildContext context) {
return _isSearching ? buildSearchBar(context) : buildAppBar(context);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment