Allow multiple map routes to share the Map widget and transition between each other
This commit is contained in:
parent
c6090a307a
commit
10826e79b2
|
@ -1,13 +1,33 @@
|
|||
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/home/home_header.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/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/utils/translucent_route.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(
|
||||
replaceInRouteName: 'Screen,Route',
|
||||
routes: <AutoRoute>[
|
||||
|
@ -25,7 +45,18 @@ import '../layouts/main/index.dart';
|
|||
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: "info", page: InfoScreen),
|
||||
]),
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
// ignore_for_file: type=lint
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:auto_route/auto_route.dart' as _i8;
|
||||
import 'package:flutter/material.dart' as _i9;
|
||||
import 'package:auto_route/auto_route.dart' as _i10;
|
||||
import 'package:flutter/material.dart' as _i11;
|
||||
|
||||
import '../layouts/main/index.dart' as _i1;
|
||||
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/info/index.dart' as _i5;
|
||||
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 'index.dart' as _i12;
|
||||
|
||||
class AppRouter extends _i8.RootStackRouter {
|
||||
AppRouter([_i9.GlobalKey<_i9.NavigatorState>? navigatorKey])
|
||||
class AppRouter extends _i10.RootStackRouter {
|
||||
AppRouter([_i11.GlobalKey<_i11.NavigatorState>? navigatorKey])
|
||||
: super(navigatorKey);
|
||||
|
||||
@override
|
||||
final Map<String, _i8.PageFactory> pagesMap = {
|
||||
final Map<String, _i10.PageFactory> pagesMap = {
|
||||
MainLayout.name: (routeData) {
|
||||
return _i8.MaterialPageX<dynamic>(
|
||||
return _i10.MaterialPageX<dynamic>(
|
||||
routeData: routeData, child: const _i1.MainLayout());
|
||||
},
|
||||
HomePageRouter.name: (routeData) {
|
||||
return _i8.MaterialPageX<dynamic>(
|
||||
return _i10.MaterialPageX<dynamic>(
|
||||
routeData: routeData, child: const _i2.HomeScreen());
|
||||
},
|
||||
MapRoute.name: (routeData) {
|
||||
return _i8.MaterialPageX<dynamic>(
|
||||
return _i10.MaterialPageX<dynamic>(
|
||||
routeData: routeData, child: const _i3.MapScreen());
|
||||
},
|
||||
EventsRoute.name: (routeData) {
|
||||
return _i8.MaterialPageX<dynamic>(
|
||||
return _i10.MaterialPageX<dynamic>(
|
||||
routeData: routeData, child: const _i4.EventsScreen());
|
||||
},
|
||||
InfoRoute.name: (routeData) {
|
||||
return _i8.MaterialPageX<dynamic>(
|
||||
return _i10.MaterialPageX<dynamic>(
|
||||
routeData: routeData, child: const _i5.InfoScreen());
|
||||
},
|
||||
HomeRoute.name: (routeData) {
|
||||
return _i8.CustomPage<dynamic>(
|
||||
return _i10.CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const _i6.HomePageHeader(),
|
||||
transitionsBuilder: _i8.TransitionsBuilders.fadeIn,
|
||||
transitionsBuilder: _i10.TransitionsBuilders.fadeIn,
|
||||
opaque: true,
|
||||
barrierDismissible: false);
|
||||
},
|
||||
StudentIdRoute.name: (routeData) {
|
||||
return _i8.CustomPage<dynamic>(
|
||||
return _i10.CustomPage<dynamic>(
|
||||
routeData: routeData,
|
||||
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,
|
||||
barrierDismissible: false);
|
||||
}
|
||||
};
|
||||
|
||||
@override
|
||||
List<_i8.RouteConfig> get routes => [
|
||||
_i8.RouteConfig(MainLayout.name, path: '/', children: [
|
||||
_i8.RouteConfig(HomePageRouter.name,
|
||||
List<_i10.RouteConfig> get routes => [
|
||||
_i10.RouteConfig(MainLayout.name, path: '/', children: [
|
||||
_i10.RouteConfig(HomePageRouter.name,
|
||||
path: 'home',
|
||||
parent: MainLayout.name,
|
||||
children: [
|
||||
_i8.RouteConfig(HomeRoute.name,
|
||||
_i10.RouteConfig(HomeRoute.name,
|
||||
path: '', parent: HomePageRouter.name),
|
||||
_i8.RouteConfig(StudentIdRoute.name,
|
||||
_i10.RouteConfig(StudentIdRoute.name,
|
||||
path: 'student-id', parent: HomePageRouter.name)
|
||||
]),
|
||||
_i8.RouteConfig(MapRoute.name, path: 'map', parent: MainLayout.name),
|
||||
_i8.RouteConfig(EventsRoute.name,
|
||||
_i10.RouteConfig(MapRoute.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),
|
||||
_i8.RouteConfig(InfoRoute.name, path: 'info', parent: MainLayout.name)
|
||||
_i10.RouteConfig(InfoRoute.name,
|
||||
path: 'info', parent: MainLayout.name)
|
||||
])
|
||||
];
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i1.MainLayout]
|
||||
class MainLayout extends _i8.PageRouteInfo<void> {
|
||||
const MainLayout({List<_i8.PageRouteInfo>? children})
|
||||
class MainLayout extends _i10.PageRouteInfo<void> {
|
||||
const MainLayout({List<_i10.PageRouteInfo>? children})
|
||||
: super(MainLayout.name, path: '/', initialChildren: children);
|
||||
|
||||
static const String name = 'MainLayout';
|
||||
|
@ -97,8 +127,8 @@ class MainLayout extends _i8.PageRouteInfo<void> {
|
|||
|
||||
/// generated route for
|
||||
/// [_i2.HomeScreen]
|
||||
class HomePageRouter extends _i8.PageRouteInfo<void> {
|
||||
const HomePageRouter({List<_i8.PageRouteInfo>? children})
|
||||
class HomePageRouter extends _i10.PageRouteInfo<void> {
|
||||
const HomePageRouter({List<_i10.PageRouteInfo>? children})
|
||||
: super(HomePageRouter.name, path: 'home', initialChildren: children);
|
||||
|
||||
static const String name = 'HomePageRouter';
|
||||
|
@ -106,15 +136,16 @@ class HomePageRouter extends _i8.PageRouteInfo<void> {
|
|||
|
||||
/// generated route for
|
||||
/// [_i3.MapScreen]
|
||||
class MapRoute extends _i8.PageRouteInfo<void> {
|
||||
const MapRoute() : super(MapRoute.name, path: 'map');
|
||||
class MapRoute extends _i10.PageRouteInfo<void> {
|
||||
const MapRoute({List<_i10.PageRouteInfo>? children})
|
||||
: super(MapRoute.name, path: 'map', initialChildren: children);
|
||||
|
||||
static const String name = 'MapRoute';
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i4.EventsScreen]
|
||||
class EventsRoute extends _i8.PageRouteInfo<void> {
|
||||
class EventsRoute extends _i10.PageRouteInfo<void> {
|
||||
const EventsRoute() : super(EventsRoute.name, path: 'events');
|
||||
|
||||
static const String name = 'EventsRoute';
|
||||
|
@ -122,7 +153,7 @@ class EventsRoute extends _i8.PageRouteInfo<void> {
|
|||
|
||||
/// generated route for
|
||||
/// [_i5.InfoScreen]
|
||||
class InfoRoute extends _i8.PageRouteInfo<void> {
|
||||
class InfoRoute extends _i10.PageRouteInfo<void> {
|
||||
const InfoRoute() : super(InfoRoute.name, path: 'info');
|
||||
|
||||
static const String name = 'InfoRoute';
|
||||
|
@ -130,7 +161,7 @@ class InfoRoute extends _i8.PageRouteInfo<void> {
|
|||
|
||||
/// generated route for
|
||||
/// [_i6.HomePageHeader]
|
||||
class HomeRoute extends _i8.PageRouteInfo<void> {
|
||||
class HomeRoute extends _i10.PageRouteInfo<void> {
|
||||
const HomeRoute() : super(HomeRoute.name, path: '');
|
||||
|
||||
static const String name = 'HomeRoute';
|
||||
|
@ -138,8 +169,40 @@ class HomeRoute extends _i8.PageRouteInfo<void> {
|
|||
|
||||
/// generated route for
|
||||
/// [_i7.StudentIdScreen]
|
||||
class StudentIdRoute extends _i8.PageRouteInfo<void> {
|
||||
class StudentIdRoute extends _i10.PageRouteInfo<void> {
|
||||
const StudentIdRoute() : super(StudentIdRoute.name, path: 'student-id');
|
||||
|
||||
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}';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
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_svg/svg.dart';
|
||||
import 'package:furman_now/src/utils/theme.dart';
|
||||
import 'package:furman_now/src/widgets/map/filter_chip.dart';
|
||||
import 'package:furman_now/src/widgets/map/rotate_compass.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:furman_now/src/screens/map/state.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:transparent_pointer/transparent_pointer.dart';
|
||||
|
||||
import 'map_widget.dart';
|
||||
|
||||
class MapScreen extends StatefulWidget {
|
||||
const MapScreen({Key? key}) : super(key: key);
|
||||
|
@ -18,206 +15,34 @@ class MapScreen extends StatefulWidget {
|
|||
|
||||
class _MapScreenState extends State<MapScreen>
|
||||
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
|
||||
void 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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
return ChangeNotifierProvider(
|
||||
create: (context) => MapPageState(vsync: this),
|
||||
builder: (context, _) => WillPopScope(
|
||||
onWillPop: () async {
|
||||
print("Will pop");
|
||||
return false;
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Container(
|
||||
color: const Color(0xffb7acc9),
|
||||
child: SafeArea(
|
||||
top: false,
|
||||
// child: MapWidget(),
|
||||
child: Stack(
|
||||
children: [
|
||||
FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: MapOptions(
|
||||
center: LatLng(34.925926, -82.439397),
|
||||
enableMultiFingerGestureRace: true,
|
||||
rotationWinGestures: MultiFingerGesture.all,
|
||||
pinchZoomThreshold: 0.2,
|
||||
rotationThreshold: 8,
|
||||
zoom: 15,
|
||||
minZoom: 12,
|
||||
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"),
|
||||
MapWidget(),
|
||||
TransparentPointer(transparent: true, child: AutoRouter()),
|
||||
],
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -4,16 +4,20 @@ import 'package:furman_now/src/utils/theme.dart';
|
|||
class MapFilterChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
final Function callback;
|
||||
|
||||
const MapFilterChip({
|
||||
required this.icon,
|
||||
required this.text,
|
||||
required this.callback,
|
||||
Key? key
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
return GestureDetector(
|
||||
onTapDown: (e) => callback(),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 6, bottom: 6, left: 8, right: 12),
|
||||
decoration: BoxDecoration(
|
||||
|
@ -50,6 +54,7 @@ class MapFilterChip extends StatelessWidget {
|
|||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -812,6 +812,13 @@ packages:
|
|||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
@ -53,6 +53,7 @@ dependencies:
|
|||
weather: ^2.0.1
|
||||
geolocator: ^9.0.1
|
||||
flutter_map_marker_cluster: ^0.5.4
|
||||
transparent_pointer: ^1.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
Loading…
Reference in New Issue