Hibok
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

407 行
12 KiB

  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'package:chat/map/auto_comp_iete_item.dart';
  4. import 'package:chat/map/location_provider.dart';
  5. import 'package:chat/map/location_result.dart';
  6. import 'package:chat/map/map.dart';
  7. import 'package:chat/map/nearby_place.dart';
  8. import 'package:chat/map/rich_suggestion.dart';
  9. import 'package:chat/map/search_input.dart';
  10. import 'package:chat/utils/CustomUI.dart';
  11. import 'package:chat/utils/uuid.dart';
  12. import 'package:flutter/material.dart';
  13. import 'package:google_maps_flutter/google_maps_flutter.dart';
  14. import 'package:http/http.dart' as http;
  15. import 'package:provider/provider.dart';
  16. class LocationPicker extends StatefulWidget {
  17. LocationPicker(
  18. this.apiKey, {
  19. Key key,
  20. this.initialCenter,
  21. this.requiredGPS = true,
  22. });
  23. final String apiKey;
  24. final LatLng initialCenter;
  25. final bool requiredGPS;
  26. @override
  27. LocationPickerState createState() => LocationPickerState();
  28. /// Returns a [LatLng] object of the location that was picked.
  29. ///
  30. /// The [apiKey] argument API key generated from Google Cloud Console.
  31. /// You can get an API key [here](https://cloud.google.com/maps-platform/)
  32. ///
  33. /// [initialCenter] The geographical location that the camera is pointing at.
  34. ///
  35. static Future<LocationResult> pickLocation(
  36. BuildContext context,
  37. String apiKey, {
  38. LatLng initialCenter = const LatLng(45.521563, -122.677433),
  39. bool requiredGPS = true,
  40. }) async {
  41. var results = await Navigator.of(context).push(
  42. MaterialPageRoute<dynamic>(
  43. builder: (BuildContext context) {
  44. return LocationPicker(
  45. apiKey,
  46. initialCenter: initialCenter,
  47. requiredGPS: requiredGPS,
  48. );
  49. },
  50. ),
  51. );
  52. if (results != null && results.containsKey('location')) {
  53. return results['location'];
  54. } else {
  55. return null;
  56. }
  57. }
  58. }
  59. class LocationPickerState extends State<LocationPicker> {
  60. /// Result returned after user completes selection
  61. LocationResult locationResult;
  62. /// Overlay to display autocomplete suggestions
  63. OverlayEntry overlayEntry;
  64. List<NearbyPlace> nearbyPlaces = List();
  65. /// Session token required for autocomplete API call
  66. String sessionToken = Uuid().generateV4();
  67. var mapKey = GlobalKey<MapPickerState>();
  68. var appBarKey = GlobalKey();
  69. var searchInputKey = GlobalKey<SearchInputState>();
  70. bool hasSearchTerm = false;
  71. /// Hides the autocomplete overlay
  72. void clearOverlay() {
  73. if (overlayEntry != null) {
  74. overlayEntry.remove();
  75. overlayEntry = null;
  76. }
  77. }
  78. /// Begins the search process by displaying a "wait" overlay then
  79. /// proceeds to fetch the autocomplete list. The bottom "dialog"
  80. /// is hidden so as to give more room and better experience for the
  81. /// autocomplete list overlay.
  82. void searchPlace(String place) {
  83. if (context == null) return;
  84. clearOverlay();
  85. setState(() => hasSearchTerm = place.length > 0);
  86. if (place.length < 1) return;
  87. final RenderBox renderBox = context.findRenderObject();
  88. Size size = renderBox.size;
  89. final RenderBox appBarBox = appBarKey.currentContext.findRenderObject();
  90. overlayEntry = OverlayEntry(
  91. builder: (context) => Positioned(
  92. top: appBarBox.size.height,
  93. width: size.width,
  94. child: Material(
  95. elevation: 1,
  96. child: Container(
  97. padding: EdgeInsets.symmetric(
  98. vertical: 16,
  99. horizontal: 24,
  100. ),
  101. color: Colors.white,
  102. child: Row(
  103. children: <Widget>[
  104. SizedBox(
  105. height: 24,
  106. width: 24,
  107. child: CircularProgressIndicator(
  108. strokeWidth: 3,
  109. ),
  110. ),
  111. SizedBox(
  112. width: 24,
  113. ),
  114. Expanded(
  115. child: Text(
  116. "Finding place...",
  117. style: TextStyle(
  118. fontSize: 16,
  119. ),
  120. ),
  121. )
  122. ],
  123. ),
  124. ),
  125. ),
  126. ),
  127. );
  128. Overlay.of(context).insert(overlayEntry);
  129. autoCompleteSearch(place);
  130. }
  131. /// Fetches the place autocomplete list with the query [place].
  132. void autoCompleteSearch(String place) {
  133. place = place.replaceAll(" ", "+");
  134. var endpoint =
  135. "https://maps.googleapis.com/maps/api/place/autocomplete/json?" +
  136. "key=${widget.apiKey}&" +
  137. "input={$place}&sessiontoken=$sessionToken";
  138. if (locationResult != null) {
  139. endpoint += "&location=${locationResult.latLng.latitude}," +
  140. "${locationResult.latLng.longitude}";
  141. }
  142. http.get(endpoint).then((response) {
  143. if (response.statusCode == 200) {
  144. Map<String, dynamic> data = jsonDecode(response.body);
  145. List<dynamic> predictions = data['predictions'];
  146. List<RichSuggestion> suggestions = [];
  147. if (predictions.isEmpty) {
  148. AutoCompleteItem aci = AutoCompleteItem();
  149. aci.text = "No result found";
  150. aci.offset = 0;
  151. aci.length = 0;
  152. suggestions.add(RichSuggestion(aci, () {}));
  153. } else {
  154. for (dynamic t in predictions) {
  155. AutoCompleteItem aci = AutoCompleteItem();
  156. aci.id = t['place_id'];
  157. aci.text = t['description'];
  158. aci.offset = t['matched_substrings'][0]['offset'];
  159. aci.length = t['matched_substrings'][0]['length'];
  160. suggestions.add(RichSuggestion(aci, () {
  161. decodeAndSelectPlace(aci.id);
  162. }));
  163. }
  164. }
  165. displayAutoCompleteSuggestions(suggestions);
  166. }
  167. }).catchError((error) {
  168. print(error);
  169. });
  170. }
  171. /// To navigate to the selected place from the autocomplete list to the map,
  172. /// the lat,lng is required. This method fetches the lat,lng of the place and
  173. /// proceeds to moving the map to that location.
  174. void decodeAndSelectPlace(String placeId) {
  175. clearOverlay();
  176. String endpoint =
  177. "https://maps.googleapis.com/maps/api/place/details/json?key=${widget.apiKey}" +
  178. "&placeid=$placeId";
  179. http.get(endpoint).then((response) {
  180. if (response.statusCode == 200) {
  181. Map<String, dynamic> location =
  182. jsonDecode(response.body)['result']['geometry']['location'];
  183. LatLng latLng = LatLng(location['lat'], location['lng']);
  184. moveToLocation(latLng);
  185. }
  186. }).catchError((error) {
  187. print(error);
  188. });
  189. }
  190. /// Display autocomplete suggestions with the overlay.
  191. void displayAutoCompleteSuggestions(List<RichSuggestion> suggestions) {
  192. final RenderBox renderBox = context.findRenderObject();
  193. Size size = renderBox.size;
  194. final RenderBox appBarBox = appBarKey.currentContext.findRenderObject();
  195. clearOverlay();
  196. overlayEntry = OverlayEntry(
  197. builder: (context) => Positioned(
  198. width: size.width,
  199. top: appBarBox.size.height,
  200. child: Material(
  201. elevation: 1,
  202. color: Colors.white,
  203. child: Column(
  204. children: suggestions,
  205. ),
  206. ),
  207. ),
  208. );
  209. Overlay.of(context).insert(overlayEntry);
  210. }
  211. /// Utility function to get clean readable name of a location. First checks
  212. /// for a human-readable name from the nearby list. This helps in the cases
  213. /// that the user selects from the nearby list (and expects to see that as a
  214. /// result, instead of road name). If no name is found from the nearby list,
  215. /// then the road name returned is used instead.
  216. // String getLocationName() {
  217. // if (locationResult == null) {
  218. // return "Unnamed location";
  219. // }
  220. //
  221. // for (NearbyPlace np in nearbyPlaces) {
  222. // if (np.latLng == locationResult.latLng) {
  223. // locationResult.name = np.name;
  224. // return np.name;
  225. // }
  226. // }
  227. //
  228. // return "${locationResult.name}, ${locationResult.locality}";
  229. // }
  230. /// Fetches and updates the nearby places to the provided lat,lng
  231. void getNearbyPlaces(LatLng latLng) {
  232. http
  233. .get("https://maps.googleapis.com/maps/api/place/nearbysearch/json?" +
  234. "key=${widget.apiKey}&" +
  235. "location=${latLng.latitude},${latLng.longitude}&radius=150")
  236. .then((response) {
  237. if (response.statusCode == 200) {
  238. nearbyPlaces.clear();
  239. for (Map<String, dynamic> item
  240. in jsonDecode(response.body)['results']) {
  241. NearbyPlace nearbyPlace = NearbyPlace();
  242. nearbyPlace.name = item['name'];
  243. nearbyPlace.icon = item['icon'];
  244. double latitude = item['geometry']['location']['lat'];
  245. double longitude = item['geometry']['location']['lng'];
  246. LatLng _latLng = LatLng(latitude, longitude);
  247. nearbyPlace.latLng = _latLng;
  248. nearbyPlaces.add(nearbyPlace);
  249. }
  250. }
  251. // to update the nearby places
  252. setState(() {
  253. // this is to require the result to show
  254. hasSearchTerm = false;
  255. });
  256. }).catchError((error) {});
  257. }
  258. /// This method gets the human readable name of the location. Mostly appears
  259. /// to be the road name and the locality.
  260. Future reverseGeocodeLatLng(LatLng latLng) async {
  261. /*
  262. var placeMarks = await Geolocator()
  263. .placemarkFromCoordinates(latLng.latitude, latLng.longitude);
  264. if (placeMarks == null) {
  265. return;
  266. }
  267. Placemark place = placeMarks.first;
  268. print('~~~~~~~~~~~~~~~~~~~~~~~~');
  269. print(place.toString());
  270. setState(() {
  271. locationResult = LocationResult();
  272. locationResult.address = place.name;
  273. locationResult.latLng =
  274. LatLng(place.position.latitude, place.position.longitude);
  275. });
  276. */
  277. var response = await http.get(
  278. "https://maps.googleapis.com/maps/api/geocode/json?latlng=${latLng.latitude},${latLng.longitude}"
  279. "&key=${widget.apiKey}");
  280. if (response.statusCode == 200) {
  281. Map<String, dynamic> responseJson = jsonDecode(response.body);
  282. String road =
  283. responseJson['results'][0]['address_components'][0]['short_name'];
  284. setState(() {
  285. locationResult = LocationResult();
  286. locationResult.address = road;
  287. locationResult.latLng = latLng;
  288. });
  289. }
  290. }
  291. /// Moves the camera to the provided location and updates other UI features to
  292. /// match the location.
  293. void moveToLocation(LatLng latLng) {
  294. mapKey.currentState.mapController.future.then((controller) {
  295. controller.animateCamera(
  296. CameraUpdate.newCameraPosition(
  297. CameraPosition(
  298. target: latLng,
  299. zoom: 18.0,
  300. ),
  301. ),
  302. );
  303. });
  304. reverseGeocodeLatLng(latLng);
  305. getNearbyPlaces(latLng);
  306. }
  307. @override
  308. void dispose() {
  309. mapKey = null;
  310. appBarKey = null;
  311. clearOverlay();
  312. super.dispose();
  313. }
  314. @override
  315. Widget build(BuildContext context) {
  316. return MultiProvider(
  317. providers: [
  318. ChangeNotifierProvider(create: (_) => LocationProvider()),
  319. ],
  320. child: Builder(builder: (context) {
  321. return Scaffold(
  322. appBar: AppBar(
  323. backgroundColor: Colors.white,
  324. iconTheme: IconThemeData(color: Colors.black),
  325. key: appBarKey,
  326. title: SearchInput(
  327. (input) => searchPlace(input),
  328. key: searchInputKey,
  329. ),
  330. leading: CustomUI.buildCustomLeading(context),
  331. ),
  332. body: MapPicker(
  333. initialCenter: widget.initialCenter,
  334. key: mapKey,
  335. apiKey: widget.apiKey,
  336. requiredGPS: widget.requiredGPS,
  337. ),
  338. );
  339. }),
  340. );
  341. }
  342. }