Allow multiple map routes to share the Map widget and transition between each other

This commit is contained in:
Michael Thomas 2022-09-12 15:24:14 -04:00
parent c6090a307a
commit 10826e79b2
13 changed files with 715 additions and 265 deletions

View File

@ -1,13 +1,33 @@
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:furman_now/src/screens/events/index.dart'; import 'package:furman_now/src/screens/events/index.dart';
import 'package:furman_now/src/screens/home/home_header.dart'; import 'package:furman_now/src/screens/home/home_header.dart';
import 'package:furman_now/src/screens/home/index.dart'; import 'package:furman_now/src/screens/home/index.dart';
import 'package:furman_now/src/screens/info/index.dart'; import 'package:furman_now/src/screens/info/index.dart';
import 'package:furman_now/src/screens/map/index.dart'; import 'package:furman_now/src/screens/map/index.dart';
import 'package:furman_now/src/screens/map/map_category.dart';
import 'package:furman_now/src/screens/map/map_home.dart';
import 'package:furman_now/src/screens/student_id/index.dart'; import 'package:furman_now/src/screens/student_id/index.dart';
import 'package:furman_now/src/utils/translucent_route.dart';
import '../layouts/main/index.dart'; import '../layouts/main/index.dart';
Route<T> mapRouteBuilder<T>(BuildContext context, Widget child, CustomPage<T> page){
return TranslucentRoute(
settings: page,
transitionDuration: const Duration(milliseconds: 200),
transitionBuilder: (context, animation, secondaryAnimation, child) =>
FadeTransition(
opacity: animation,
child: FadeTransition(
opacity: secondaryAnimation.drive(Tween<double>(begin: 1, end: 0)),
child: child,
),
),
pageBuilder: (context) => child,
);
}
@MaterialAutoRouter( @MaterialAutoRouter(
replaceInRouteName: 'Screen,Route', replaceInRouteName: 'Screen,Route',
routes: <AutoRoute>[ routes: <AutoRoute>[
@ -25,7 +45,18 @@ import '../layouts/main/index.dart';
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
]), ]),
AutoRoute(path: "map", page: MapScreen), AutoRoute(path: "map", page: MapScreen, children: [
CustomRoute(
path: "",
page: MapHomeScreen,
customRouteBuilder: mapRouteBuilder,
),
CustomRoute(
path: "category/:id",
page: MapCategoryScreen,
customRouteBuilder: mapRouteBuilder,
),
]),
AutoRoute(path: "events", page: EventsScreen), AutoRoute(path: "events", page: EventsScreen),
AutoRoute(path: "info", page: InfoScreen), AutoRoute(path: "info", page: InfoScreen),
]), ]),

View File

@ -11,8 +11,8 @@
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i8; import 'package:auto_route/auto_route.dart' as _i10;
import 'package:flutter/material.dart' as _i9; import 'package:flutter/material.dart' as _i11;
import '../layouts/main/index.dart' as _i1; import '../layouts/main/index.dart' as _i1;
import '../screens/events/index.dart' as _i4; import '../screens/events/index.dart' as _i4;
@ -20,76 +20,106 @@ import '../screens/home/home_header.dart' as _i6;
import '../screens/home/index.dart' as _i2; import '../screens/home/index.dart' as _i2;
import '../screens/info/index.dart' as _i5; import '../screens/info/index.dart' as _i5;
import '../screens/map/index.dart' as _i3; import '../screens/map/index.dart' as _i3;
import '../screens/map/map_category.dart' as _i9;
import '../screens/map/map_home.dart' as _i8;
import '../screens/map/state.dart' as _i13;
import '../screens/student_id/index.dart' as _i7; import '../screens/student_id/index.dart' as _i7;
import 'index.dart' as _i12;
class AppRouter extends _i8.RootStackRouter { class AppRouter extends _i10.RootStackRouter {
AppRouter([_i9.GlobalKey<_i9.NavigatorState>? navigatorKey]) AppRouter([_i11.GlobalKey<_i11.NavigatorState>? navigatorKey])
: super(navigatorKey); : super(navigatorKey);
@override @override
final Map<String, _i8.PageFactory> pagesMap = { final Map<String, _i10.PageFactory> pagesMap = {
MainLayout.name: (routeData) { MainLayout.name: (routeData) {
return _i8.MaterialPageX<dynamic>( return _i10.MaterialPageX<dynamic>(
routeData: routeData, child: const _i1.MainLayout()); routeData: routeData, child: const _i1.MainLayout());
}, },
HomePageRouter.name: (routeData) { HomePageRouter.name: (routeData) {
return _i8.MaterialPageX<dynamic>( return _i10.MaterialPageX<dynamic>(
routeData: routeData, child: const _i2.HomeScreen()); routeData: routeData, child: const _i2.HomeScreen());
}, },
MapRoute.name: (routeData) { MapRoute.name: (routeData) {
return _i8.MaterialPageX<dynamic>( return _i10.MaterialPageX<dynamic>(
routeData: routeData, child: const _i3.MapScreen()); routeData: routeData, child: const _i3.MapScreen());
}, },
EventsRoute.name: (routeData) { EventsRoute.name: (routeData) {
return _i8.MaterialPageX<dynamic>( return _i10.MaterialPageX<dynamic>(
routeData: routeData, child: const _i4.EventsScreen()); routeData: routeData, child: const _i4.EventsScreen());
}, },
InfoRoute.name: (routeData) { InfoRoute.name: (routeData) {
return _i8.MaterialPageX<dynamic>( return _i10.MaterialPageX<dynamic>(
routeData: routeData, child: const _i5.InfoScreen()); routeData: routeData, child: const _i5.InfoScreen());
}, },
HomeRoute.name: (routeData) { HomeRoute.name: (routeData) {
return _i8.CustomPage<dynamic>( return _i10.CustomPage<dynamic>(
routeData: routeData, routeData: routeData,
child: const _i6.HomePageHeader(), child: const _i6.HomePageHeader(),
transitionsBuilder: _i8.TransitionsBuilders.fadeIn, transitionsBuilder: _i10.TransitionsBuilders.fadeIn,
opaque: true, opaque: true,
barrierDismissible: false); barrierDismissible: false);
}, },
StudentIdRoute.name: (routeData) { StudentIdRoute.name: (routeData) {
return _i8.CustomPage<dynamic>( return _i10.CustomPage<dynamic>(
routeData: routeData, routeData: routeData,
child: const _i7.StudentIdScreen(), child: const _i7.StudentIdScreen(),
transitionsBuilder: _i8.TransitionsBuilders.fadeIn, transitionsBuilder: _i10.TransitionsBuilders.fadeIn,
opaque: true,
barrierDismissible: false);
},
MapHomeRoute.name: (routeData) {
return _i10.CustomPage<dynamic>(
routeData: routeData,
child: const _i8.MapHomeScreen(),
customRouteBuilder: _i12.mapRouteBuilder,
opaque: true,
barrierDismissible: false);
},
MapCategoryRoute.name: (routeData) {
final args = routeData.argsAs<MapCategoryRouteArgs>();
return _i10.CustomPage<dynamic>(
routeData: routeData,
child: _i9.MapCategoryScreen(category: args.category, key: args.key),
customRouteBuilder: _i12.mapRouteBuilder,
opaque: true, opaque: true,
barrierDismissible: false); barrierDismissible: false);
} }
}; };
@override @override
List<_i8.RouteConfig> get routes => [ List<_i10.RouteConfig> get routes => [
_i8.RouteConfig(MainLayout.name, path: '/', children: [ _i10.RouteConfig(MainLayout.name, path: '/', children: [
_i8.RouteConfig(HomePageRouter.name, _i10.RouteConfig(HomePageRouter.name,
path: 'home', path: 'home',
parent: MainLayout.name, parent: MainLayout.name,
children: [ children: [
_i8.RouteConfig(HomeRoute.name, _i10.RouteConfig(HomeRoute.name,
path: '', parent: HomePageRouter.name), path: '', parent: HomePageRouter.name),
_i8.RouteConfig(StudentIdRoute.name, _i10.RouteConfig(StudentIdRoute.name,
path: 'student-id', parent: HomePageRouter.name) path: 'student-id', parent: HomePageRouter.name)
]), ]),
_i8.RouteConfig(MapRoute.name, path: 'map', parent: MainLayout.name), _i10.RouteConfig(MapRoute.name,
_i8.RouteConfig(EventsRoute.name, path: 'map',
parent: MainLayout.name,
children: [
_i10.RouteConfig(MapHomeRoute.name,
path: '', parent: MapRoute.name),
_i10.RouteConfig(MapCategoryRoute.name,
path: 'category/:id', parent: MapRoute.name)
]),
_i10.RouteConfig(EventsRoute.name,
path: 'events', parent: MainLayout.name), path: 'events', parent: MainLayout.name),
_i8.RouteConfig(InfoRoute.name, path: 'info', parent: MainLayout.name) _i10.RouteConfig(InfoRoute.name,
path: 'info', parent: MainLayout.name)
]) ])
]; ];
} }
/// generated route for /// generated route for
/// [_i1.MainLayout] /// [_i1.MainLayout]
class MainLayout extends _i8.PageRouteInfo<void> { class MainLayout extends _i10.PageRouteInfo<void> {
const MainLayout({List<_i8.PageRouteInfo>? children}) const MainLayout({List<_i10.PageRouteInfo>? children})
: super(MainLayout.name, path: '/', initialChildren: children); : super(MainLayout.name, path: '/', initialChildren: children);
static const String name = 'MainLayout'; static const String name = 'MainLayout';
@ -97,8 +127,8 @@ class MainLayout extends _i8.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i2.HomeScreen] /// [_i2.HomeScreen]
class HomePageRouter extends _i8.PageRouteInfo<void> { class HomePageRouter extends _i10.PageRouteInfo<void> {
const HomePageRouter({List<_i8.PageRouteInfo>? children}) const HomePageRouter({List<_i10.PageRouteInfo>? children})
: super(HomePageRouter.name, path: 'home', initialChildren: children); : super(HomePageRouter.name, path: 'home', initialChildren: children);
static const String name = 'HomePageRouter'; static const String name = 'HomePageRouter';
@ -106,15 +136,16 @@ class HomePageRouter extends _i8.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i3.MapScreen] /// [_i3.MapScreen]
class MapRoute extends _i8.PageRouteInfo<void> { class MapRoute extends _i10.PageRouteInfo<void> {
const MapRoute() : super(MapRoute.name, path: 'map'); const MapRoute({List<_i10.PageRouteInfo>? children})
: super(MapRoute.name, path: 'map', initialChildren: children);
static const String name = 'MapRoute'; static const String name = 'MapRoute';
} }
/// generated route for /// generated route for
/// [_i4.EventsScreen] /// [_i4.EventsScreen]
class EventsRoute extends _i8.PageRouteInfo<void> { class EventsRoute extends _i10.PageRouteInfo<void> {
const EventsRoute() : super(EventsRoute.name, path: 'events'); const EventsRoute() : super(EventsRoute.name, path: 'events');
static const String name = 'EventsRoute'; static const String name = 'EventsRoute';
@ -122,7 +153,7 @@ class EventsRoute extends _i8.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i5.InfoScreen] /// [_i5.InfoScreen]
class InfoRoute extends _i8.PageRouteInfo<void> { class InfoRoute extends _i10.PageRouteInfo<void> {
const InfoRoute() : super(InfoRoute.name, path: 'info'); const InfoRoute() : super(InfoRoute.name, path: 'info');
static const String name = 'InfoRoute'; static const String name = 'InfoRoute';
@ -130,7 +161,7 @@ class InfoRoute extends _i8.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i6.HomePageHeader] /// [_i6.HomePageHeader]
class HomeRoute extends _i8.PageRouteInfo<void> { class HomeRoute extends _i10.PageRouteInfo<void> {
const HomeRoute() : super(HomeRoute.name, path: ''); const HomeRoute() : super(HomeRoute.name, path: '');
static const String name = 'HomeRoute'; static const String name = 'HomeRoute';
@ -138,8 +169,40 @@ class HomeRoute extends _i8.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i7.StudentIdScreen] /// [_i7.StudentIdScreen]
class StudentIdRoute extends _i8.PageRouteInfo<void> { class StudentIdRoute extends _i10.PageRouteInfo<void> {
const StudentIdRoute() : super(StudentIdRoute.name, path: 'student-id'); const StudentIdRoute() : super(StudentIdRoute.name, path: 'student-id');
static const String name = 'StudentIdRoute'; static const String name = 'StudentIdRoute';
} }
/// generated route for
/// [_i8.MapHomeScreen]
class MapHomeRoute extends _i10.PageRouteInfo<void> {
const MapHomeRoute() : super(MapHomeRoute.name, path: '');
static const String name = 'MapHomeRoute';
}
/// generated route for
/// [_i9.MapCategoryScreen]
class MapCategoryRoute extends _i10.PageRouteInfo<MapCategoryRouteArgs> {
MapCategoryRoute({required _i13.MapCategory category, _i11.Key? key})
: super(MapCategoryRoute.name,
path: 'category/:id',
args: MapCategoryRouteArgs(category: category, key: key));
static const String name = 'MapCategoryRoute';
}
class MapCategoryRouteArgs {
const MapCategoryRouteArgs({required this.category, this.key});
final _i13.MapCategory category;
final _i11.Key? key;
@override
String toString() {
return 'MapCategoryRouteArgs{category: $category, key: $key}';
}
}

View File

@ -1,13 +1,10 @@
import 'dart:async'; import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:furman_now/src/screens/map/state.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart'; import 'package:provider/provider.dart';
import 'package:flutter_svg/svg.dart'; import 'package:transparent_pointer/transparent_pointer.dart';
import 'package:furman_now/src/utils/theme.dart';
import 'package:furman_now/src/widgets/map/filter_chip.dart'; import 'map_widget.dart';
import 'package:furman_now/src/widgets/map/rotate_compass.dart';
import 'package:latlong2/latlong.dart';
class MapScreen extends StatefulWidget { class MapScreen extends StatefulWidget {
const MapScreen({Key? key}) : super(key: key); const MapScreen({Key? key}) : super(key: key);
@ -18,206 +15,34 @@ class MapScreen extends StatefulWidget {
class _MapScreenState extends State<MapScreen> class _MapScreenState extends State<MapScreen>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
final MapController _mapController = MapController();
late final AnimationController _animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
late CenterOnLocationUpdate _centerOnLocationUpdate;
late StreamController<double?> _centerCurrentLocationStreamController;
var _rotation = 0.0;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_mapController.mapEventStream.listen((event) {
if (event is MapEventRotate) {
setState(() {
_rotation = _mapController.rotation * (2 * pi) / 360;
});
}
});
_centerOnLocationUpdate = CenterOnLocationUpdate.always;
_centerCurrentLocationStreamController = StreamController<double?>();
}
@override
void dispose() {
super.dispose();
_animationController.dispose();
}
void resetRotation() async {
// take the shortest rotation path
var end = _mapController.rotation > 180 ? 360.0 : 0.0;
var animation = Tween<double>(
begin: _mapController.rotation,
end: end,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
animationListener() {
_mapController.rotate(animation.value);
}
animation.addListener(animationListener);
await _animationController.forward();
animation.removeListener(animationListener);
_animationController.reset();
_mapController.rotate(0);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return ChangeNotifierProvider(
body: Container( create: (context) => MapPageState(vsync: this),
color: const Color(0xffb7acc9), builder: (context, _) => WillPopScope(
child: SafeArea( onWillPop: () async {
top: false, print("Will pop");
child: Stack( return false;
children: [ },
FlutterMap( child: Scaffold(
mapController: _mapController, body: Container(
options: MapOptions( color: const Color(0xffb7acc9),
center: LatLng(34.925926, -82.439397), child: SafeArea(
enableMultiFingerGestureRace: true, top: false,
rotationWinGestures: MultiFingerGesture.all, // child: MapWidget(),
pinchZoomThreshold: 0.2, child: Stack(
rotationThreshold: 8, children: const [
zoom: 15, MapWidget(),
minZoom: 12, TransparentPointer(transparent: true, child: AutoRouter()),
maxZoom: 18,
plugins: [
LocationMarkerPlugin(
centerCurrentLocationStream:
_centerCurrentLocationStreamController.stream,
centerOnLocationUpdate: _centerOnLocationUpdate,
),
],
onPositionChanged: (MapPosition position, bool hasGesture) {
if (hasGesture) {
setState(
() => _centerOnLocationUpdate = CenterOnLocationUpdate.never,
);
}
},
),
layers: [
TileLayerOptions(
urlTemplate:
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
userAgentPackageName: 'edu.furman.now',
),
LocationMarkerLayerOptions(),
],
nonRotatedChildren: [
AttributionWidget(
attributionBuilder: (BuildContext context) {
return const ColoredBox(
color: Color(0xCCFFFFFF),
child: Padding(
padding: EdgeInsets.all(3),
child: Text("©️ OpenStreetMap contributors"),
),
);
},
),
], ],
), ),
// Rotation reset fab ),
Positioned(
top: 12,
left: 0,
right: 0,
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Container(
width: double.infinity,
height: 50,
padding: const EdgeInsets.only(left: 10, right: 20),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(60)),
boxShadow: [
BoxShadow(
color: Color(0x33000000),
blurRadius: 8,
),
],
),
child: Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
SvgPicture.asset("assets/images/bell-tower.svg", color: Theme.of(context).primaryColor, height: 32),
const SizedBox(width: 10),
Text(
"Search locations",
style: furmanTextStyle(TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.grey.shade500,
)),
),
],
),
),
],
),
),
),
// const SizedBox(height: 12),
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 6,
children: const [
MapFilterChip(icon: Icons.restaurant, text: "Restaurants"),
MapFilterChip(icon: Icons.train, text: "Transportation"),
MapFilterChip(icon: Icons.school, text: "Campus Buildings"),
],
),
),
MapRotateCompass(rotation: _rotation, resetRotation: resetRotation),
],
),
),
),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
onPressed: () {
// Automatically center the location marker on the map when location updated until user interact with the map.
setState(
() => _centerOnLocationUpdate = CenterOnLocationUpdate.always,
);
// Center the location marker on the map and zoom the map to level 18.
_centerCurrentLocationStreamController.add(16);
},
child: const Icon(
Icons.my_location,
color: Colors.white,
),
)
)
],
), ),
), ),
), ),

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:furman_now/src/screens/map/state.dart';
import 'package:furman_now/src/widgets/map/map_header.dart';
import 'package:furman_now/src/widgets/map/rotate_compass.dart';
import 'package:provider/provider.dart';
import 'map_widget.dart';
class MapCategoryScreen extends StatelessWidget {
final MapCategory category;
const MapCategoryScreen({
required this.category,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<MapPageState>(
builder: (context, state, _) => Stack(
children: [
Positioned(
top: 12,
left: 0,
right: 0,
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Hero(
tag: "header",
child: MapHeader(
activeCategory: category.name,
),
),
// const SizedBox(height: 12),
// SingleChildScrollView(
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
// scrollDirection: Axis.horizontal,
// child: Wrap(
// spacing: 6,
// children: [
// // ...categories.map((category) => MapFilterChip(
// // icon: Icons.restaurant,
// // text: category.name,
// // callback: category.activator
// // )),
// // MapFilterChip(
// // icon: Icons.restaurant,
// // text: "Restaurants",
// // callback: () => null,
// // ),
// // MapFilterChip(icon: Icons.train, text: "Transportation"),
// // MapFilterChip(icon: Icons.school, text: "Campus Buildings"),
// ],
// ),
// ),
MapRotateCompass(rotation: state.rotation, resetRotation: state.resetRotation),
],
),
),
),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
onPressed: () {
// Automatically center the location marker on the map when location updated until user interact with the map.
state.centerOnLocationUpdate = CenterOnLocationUpdate.always;
// Center the location marker on the map and zoom the map to level 16.
state.centerCurrentLocationStreamController.add(16);
},
child: const Icon(
Icons.my_location,
color: Colors.white,
),
)
),
],
),
);
}
}

View File

@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:furman_now/src/screens/map/map_widget.dart';
import 'package:furman_now/src/screens/map/state.dart';
import 'package:furman_now/src/widgets/map/filter_chip.dart';
import 'package:furman_now/src/widgets/map/map_header.dart';
import 'package:furman_now/src/widgets/map/rotate_compass.dart';
import 'package:provider/provider.dart';
class MapHomeScreen extends StatelessWidget {
const MapHomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<MapPageState>(
builder: (context, state, _) => Stack(
children: [
Positioned(
top: 12,
left: 0,
right: 0,
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Hero(
tag: "header",
child: MapHeader()
),
// const SizedBox(height: 12),
SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 6,
children: [
...state.categories.map((category) => MapFilterChip(
icon: Icons.restaurant,
text: category.name,
callback: category.activator
)),
// MapFilterChip(
// icon: Icons.restaurant,
// text: "Restaurants",
// callback: () => null,
// ),
// MapFilterChip(icon: Icons.train, text: "Transportation"),
// MapFilterChip(icon: Icons.school, text: "Campus Buildings"),
],
),
),
MapRotateCompass(rotation: state.rotation, resetRotation: state.resetRotation),
],
),
),
),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
onPressed: () {
// Automatically center the location marker on the map when location updated until user interact with the map.
state.centerOnLocationUpdate = CenterOnLocationUpdate.always;
// Center the location marker on the map and zoom the map to level 16.
state.centerCurrentLocationStreamController.add(16);
},
child: const Icon(
Icons.my_location,
color: Colors.white,
),
),
),
],
),
);
}
}

View File

View File

@ -0,0 +1,99 @@
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart';
import 'package:furman_now/src/screens/map/state.dart';
import 'package:furman_now/src/utils/theme.dart';
import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart';
class MapWidget extends StatelessWidget {
const MapWidget({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Consumer<MapPageState>(
builder: (context, state, _) => Scaffold(
body: Stack(
children: [
FlutterMap(
mapController: state.mapController,
options: MapOptions(
center: LatLng(34.925926, -82.439397),
maxBounds: LatLngBounds(
LatLng(34.991937, -82.536251),
LatLng(34.813016, -82.328766),
),
enableMultiFingerGestureRace: true,
rotationWinGestures: MultiFingerGesture.all,
pinchZoomThreshold: 0.2,
rotationThreshold: 8,
zoom: 15,
minZoom: 12,
maxZoom: 18,
plugins: [
LocationMarkerPlugin(
centerCurrentLocationStream:
state.centerCurrentLocationStreamController.stream,
centerOnLocationUpdate: state.centerOnLocationUpdate,
),
MarkerClusterPlugin(),
],
onPositionChanged: (MapPosition position, bool hasGesture) {
if (hasGesture) {
state.centerOnLocationUpdate = CenterOnLocationUpdate.never;
}
},
),
layers: [
TileLayerOptions(
urlTemplate:
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
userAgentPackageName: 'edu.furman.now',
),
LocationMarkerLayerOptions(),
MarkerClusterLayerOptions(
maxClusterRadius: 60,
size: const Size(40, 40),
fitBoundsOptions: const FitBoundsOptions(
padding: EdgeInsets.all(50),
),
markers: state.markers,
polygonOptions: const PolygonOptions(
borderColor: Colors.blueAccent,
color: Colors.black12,
borderStrokeWidth: 3,
),
builder: (context, markers) {
return FloatingActionButton(
onPressed: null,
child: Text(markers.length.toString()),
);
},
),
],
nonRotatedChildren: [
AttributionWidget(
attributionBuilder: (BuildContext context) {
return ColoredBox(
color: const Color(0xCCFFFFFF),
child: Padding(
padding: const EdgeInsets.all(3),
child: Text(
"©️ OpenStreetMap contributors",
style: furmanTextStyle(const TextStyle(
fontSize: 10,
)),
),
),
);
},
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,115 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:flutter_map_location_marker/flutter_map_location_marker.dart';
import 'package:flutter_remix/flutter_remix.dart';
import 'package:furman_now/src/services/restaurants/restaurant_service.dart';
@immutable
class MapCategory {
final String name;
final IconData icon;
final Function activator;
const MapCategory({
required this.name,
required this.icon,
required this.activator,
});
}
class MapPageState extends ChangeNotifier {
MapPageState({
required TickerProvider vsync
}): _animationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: vsync,
) {
_initState();
}
void _initState() {
mapController.mapEventStream.listen((event) {
if (event is MapEventRotate) {
rotation = mapController.rotation * (2 * pi) / 360;
}
});
centerOnLocationUpdate = CenterOnLocationUpdate.always;
centerCurrentLocationStreamController = StreamController<double?>();
}
final MapController mapController = MapController();
final AnimationController _animationController;
late CenterOnLocationUpdate _centerOnLocationUpdate;
CenterOnLocationUpdate get centerOnLocationUpdate => _centerOnLocationUpdate;
set centerOnLocationUpdate (CenterOnLocationUpdate value) {
_centerOnLocationUpdate = value;
notifyListeners();
}
late StreamController<double?> centerCurrentLocationStreamController;
var _rotation = 0.0;
double get rotation => _rotation;
set rotation (double value) {
_rotation = value;
notifyListeners();
}
List<Marker> _markers = <Marker>[];
List<Marker> get markers => _markers;
set markers (List<Marker> value) {
_markers = value;
notifyListeners();
}
void resetRotation() async {
// take the shortest rotation path
var end = mapController.rotation > 180 ? 360.0 : 0.0;
var animation = Tween<double>(
begin: mapController.rotation,
end: end,
).animate(CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
));
animationListener() {
mapController.rotate(animation.value);
}
animation.addListener(animationListener);
await _animationController.forward();
animation.removeListener(animationListener);
_animationController.reset();
mapController.rotate(0);
}
late final List<MapCategory> categories = [
MapCategory(
name: "Restaurants",
icon: FlutterRemix.restaurant_line,
activator: showRestaurants,
),
];
Future<void> showRestaurants() async {
var restaurants = await RestaurantService.fetchRestaurants();
var newMarkers = restaurants.map((restaurant) => Marker(
point: restaurant.mapLocation,
width: 40,
height: 40,
builder: (context) => GestureDetector(
onTapDown: (e) => print("tapped"),
child: const FlutterLogo()
),
));
markers = newMarkers.toList();
}
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
class TranslucentRoute<T> extends TransitionRoute<T> {
final bool _opaque;
final Duration _transitionDuration;
final Widget Function(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child
) _transitionBuilder;
final Widget Function(BuildContext) _pageBuilder;
TranslucentRoute({
opaque = true,
transitionDuration = const Duration(milliseconds: 300),
required Widget Function(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child
) transitionBuilder,
required Widget Function(BuildContext) pageBuilder,
RouteSettings? settings,
}):
_opaque = opaque,
_transitionDuration = transitionDuration,
_transitionBuilder = transitionBuilder,
_pageBuilder = pageBuilder,
super(settings: settings);
@override
Iterable<OverlayEntry> createOverlayEntries() {
return <OverlayEntry>[
OverlayEntry(
builder: (context) => _transitionBuilder(
context,
animation!,
secondaryAnimation!,
_pageBuilder(context),
),
),
];
}
@override
bool get opaque => _opaque;
@override
Duration get transitionDuration => _transitionDuration;
}

View File

@ -4,51 +4,56 @@ import 'package:furman_now/src/utils/theme.dart';
class MapFilterChip extends StatelessWidget { class MapFilterChip extends StatelessWidget {
final IconData icon; final IconData icon;
final String text; final String text;
final Function callback;
const MapFilterChip({ const MapFilterChip({
required this.icon, required this.icon,
required this.text, required this.text,
required this.callback,
Key? key Key? key
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return GestureDetector(
padding: const EdgeInsets.only( onTapDown: (e) => callback(),
top: 6, bottom: 6, left: 8, right: 12), child: Container(
decoration: BoxDecoration( padding: const EdgeInsets.only(
color: Colors.white, top: 6, bottom: 6, left: 8, right: 12),
borderRadius: BorderRadius.circular(100), decoration: BoxDecoration(
boxShadow: const [ color: Colors.white,
BoxShadow( borderRadius: BorderRadius.circular(100),
color: Color(0x33000000), boxShadow: const [
blurRadius: 4, BoxShadow(
), color: Color(0x33000000),
], blurRadius: 4,
),
child: RichText(
text: TextSpan(
children: [
WidgetSpan(
child: Icon(icon,
size: 18,
color: Colors.grey.shade800),
alignment: PlaceholderAlignment.middle,
),
const WidgetSpan(child: SizedBox(width: 6)),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Text(
text,
style: furmanTextStyle(TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
)),
),
), ),
], ],
), ),
child: RichText(
text: TextSpan(
children: [
WidgetSpan(
child: Icon(icon,
size: 18,
color: Colors.grey.shade800),
alignment: PlaceholderAlignment.middle,
),
const WidgetSpan(child: SizedBox(width: 6)),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Text(
text,
style: furmanTextStyle(TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
)),
),
),
],
),
),
), ),
); );
} }

View File

@ -0,0 +1,92 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_remix/flutter_remix.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:furman_now/src/routes/index.gr.dart';
import 'package:furman_now/src/screens/map/state.dart';
import 'package:furman_now/src/utils/theme.dart';
class MapHeader extends StatelessWidget {
final String? activeCategory;
const MapHeader({
this.activeCategory,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Container(
width: double.infinity,
height: 50,
padding: const EdgeInsets.only(left: 10, right: 20),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(60)),
boxShadow: [
BoxShadow(
color: Color(0x33000000),
blurRadius: 8,
),
],
),
child: Stack(
children: [
Align(
alignment: Alignment.centerLeft,
child: AnimatedCrossFade(
duration: const Duration(milliseconds: 300),
crossFadeState: activeCategory == null ? CrossFadeState.showFirst : CrossFadeState.showSecond,
firstChild: GestureDetector(
onTapDown: (e) => context.router.push(MapCategoryRoute(
category: MapCategory(
name: "Restaurants",
icon: FlutterRemix.restaurant_line,
activator: () => null,
),
)),
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
SvgPicture.asset("assets/images/bell-tower.svg", color: Theme.of(context).primaryColor, height: 32),
const SizedBox(width: 10),
Text(
"Search locations",
style: furmanTextStyle(TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Colors.grey.shade500,
)),
),
],
),
),
secondChild: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: [
GestureDetector(
onTapDown: (e) => context.router.navigateBack(),
child: Icon(FlutterRemix.arrow_left_line, size: 28, color: Colors.grey.shade800,),
),
const SizedBox(width: 10),
if (activeCategory != null)
Text(
activeCategory!,
style: furmanTextStyle(TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
)),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -812,6 +812,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.0" version: "1.0.0"
transparent_pointer:
dependency: "direct main"
description:
name: transparent_pointer
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
tuple: tuple:
dependency: transitive dependency: transitive
description: description:

View File

@ -53,6 +53,7 @@ dependencies:
weather: ^2.0.1 weather: ^2.0.1
geolocator: ^9.0.1 geolocator: ^9.0.1
flutter_map_marker_cluster: ^0.5.4 flutter_map_marker_cluster: ^0.5.4
transparent_pointer: ^1.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: