diff options
Diffstat (limited to 'lib/mapbox.dart')
-rw-r--r-- | lib/mapbox.dart | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/lib/mapbox.dart b/lib/mapbox.dart new file mode 100644 index 0000000..f7bf91e --- /dev/null +++ b/lib/mapbox.dart @@ -0,0 +1,293 @@ +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:mapbox_search/mapbox_search.dart'; + +class MapBoxPlaceSearchWidget extends StatefulWidget { + MapBoxPlaceSearchWidget({ + required this.apiKey, + required this.onSelected, + // this.onSearch, + this.fontSize, + this.searchHint = 'Search', + required this.context, + this.height, + this.popOnSelect = false, + this.location, + this.country, + }); + + /// True if there is different search screen and you want to pop screen on select + final bool popOnSelect; + + ///To get the height of the page + final BuildContext context; + + /// Height of whole search widget + final double? height; + + /// API Key of the MapBox. + final String apiKey; + + /// The callback that is called when one Place is selected by the user. + final void Function(MapBoxPlace place) onSelected; + + /// The callback that is called when the user taps on the search icon. + // final void Function(MapBoxPlaces place) onSearch; + + /// The point around which you wish to retrieve place information. + final Location? location; + + ///Limits the search to the given country + /// + /// Check the full list of [supported countries](https://docs.mapbox.com/api/search/) for the MapBox API + final String? country; + + ///Search Hint Localization + final String? searchHint; + + ///Font Size + final String? fontSize; + + @override + _MapBoxPlaceSearchWidgetState createState() => + _MapBoxPlaceSearchWidgetState(); +} + +class _MapBoxPlaceSearchWidgetState extends State<MapBoxPlaceSearchWidget> + with SingleTickerProviderStateMixin { + TextEditingController _textEditingController = TextEditingController(); + late AnimationController _animationController; + + + late Animation _containerHeight; + + + late Animation _listOpacity; + + List<MapBoxPlace> _placePredictions = []; + + + + late Timer _debounceTimer; + + + + + @override + void initState() { + _animationController = + AnimationController(vsync: this, duration: Duration(milliseconds: 500)); + + _containerHeight = Tween<double>( + begin: 73, + end: widget.height ?? + MediaQuery.of(widget.context).size.height - 60 ?? + 300) + .animate( + CurvedAnimation( + curve: Interval(0.0, 0.5, curve: Curves.easeInOut), + parent: _animationController, + ), + ); + _listOpacity = Tween<double>( + begin: 0, + end: 1, + ).animate( + CurvedAnimation( + curve: Interval(0.5, 1.0, curve: Curves.easeInOut), + parent: _animationController, + ), + ); + super.initState(); + } + + @override + void dispose() { + _debounceTimer.cancel(); + _animationController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => Container( + padding: EdgeInsets.symmetric(horizontal: 5), + width: MediaQuery.of(context).size.width, + child: _searchContainer( + child: _searchInput(context), + ), + ); + + // Widgets + Widget _searchContainer({Widget? child}) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, _) { + return Container( + height: _containerHeight.value, + decoration: _containerDecoration(), + padding: EdgeInsets.only(left: 0, right: 0, top: 15), + alignment: Alignment.center, + child: Column( + children: <Widget>[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12.0), + child: child, + ), + SizedBox(height: 10), + Expanded( + child: Opacity( + opacity: _listOpacity.value, + child: ListView( + // addSemanticIndexes: true, + // itemExtent: 10, + children: <Widget>[ + for (var places in _placePredictions) + _placeOption(places), + ], + ), + ), + ), + ], + ), + ); + }); + } + + Widget _searchInput(BuildContext context) { + return Center( + child: Row( + children: <Widget>[ + Expanded( + child: TextField( + decoration: _inputStyle(), + controller: _textEditingController, + style: TextStyle( + fontSize: MediaQuery.of(context).size.width * 0.04, + ), + onChanged: (value) async { + // _debounceTimer?.cancel(); + _debounceTimer = Timer( + Duration(milliseconds: 750), + () async { + await _autocompletePlace(value); + if (mounted) { + setState(() {}); + } + }, + ); + }, + ), + ), + Container(width: 15), + GestureDetector( + child: Icon(Icons.search, color: Colors.blue), + onTap: () {}, + ) + ], + ), + ); + } + + Widget _placeOption(MapBoxPlace prediction) { + String? place = prediction.text; + String? fullName = prediction.placeName; + + return MaterialButton( + padding: EdgeInsets.symmetric(horizontal: 10, vertical: 3), + onPressed: () => _selectPlace(prediction), + child: ListTile( + title: Text( + place?.length != 0 ? place!.length < 45 + ? "$place" + : "${place?.replaceRange(45, place.length, "")} ..." : "", + style: TextStyle(fontSize: MediaQuery.of(context).size.width * 0.04), + maxLines: 1, + ), + subtitle: Text( + fullName!, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: MediaQuery.of(context).size.width * 0.03), + maxLines: 1, + ), + contentPadding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 0, + ), + ), + ); + } + + // Styling + InputDecoration _inputStyle() { + return InputDecoration( + hintText: widget.searchHint, + border: InputBorder.none, + contentPadding: EdgeInsets.symmetric(horizontal: 0.0, vertical: 0.0), + ); + } + + BoxDecoration _containerDecoration() { + return BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(6.0)), + boxShadow: [ + BoxShadow(color: Colors.black, blurRadius: 0, spreadRadius: 0) + ], + ); + } + + // Methods + Future _autocompletePlace(String input) async { + /// Will be called when the input changes. Making callbacks to the Places + /// Api and giving the user Place options + /// + if (input.length > 0) { + var placesSearch = PlacesSearch( + apiKey: widget.apiKey, + country: widget.country, + ); + + final predictions = await placesSearch.getPlaces( + input, + location: widget.location, + ); + + await _animationController.animateTo(0.5); + + + + setState(() => _placePredictions = predictions!); + + await _animationController.forward(); + } else { + + await _animationController.animateTo(0.5); + setState(() => _placePredictions = []); + await _animationController.reverse(); + } + } + + void _selectPlace(MapBoxPlace prediction) async { + /// Will be called when a user selects one of the Place options. + // Sets TextField value to be the location selected + _textEditingController.value = TextEditingValue( + text: prediction.placeName.toString(), + selection: TextSelection.collapsed(offset: prediction.placeName!.length), + ); + + // Makes animation + await _animationController.animateTo(0.5); + setState(() { + _placePredictions = []; + + }); + _animationController.reverse(); + + // Calls the `onSelected` callback + widget.onSelected(prediction); + if (widget.popOnSelect) Navigator.pop(context); + } +}
\ No newline at end of file |