You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
342 lines
9.8 KiB
342 lines
9.8 KiB
import 'dart:async';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_maps_webservice/places.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:healthcare_user/google_maps_place_picker_mb/google_maps_place_picker.dart';
|
|
import 'package:healthcare_user/google_maps_place_picker_mb/providers/place_provider.dart';
|
|
import 'package:healthcare_user/google_maps_place_picker_mb/providers/search_provider.dart';
|
|
import 'package:healthcare_user/google_maps_place_picker_mb/src/components/prediction_tile.dart';
|
|
import 'package:healthcare_user/google_maps_place_picker_mb/src/components/rounded_frame.dart';
|
|
import 'package:healthcare_user/google_maps_place_picker_mb/src/controllers/autocomplete_search_controller.dart';
|
|
|
|
class AutoCompleteSearch extends StatefulWidget {
|
|
const AutoCompleteSearch(
|
|
{Key? key,
|
|
required this.sessionToken,
|
|
required this.onPicked,
|
|
required this.appBarKey,
|
|
this.hintText = "Search here",
|
|
this.searchingText = "Searching...",
|
|
this.hidden = false,
|
|
this.height = 40,
|
|
this.contentPadding = EdgeInsets.zero,
|
|
this.debounceMilliseconds,
|
|
this.onSearchFailed,
|
|
required this.searchBarController,
|
|
this.autocompleteOffset,
|
|
this.autocompleteRadius,
|
|
this.autocompleteLanguage,
|
|
this.autocompleteComponents,
|
|
this.autocompleteTypes,
|
|
this.strictbounds,
|
|
this.region,
|
|
this.initialSearchString,
|
|
this.searchForInitialValue,
|
|
this.autocompleteOnTrailingWhitespace})
|
|
: super(key: key);
|
|
|
|
final String? sessionToken;
|
|
final String? hintText;
|
|
final String? searchingText;
|
|
final bool hidden;
|
|
final double height;
|
|
final EdgeInsetsGeometry contentPadding;
|
|
final int? debounceMilliseconds;
|
|
final ValueChanged<Prediction> onPicked;
|
|
final ValueChanged<String>? onSearchFailed;
|
|
final SearchBarController searchBarController;
|
|
final num? autocompleteOffset;
|
|
final num? autocompleteRadius;
|
|
final String? autocompleteLanguage;
|
|
final List<String>? autocompleteTypes;
|
|
final List<Component>? autocompleteComponents;
|
|
final bool? strictbounds;
|
|
final String? region;
|
|
final GlobalKey appBarKey;
|
|
final String? initialSearchString;
|
|
final bool? searchForInitialValue;
|
|
final bool? autocompleteOnTrailingWhitespace;
|
|
|
|
@override
|
|
AutoCompleteSearchState createState() => AutoCompleteSearchState();
|
|
}
|
|
|
|
class AutoCompleteSearchState extends State<AutoCompleteSearch> {
|
|
TextEditingController controller = TextEditingController();
|
|
FocusNode focus = FocusNode();
|
|
OverlayEntry? overlayEntry;
|
|
SearchProvider provider = SearchProvider();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (widget.initialSearchString != null) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
controller.text = widget.initialSearchString!;
|
|
if (widget.searchForInitialValue!) {
|
|
_onSearchInputChange();
|
|
}
|
|
});
|
|
}
|
|
controller.addListener(_onSearchInputChange);
|
|
focus.addListener(_onFocusChanged);
|
|
|
|
widget.searchBarController.attach(this);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
controller.removeListener(_onSearchInputChange);
|
|
controller.dispose();
|
|
|
|
focus.removeListener(_onFocusChanged);
|
|
focus.dispose();
|
|
_clearOverlay();
|
|
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return !widget.hidden
|
|
? ChangeNotifierProvider.value(
|
|
value: provider,
|
|
child: RoundedFrame(
|
|
height: widget.height,
|
|
padding: const EdgeInsets.only(right: 10),
|
|
color: Theme.of(context).brightness == Brightness.dark
|
|
? Colors.black54
|
|
: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
elevation: 4.0,
|
|
child: Row(
|
|
children: <Widget>[
|
|
SizedBox(width: 10),
|
|
Icon(Icons.search),
|
|
SizedBox(width: 10),
|
|
Expanded(child: _buildSearchTextField()),
|
|
_buildTextClearIcon(),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
: Container();
|
|
}
|
|
|
|
Widget _buildSearchTextField() {
|
|
return TextField(
|
|
controller: controller,
|
|
focusNode: focus,
|
|
decoration: InputDecoration(
|
|
hintText: widget.hintText,
|
|
border: InputBorder.none,
|
|
errorBorder: InputBorder.none,
|
|
enabledBorder: InputBorder.none,
|
|
focusedBorder: InputBorder.none,
|
|
disabledBorder: InputBorder.none,
|
|
focusedErrorBorder: InputBorder.none,
|
|
isDense: true,
|
|
contentPadding: widget.contentPadding,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildTextClearIcon() {
|
|
return Selector<SearchProvider, String>(
|
|
selector: (_, provider) => provider.searchTerm,
|
|
builder: (_, data, __) {
|
|
if (data.length > 0) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8.0),
|
|
child: GestureDetector(
|
|
child: Icon(
|
|
Icons.clear,
|
|
color: Theme.of(context).brightness == Brightness.dark
|
|
? Colors.white
|
|
: Colors.black,
|
|
),
|
|
onTap: () {
|
|
clearText();
|
|
},
|
|
),
|
|
);
|
|
} else {
|
|
return SizedBox(width: 10);
|
|
}
|
|
});
|
|
}
|
|
|
|
_onSearchInputChange() {
|
|
if (!mounted) return;
|
|
this.provider.searchTerm = controller.text;
|
|
|
|
PlaceProvider provider = PlaceProvider.of(context, listen: false);
|
|
|
|
if (controller.text.isEmpty) {
|
|
provider.debounceTimer?.cancel();
|
|
_searchPlace(controller.text);
|
|
return;
|
|
}
|
|
|
|
if (controller.text.trim() == this.provider.prevSearchTerm.trim()) {
|
|
provider.debounceTimer?.cancel();
|
|
return;
|
|
}
|
|
|
|
if (!widget.autocompleteOnTrailingWhitespace! &&
|
|
controller.text.substring(controller.text.length - 1) == " ") {
|
|
provider.debounceTimer?.cancel();
|
|
return;
|
|
}
|
|
|
|
if (provider.debounceTimer?.isActive ?? false) {
|
|
provider.debounceTimer!.cancel();
|
|
}
|
|
|
|
provider.debounceTimer =
|
|
Timer(Duration(milliseconds: widget.debounceMilliseconds!), () {
|
|
_searchPlace(controller.text.trim());
|
|
});
|
|
}
|
|
|
|
_onFocusChanged() {
|
|
PlaceProvider provider = PlaceProvider.of(context, listen: false);
|
|
provider.isSearchBarFocused = focus.hasFocus;
|
|
provider.debounceTimer?.cancel();
|
|
provider.placeSearchingState = SearchingState.Idle;
|
|
}
|
|
|
|
_searchPlace(String searchTerm) {
|
|
this.provider.prevSearchTerm = searchTerm;
|
|
|
|
_clearOverlay();
|
|
|
|
if (searchTerm.length < 1) return;
|
|
|
|
_displayOverlay(_buildSearchingOverlay());
|
|
|
|
_performAutoCompleteSearch(searchTerm);
|
|
}
|
|
|
|
_clearOverlay() {
|
|
if (overlayEntry != null) {
|
|
overlayEntry!.remove();
|
|
overlayEntry = null;
|
|
}
|
|
}
|
|
|
|
_displayOverlay(Widget overlayChild) {
|
|
_clearOverlay();
|
|
|
|
final RenderBox? appBarRenderBox =
|
|
widget.appBarKey.currentContext!.findRenderObject() as RenderBox?;
|
|
final translation = appBarRenderBox?.getTransformTo(null).getTranslation();
|
|
final Offset offset = translation != null
|
|
? Offset(translation.x, translation.y)
|
|
: Offset(0.0, 0.0);
|
|
final screenWidth = MediaQuery.of(context).size.width;
|
|
|
|
overlayEntry = OverlayEntry(
|
|
builder: (context) => Positioned(
|
|
top: appBarRenderBox!.paintBounds.shift(offset).top +
|
|
appBarRenderBox.size.height,
|
|
left: screenWidth * 0.025,
|
|
right: screenWidth * 0.025,
|
|
child: Material(
|
|
elevation: 4.0,
|
|
child: overlayChild,
|
|
),
|
|
),
|
|
);
|
|
|
|
Overlay.of(context)!.insert(overlayEntry!);
|
|
}
|
|
|
|
Widget _buildSearchingOverlay() {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 24),
|
|
child: Row(
|
|
children: <Widget>[
|
|
SizedBox(
|
|
height: 24,
|
|
width: 24,
|
|
child: CircularProgressIndicator(strokeWidth: 3),
|
|
),
|
|
SizedBox(width: 24),
|
|
Expanded(
|
|
child: Text(
|
|
widget.searchingText ?? "Searching...",
|
|
style: TextStyle(fontSize: 16),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildPredictionOverlay(List<Prediction> predictions) {
|
|
return ListBody(
|
|
children: predictions
|
|
.map(
|
|
(p) => PredictionTile(
|
|
prediction: p,
|
|
onTap: (selectedPrediction) {
|
|
resetSearchBar();
|
|
widget.onPicked(selectedPrediction);
|
|
},
|
|
),
|
|
)
|
|
.toList(),
|
|
);
|
|
}
|
|
|
|
_performAutoCompleteSearch(String searchTerm) async {
|
|
PlaceProvider provider = PlaceProvider.of(context, listen: false);
|
|
|
|
if (searchTerm.isNotEmpty) {
|
|
final PlacesAutocompleteResponse response =
|
|
await provider.places.autocomplete(
|
|
searchTerm,
|
|
sessionToken: widget.sessionToken,
|
|
location: provider.currentPosition == null
|
|
? null
|
|
: Location(
|
|
lat: provider.currentPosition!.latitude,
|
|
lng: provider.currentPosition!.longitude),
|
|
offset: widget.autocompleteOffset,
|
|
radius: widget.autocompleteRadius,
|
|
language: widget.autocompleteLanguage,
|
|
types: widget.autocompleteTypes ?? const [],
|
|
components: widget.autocompleteComponents ?? const [],
|
|
strictbounds: widget.strictbounds ?? false,
|
|
region: widget.region,
|
|
);
|
|
|
|
if (response.errorMessage?.isNotEmpty == true ||
|
|
response.status == "REQUEST_DENIED") {
|
|
if (widget.onSearchFailed != null) {
|
|
widget.onSearchFailed!(response.status);
|
|
}
|
|
return;
|
|
}
|
|
|
|
_displayOverlay(_buildPredictionOverlay(response.predictions));
|
|
}
|
|
}
|
|
|
|
clearText() {
|
|
provider.searchTerm = "";
|
|
controller.clear();
|
|
}
|
|
|
|
resetSearchBar() {
|
|
clearText();
|
|
focus.unfocus();
|
|
}
|
|
|
|
clearOverlay() {
|
|
_clearOverlay();
|
|
}
|
|
}
|