mirror of
https://github.com/flutter/samples.git
synced 2025-11-08 13:58:47 +00:00
Add Google Maps Place Tracker sample app to flutter/samples. (#20)
Sample app that displays places on a map. Add/edit places. Interact with map. Iterations to follow.
This commit is contained in:
24
place_tracker/lib/main.dart
Normal file
24
place_tracker/lib/main.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'place_map.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
class _Home extends StatelessWidget {
|
||||
const _Home({ Key key }) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PlaceMap(
|
||||
center: const LatLng(45.521563, -122.677433),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
runApp(
|
||||
MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Place Tracker',
|
||||
home: _Home(),
|
||||
)
|
||||
);
|
||||
}
|
||||
46
place_tracker/lib/place.dart
Normal file
46
place_tracker/lib/place.dart
Normal file
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
enum PlaceCategory {
|
||||
favorite,
|
||||
visited,
|
||||
wantToGo,
|
||||
}
|
||||
|
||||
class Place {
|
||||
const Place({
|
||||
@required this.latLng,
|
||||
@required this.name,
|
||||
@required this.category,
|
||||
this.description,
|
||||
this.starRating = 0,
|
||||
}) : assert(latLng != null),
|
||||
assert(name != null),
|
||||
assert(category != null),
|
||||
assert(starRating != null && starRating >= 0 && starRating <= 5);
|
||||
|
||||
final LatLng latLng;
|
||||
final String name;
|
||||
final PlaceCategory category;
|
||||
final String description;
|
||||
final int starRating;
|
||||
|
||||
double get latitude => latLng.latitude;
|
||||
double get longitude => latLng.longitude;
|
||||
|
||||
Place copyWith({
|
||||
LatLng latLng,
|
||||
String name,
|
||||
PlaceCategory category,
|
||||
String description,
|
||||
int starRating,
|
||||
}) {
|
||||
return Place(
|
||||
latLng: latLng ?? this.latLng,
|
||||
name: name ?? this.name,
|
||||
category: category ?? this.category,
|
||||
description: description ?? this.description,
|
||||
starRating: starRating ?? this.starRating,
|
||||
);
|
||||
}
|
||||
}
|
||||
238
place_tracker/lib/place_details.dart
Normal file
238
place_tracker/lib/place_details.dart
Normal file
@@ -0,0 +1,238 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
import 'place.dart';
|
||||
|
||||
class PlaceDetails extends StatefulWidget {
|
||||
const PlaceDetails({
|
||||
Key key,
|
||||
@required this.place,
|
||||
@required this.onChanged,
|
||||
}) : assert(place != null),
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
final Place place;
|
||||
final ValueChanged<Place> onChanged;
|
||||
|
||||
@override
|
||||
PlaceDetailsState createState() => PlaceDetailsState();
|
||||
}
|
||||
|
||||
class PlaceDetailsState extends State<PlaceDetails> {
|
||||
|
||||
Place _place;
|
||||
GoogleMapController _mapController;
|
||||
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _descriptionController = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_place = widget.place;
|
||||
_nameController.text = _place.name;
|
||||
_descriptionController.text = _place.description;
|
||||
return super.initState();
|
||||
}
|
||||
|
||||
void _onMapCreated(GoogleMapController controller) {
|
||||
setState(() {
|
||||
_mapController = controller;
|
||||
_mapController.addMarker(MarkerOptions(position: _place.latLng));
|
||||
});
|
||||
}
|
||||
|
||||
Widget _detailsBody() {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.fromLTRB(24.0, 12.0, 24.0, 12.0),
|
||||
children: <Widget>[
|
||||
_NameTextField(
|
||||
controller: _nameController,
|
||||
onChanged: (String value) {
|
||||
setState(() {
|
||||
_place = _place.copyWith(name: value);
|
||||
});
|
||||
},
|
||||
),
|
||||
_DescriptionTextField(
|
||||
controller: _descriptionController,
|
||||
onChanged: (String value) {
|
||||
setState(() {
|
||||
_place = _place.copyWith(description: value);
|
||||
});
|
||||
},
|
||||
),
|
||||
_StarBar(
|
||||
rating: _place.starRating,
|
||||
onChanged: (int value) {
|
||||
setState(() {
|
||||
_place = _place.copyWith(starRating: value);
|
||||
});
|
||||
},
|
||||
),
|
||||
_Map(
|
||||
center: _place.latLng,
|
||||
mapController: _mapController,
|
||||
onMapCreated: _onMapCreated,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text('${_place.name}'),
|
||||
backgroundColor: Colors.green[700],
|
||||
actions: <Widget>[
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.save, size: 30.0),
|
||||
onPressed: () {
|
||||
widget.onChanged(_place);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: GestureDetector(
|
||||
onTap: () {
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
},
|
||||
child: _detailsBody(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NameTextField extends StatelessWidget {
|
||||
_NameTextField({
|
||||
@required this.controller,
|
||||
@required this.onChanged,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 16.0),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Name',
|
||||
labelStyle: const TextStyle(fontSize: 18.0),
|
||||
),
|
||||
style: const TextStyle(fontSize: 20.0, color: Colors.black87),
|
||||
autocorrect: true,
|
||||
controller: controller,
|
||||
onChanged: (String value) {
|
||||
onChanged(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DescriptionTextField extends StatelessWidget {
|
||||
_DescriptionTextField({
|
||||
@required this.controller,
|
||||
@required this.onChanged,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final ValueChanged<String> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 16.0),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Description',
|
||||
labelStyle: const TextStyle(fontSize: 18.0),
|
||||
),
|
||||
style: const TextStyle(fontSize: 20.0, color: Colors.black87),
|
||||
maxLines: null,
|
||||
autocorrect: true,
|
||||
controller: controller,
|
||||
onChanged: (String value) {
|
||||
onChanged(value);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StarBar extends StatelessWidget {
|
||||
const _StarBar({
|
||||
Key key,
|
||||
@required this.rating,
|
||||
@required this.onChanged,
|
||||
}) : assert(rating != null && rating >= 0 && rating <= 5),
|
||||
super(key: key);
|
||||
|
||||
static const int maxStars = 5;
|
||||
final int rating;
|
||||
final ValueChanged<int> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(maxStars, (int index) {
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.star),
|
||||
iconSize: 40.0,
|
||||
color: rating > index ? Colors.amber : Colors.grey[400],
|
||||
onPressed: () {
|
||||
onChanged(index + 1);
|
||||
},
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Map extends StatelessWidget {
|
||||
_Map({
|
||||
Key key,
|
||||
@required this.center,
|
||||
@required this.mapController,
|
||||
@required this.onMapCreated,
|
||||
}) : assert(center != null);
|
||||
|
||||
final LatLng center;
|
||||
final GoogleMapController mapController;
|
||||
final ArgumentCallback<GoogleMapController> onMapCreated;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
elevation: 4.0,
|
||||
child: SizedBox(
|
||||
width: 340.0,
|
||||
height: 240.0,
|
||||
child: GoogleMap(
|
||||
onMapCreated: onMapCreated,
|
||||
options: GoogleMapOptions(
|
||||
cameraPosition: CameraPosition(
|
||||
target: center,
|
||||
zoom: 16.0,
|
||||
),
|
||||
zoomGesturesEnabled: false,
|
||||
rotateGesturesEnabled: false,
|
||||
tiltGesturesEnabled: false,
|
||||
scrollGesturesEnabled: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
433
place_tracker/lib/place_map.dart
Normal file
433
place_tracker/lib/place_map.dart
Normal file
@@ -0,0 +1,433 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
import 'place.dart';
|
||||
import 'place_stub_data.dart';
|
||||
import 'place_details.dart';
|
||||
|
||||
class PlaceMap extends StatefulWidget {
|
||||
const PlaceMap({
|
||||
Key key,
|
||||
this.center,
|
||||
}) : super(key: key);
|
||||
|
||||
final LatLng center;
|
||||
|
||||
@override
|
||||
PlaceMapState createState() => PlaceMapState();
|
||||
}
|
||||
|
||||
class PlaceMapState extends State<PlaceMap> {
|
||||
|
||||
static BitmapDescriptor _getPlaceMarkerIcon(PlaceCategory category) {
|
||||
// TODO(kenzieschmoll): use custom marker assets.
|
||||
double markerHue;
|
||||
switch (category) {
|
||||
case PlaceCategory.favorite:
|
||||
markerHue = BitmapDescriptor.hueRed;
|
||||
break;
|
||||
case PlaceCategory.visited:
|
||||
markerHue = BitmapDescriptor.hueViolet;
|
||||
break;
|
||||
case PlaceCategory.wantToGo:
|
||||
default:
|
||||
markerHue = BitmapDescriptor.hueAzure;
|
||||
}
|
||||
return BitmapDescriptor.defaultMarkerWithHue(markerHue);
|
||||
}
|
||||
|
||||
static List<Place> _getPlacesForCategory(PlaceCategory category, Map<Marker, Place> places) {
|
||||
return places.values.where((Place place) => place.category == category).toList();
|
||||
}
|
||||
|
||||
GoogleMapController mapController;
|
||||
PlaceCategory _selectedPlaceCategory = PlaceCategory.favorite;
|
||||
Map<Marker, Place> _places = Map<Marker, Place>();
|
||||
Marker _pendingMarker;
|
||||
|
||||
void onMapCreated(GoogleMapController controller) async {
|
||||
mapController = controller;
|
||||
mapController.onInfoWindowTapped.add(_onInfoWindowTapped);
|
||||
|
||||
// Add stub data on creation so we have something interesting to look at.
|
||||
final Map<Marker, Place> places = await _initializeStubPlaces();
|
||||
_zoomToFitPlaces(_getPlacesForCategory(_selectedPlaceCategory, places));
|
||||
}
|
||||
|
||||
Future<Map<Marker, Place>> _initializeStubPlaces() async {
|
||||
await Future.wait(PlaceStubData.places.map((Place place) => _initializeStubPlace(place)));
|
||||
return _places;
|
||||
}
|
||||
|
||||
Future<void> _initializeStubPlace(Place place) async {
|
||||
final Marker marker = await mapController.addMarker(
|
||||
MarkerOptions(
|
||||
position: place.latLng,
|
||||
icon: _getPlaceMarkerIcon(place.category),
|
||||
infoWindowText: InfoWindowText(
|
||||
place.name,
|
||||
'${place.starRating} Star Rating',
|
||||
),
|
||||
visible: place.category == _selectedPlaceCategory,
|
||||
),
|
||||
);
|
||||
_places[marker] = place;
|
||||
}
|
||||
|
||||
void _onInfoWindowTapped(Marker marker) async {
|
||||
_pushPlaceDetailsScreen(marker);
|
||||
}
|
||||
|
||||
Future<void> _pushPlaceDetailsScreen(Marker marker) async {
|
||||
assert(marker != null);
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) {
|
||||
return PlaceDetails(
|
||||
place: _places[marker],
|
||||
onChanged: (Place value) {
|
||||
_updatePlaceAndMarker(value, marker);
|
||||
},
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _updatePlaceAndMarker(Place place, Marker marker) async {
|
||||
_places[marker] = place;
|
||||
|
||||
// Set marker visibility to false to ensure the info window is hidden. Once
|
||||
// the plugin fully supports the Google Maps API, use hideInfoWindow()
|
||||
// instead.
|
||||
await mapController.updateMarker(
|
||||
marker,
|
||||
MarkerOptions(
|
||||
visible: false,
|
||||
),
|
||||
);
|
||||
await mapController.updateMarker(
|
||||
marker,
|
||||
MarkerOptions(
|
||||
infoWindowText: InfoWindowText(
|
||||
place.name,
|
||||
place.starRating != 0
|
||||
? '${place.starRating} Star Rating'
|
||||
: null,
|
||||
),
|
||||
visible: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _updatePlaces(PlaceCategory category) {
|
||||
setState(() {
|
||||
_selectedPlaceCategory = category;
|
||||
_showPlacesForSelectedCategory();
|
||||
});
|
||||
}
|
||||
|
||||
void _showPlacesForSelectedCategory() {
|
||||
_places.forEach((Marker marker, Place place) {
|
||||
mapController.updateMarker(
|
||||
marker,
|
||||
MarkerOptions(
|
||||
visible: place.category == _selectedPlaceCategory,
|
||||
),
|
||||
);
|
||||
});
|
||||
_zoomToFitPlaces(_getPlacesForCategory(_selectedPlaceCategory, _places));
|
||||
}
|
||||
|
||||
void _zoomToFitPlaces(List<Place> places) {
|
||||
// Default min/max values to latitude and longitude of center.
|
||||
double minLat = widget.center.latitude;
|
||||
double maxLat = widget.center.latitude;
|
||||
double minLong = widget.center.longitude;
|
||||
double maxLong = widget.center.longitude;
|
||||
|
||||
for (Place place in places) {
|
||||
minLat = min(minLat, place.latitude);
|
||||
maxLat = max(maxLat, place.latitude);
|
||||
minLong = min(minLong, place.longitude);
|
||||
maxLong = max(maxLong, place.longitude);
|
||||
}
|
||||
|
||||
mapController.animateCamera(
|
||||
CameraUpdate.newLatLngBounds(
|
||||
LatLngBounds(
|
||||
southwest: LatLng(minLat, minLong),
|
||||
northeast: LatLng(maxLat, maxLong),
|
||||
),
|
||||
48.0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onAddPlaceFabPressed() async {
|
||||
Marker newMarker = await mapController.addMarker(
|
||||
MarkerOptions(
|
||||
position: LatLng(
|
||||
mapController.cameraPosition.target.latitude,
|
||||
mapController.cameraPosition.target.longitude,
|
||||
),
|
||||
draggable: true,
|
||||
icon: BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueGreen),
|
||||
),
|
||||
);
|
||||
setState(() {
|
||||
_pendingMarker = newMarker;
|
||||
});
|
||||
}
|
||||
|
||||
void _confirmAddPlace(BuildContext context) async {
|
||||
if (_pendingMarker != null) {
|
||||
await mapController.updateMarker(
|
||||
_pendingMarker,
|
||||
MarkerOptions(
|
||||
icon: _getPlaceMarkerIcon(_selectedPlaceCategory),
|
||||
infoWindowText: InfoWindowText('New Place', null),
|
||||
draggable: false,
|
||||
),
|
||||
);
|
||||
|
||||
// Store a reference to the new marker so that we can pass it to the
|
||||
// snackbar action. We cannot pass [_pendingMarker] since it will get
|
||||
// reset to null.
|
||||
Marker newMarker = _pendingMarker;
|
||||
Scaffold.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
duration: Duration(seconds: 3),
|
||||
content: const Text(
|
||||
'New place added.',
|
||||
style: const TextStyle(fontSize: 16.0)
|
||||
),
|
||||
action: SnackBarAction(
|
||||
label: 'Edit',
|
||||
onPressed: () async {
|
||||
_pushPlaceDetailsScreen(newMarker);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
// Create a new Place and map it to the marker we just added.
|
||||
_places[_pendingMarker] = Place(
|
||||
latLng: _pendingMarker.options.position,
|
||||
name: _pendingMarker.options.infoWindowText.title,
|
||||
category: _selectedPlaceCategory,
|
||||
);
|
||||
_pendingMarker = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _cancelAddPlace() {
|
||||
if (_pendingMarker != null) {
|
||||
mapController.removeMarker(_pendingMarker);
|
||||
setState(() {
|
||||
_pendingMarker = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: const <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(0.0, 0.0, 8.0, 0.0),
|
||||
child: Icon(Icons.pin_drop, size: 24.0),
|
||||
),
|
||||
Text('Place Tracker'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Colors.green[700],
|
||||
),
|
||||
// We need this additional builder here so that we can pass its context to
|
||||
// _AddPlaceButtonBar's onSavePressed callback. This callback shows a
|
||||
// SnackBar and to do this, we need a build context that has Scaffold as
|
||||
// an ancestor.
|
||||
body: Builder(builder: (BuildContext context) {
|
||||
return Center(
|
||||
child: Stack(
|
||||
children: <Widget>[
|
||||
GoogleMap(
|
||||
onMapCreated: onMapCreated,
|
||||
options: GoogleMapOptions(
|
||||
trackCameraPosition: true,
|
||||
cameraPosition: CameraPosition(
|
||||
target: widget.center,
|
||||
zoom: 11.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
_CategoryButtonBar(
|
||||
selectedPlaceCategory: _selectedPlaceCategory,
|
||||
visible: _pendingMarker == null,
|
||||
onChanged: _updatePlaces,
|
||||
),
|
||||
_AddPlaceButtonBar(
|
||||
visible: _pendingMarker != null,
|
||||
onSavePressed: () => _confirmAddPlace(context),
|
||||
onCancelPressed: _cancelAddPlace,
|
||||
),
|
||||
_AddPlaceFab(
|
||||
visible: _pendingMarker == null,
|
||||
onPressed: _onAddPlaceFabPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CategoryButtonBar extends StatelessWidget {
|
||||
const _CategoryButtonBar({
|
||||
Key key,
|
||||
@required this.selectedPlaceCategory,
|
||||
@required this.visible,
|
||||
@required this.onChanged,
|
||||
}) : assert(selectedPlaceCategory != null),
|
||||
assert(visible != null),
|
||||
assert(onChanged != null),
|
||||
super(key: key);
|
||||
|
||||
final PlaceCategory selectedPlaceCategory;
|
||||
final bool visible;
|
||||
final ValueChanged<PlaceCategory> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 14.0),
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ButtonBar(
|
||||
alignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
color: selectedPlaceCategory == PlaceCategory.favorite
|
||||
? Colors.green[700]
|
||||
: Colors.lightGreen,
|
||||
child: const Text(
|
||||
'Favorites',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14.0),
|
||||
),
|
||||
onPressed: () => onChanged(PlaceCategory.favorite),
|
||||
),
|
||||
RaisedButton(
|
||||
color: selectedPlaceCategory == PlaceCategory.visited
|
||||
? Colors.green[700]
|
||||
: Colors.lightGreen,
|
||||
child: const Text(
|
||||
'Visited',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14.0),
|
||||
),
|
||||
onPressed: () => onChanged(PlaceCategory.visited),
|
||||
),
|
||||
RaisedButton(
|
||||
color: selectedPlaceCategory == PlaceCategory.wantToGo
|
||||
? Colors.green[700]
|
||||
: Colors.lightGreen,
|
||||
child: const Text(
|
||||
'Want To Go',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14.0),
|
||||
),
|
||||
onPressed: () => onChanged(PlaceCategory.wantToGo),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddPlaceButtonBar extends StatelessWidget {
|
||||
const _AddPlaceButtonBar({
|
||||
Key key,
|
||||
@required this.visible,
|
||||
@required this.onSavePressed,
|
||||
@required this.onCancelPressed,
|
||||
}) : assert(visible != null),
|
||||
assert(onSavePressed != null),
|
||||
assert(onCancelPressed != null),
|
||||
super(key: key);
|
||||
|
||||
final bool visible;
|
||||
final VoidCallback onSavePressed;
|
||||
final VoidCallback onCancelPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 14.0),
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: ButtonBar(
|
||||
alignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
RaisedButton(
|
||||
color: Colors.blue,
|
||||
child: const Text(
|
||||
'Save',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16.0),
|
||||
),
|
||||
onPressed: onSavePressed,
|
||||
),
|
||||
RaisedButton(
|
||||
color: Colors.red,
|
||||
child: const Text(
|
||||
'Cancel',
|
||||
style: TextStyle(color: Colors.white, fontSize: 16.0),
|
||||
),
|
||||
onPressed: onCancelPressed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AddPlaceFab extends StatelessWidget {
|
||||
const _AddPlaceFab({
|
||||
Key key,
|
||||
@required this.visible,
|
||||
@required this.onPressed,
|
||||
}) : assert(visible != null),
|
||||
assert(onPressed != null),
|
||||
super(key: key);
|
||||
|
||||
final bool visible;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Opacity(
|
||||
opacity: visible ? 1.0 : 0.0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: FloatingActionButton(
|
||||
onPressed: onPressed,
|
||||
materialTapTargetSize: MaterialTapTargetSize.padded,
|
||||
backgroundColor: Colors.green,
|
||||
child: const Icon(Icons.add_location, size: 36.0),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
99
place_tracker/lib/place_stub_data.dart
Normal file
99
place_tracker/lib/place_stub_data.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||
|
||||
import 'place.dart';
|
||||
|
||||
class PlaceStubData {
|
||||
static const List<Place> places = [
|
||||
Place(
|
||||
latLng: LatLng(45.524676, -122.681922),
|
||||
name: 'Deschutes Brewery',
|
||||
description:
|
||||
'Beers brewed on-site & gourmet pub grub in a converted auto-body shop with a fireplace & wood beams.',
|
||||
category: PlaceCategory.favorite,
|
||||
starRating: 5),
|
||||
Place(
|
||||
latLng: LatLng(45.525253, -122.684423),
|
||||
name: 'TILT',
|
||||
description:
|
||||
'This stylish American eatery offers unfussy breakfast fare, cocktails & burgers in industrial-themed digs.',
|
||||
category: PlaceCategory.favorite,
|
||||
starRating: 4),
|
||||
Place(
|
||||
latLng: LatLng(45.528952, -122.698344),
|
||||
name: 'Salt & Straw',
|
||||
description:
|
||||
'Quirky flavors & handmade waffle cones draw crowds to this artisinal ice cream maker\'s 3 parlors.',
|
||||
category: PlaceCategory.favorite,
|
||||
starRating: 5),
|
||||
Place(
|
||||
latLng: LatLng(45.513485, -122.657982),
|
||||
name: 'White Owl Social Club',
|
||||
description:
|
||||
'Chill haunt with local beers, burgers & vegan eats, plus live music & an airy patio with a fire pit.',
|
||||
category: PlaceCategory.favorite,
|
||||
starRating: 4),
|
||||
Place(
|
||||
latLng: LatLng(45.383030, -122.758372),
|
||||
name: 'Thai Cuisine',
|
||||
description:
|
||||
'Informal restaurant offering Thai standards in a modest setting, plus takeout & delivery.',
|
||||
category: PlaceCategory.visited,
|
||||
starRating: 4),
|
||||
Place(
|
||||
latLng: LatLng(45.416986, -122.743171),
|
||||
name: 'Chevys',
|
||||
description:
|
||||
'Lively, informal Mexican chain with a colorful, family-friendly setting plus tequilas & margaritas.',
|
||||
category: PlaceCategory.visited,
|
||||
starRating: 4),
|
||||
Place(
|
||||
latLng: LatLng(45.430489, -122.831802),
|
||||
name: 'Cinetopia',
|
||||
description:
|
||||
'Moviegoers can take food from the on-site eatery to their seats, with table service in 21+ theaters.',
|
||||
category: PlaceCategory.visited,
|
||||
starRating: 4),
|
||||
Place(
|
||||
latLng: LatLng(45.487137, -122.799940),
|
||||
name: 'Buffalo Wild Wings',
|
||||
description:
|
||||
'Lively sports-bar chain dishing up wings & other American pub grub amid lots of large-screen TVs.',
|
||||
category: PlaceCategory.visited,
|
||||
starRating: 5),
|
||||
Place(
|
||||
latLng: LatLng(45.493321, -122.669330),
|
||||
name: 'The Old Spaghetti Factory',
|
||||
description:
|
||||
'Family-friendly chain eatery featuring traditional Italian entrees amid turn-of-the-century decor.',
|
||||
category: PlaceCategory.visited,
|
||||
starRating: 4),
|
||||
Place(
|
||||
latLng: LatLng(45.548606, -122.675286),
|
||||
name: 'Mississippi Pizza',
|
||||
description:
|
||||
'Music, trivia & other all-ages events featured at pizzeria with lounge & vegan & gluten-free pies.',
|
||||
category: PlaceCategory.wantToGo,
|
||||
starRating: 4),
|
||||
Place(
|
||||
latLng: LatLng(45.559783, -122.924103),
|
||||
name: 'TopGolf',
|
||||
description:
|
||||
'Sprawling entertainment venue with a high-tech driving range & swanky lounge with drinks & games.',
|
||||
category: PlaceCategory.wantToGo,
|
||||
starRating: 5),
|
||||
Place(
|
||||
latLng: LatLng(45.420226, -122.740347),
|
||||
name: 'Oswego Grill',
|
||||
description:
|
||||
'Wood-grilled steakhouse favorites served in a casual, romantic restaurant with a popular happy hour.',
|
||||
category: PlaceCategory.wantToGo,
|
||||
starRating: 4),
|
||||
Place(
|
||||
latLng: LatLng(45.541202, -122.676432),
|
||||
name: 'The Widmer Brothers Brewery',
|
||||
description:
|
||||
'Popular, enduring gastropub serving craft beers, sandwiches & eclectic entrees in a laid-back space.',
|
||||
category: PlaceCategory.wantToGo,
|
||||
starRating: 4),
|
||||
];
|
||||
}
|
||||
Reference in New Issue
Block a user