Last active
September 14, 2019 19:51
-
-
Save slightfoot/0b960b14c6e3e7d28372c7070aa3bd82 to your computer and use it in GitHub Desktop.
Map Widget Example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math' as math; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/physics.dart'; | |
import 'package:flutter/widgets.dart'; | |
void main() { | |
runApp( | |
MaterialApp( | |
debugShowCheckedModeBanner: false, | |
theme: ThemeData( | |
primaryColor: Colors.indigo, | |
accentColor: Colors.pinkAccent, | |
), | |
home: ExampleScreen(), | |
), | |
); | |
} | |
class ExampleScreen extends StatefulWidget { | |
@override | |
_ExampleScreenState createState() => _ExampleScreenState(); | |
} | |
class _ExampleScreenState extends State<ExampleScreen> { | |
final rand = math.Random(); | |
final _offsets = <MapLatLng>[ | |
MapLatLng(51.531139, -0.188121), // 935, 235 gb | |
MapLatLng(-26.790489, 134.797758), // 1754, 730 aus | |
MapLatLng(36.935858, -120.835227), // 199, 349 cali | |
]; | |
final _titles = <String>['UK', 'Australia', 'California']; | |
int _index = 0; | |
@override | |
void reassemble() { | |
super.reassemble(); | |
nextDestination(); | |
} | |
void nextDestination() { | |
int index = _index; | |
while (index == _index) { | |
index = rand.nextInt(_offsets.length); | |
} | |
setState(() => _index = index); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Material( | |
color: Colors.blue, | |
child: Stack( | |
children: <Widget>[ | |
MapWidget( | |
latLng: _offsets[_index], | |
), | |
SafeArea( | |
child: SizedBox( | |
width: double.infinity, | |
child: Text( | |
_titles[_index], | |
style: TextStyle( | |
color: Colors.white, | |
fontSize: 32.0, | |
shadows: [ | |
Shadow(color: Colors.black, blurRadius: 3.0), | |
], | |
), | |
textAlign: TextAlign.center, | |
), | |
), | |
), | |
], | |
), | |
); | |
} | |
} | |
class MapLatLng { | |
const MapLatLng(this.lat, this.lng); | |
final double lat; | |
final double lng; | |
@override | |
String toString() => 'MapLatLng{$lat, $lng}'; | |
} | |
class MapWidget extends StatefulWidget { | |
const MapWidget({ | |
Key key, | |
@required this.latLng, | |
this.mapImage = const ExactAssetImage('assets/images/world_map_simple.jpg'), | |
this.mapWidth = 2000, | |
this.mapHeight = 1075, | |
this.mapOrigin = const MapLatLng(5.0, 14.0), | |
this.overflow = Overflow.clip, | |
}) : assert(latLng != null), | |
assert(mapImage != null && mapWidth != null && mapHeight != null && mapOrigin != null), | |
assert(overflow != null), | |
super(key: key); | |
final MapLatLng latLng; | |
final ImageProvider mapImage; | |
final int mapWidth; | |
final int mapHeight; | |
final MapLatLng mapOrigin; | |
final Overflow overflow; | |
@override | |
_MapWidgetState createState() => _MapWidgetState(); | |
} | |
class _MapWidgetState extends State<MapWidget> with SingleTickerProviderStateMixin { | |
AnimationController _controller; | |
Animation<Offset> _animation; | |
@override | |
void initState() { | |
super.initState(); | |
_controller = AnimationController.unbounded(value: 1.0, vsync: this); | |
_animation = AlwaysStoppedAnimation<Offset>(_convert(widget.latLng)); | |
} | |
@override | |
void didUpdateWidget(MapWidget oldWidget) { | |
super.didUpdateWidget(oldWidget); | |
if (oldWidget.latLng != widget.latLng) { | |
final current = _animation?.value ?? Offset.zero; | |
_animation = Tween<Offset>(begin: current, end: _convert(widget.latLng)).animate( | |
CurvedAnimation(parent: _controller, curve: Curves.easeOutExpo), | |
); | |
_controller.animateWith(BoundedFrictionSimulation(0.5, 0.0, 0.7, -1.0, 1.0)); | |
} | |
} | |
Offset _convert(MapLatLng latLng) { | |
// https://en.wikipedia.org/wiki/Equirectangular_projection | |
final cosLat = math.cos(widget.mapOrigin.lat * math.pi / 180.0); | |
final dx = ((((latLng.lng - widget.mapOrigin.lng) + 180.0) % 360.0) / 360.0) * widget.mapWidth * cosLat; | |
final dy = (0.5 - ((latLng.lat - widget.mapOrigin.lat) / 180.0)) * widget.mapHeight; | |
return Offset(dx, dy); | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return LayoutBuilder( | |
builder: (BuildContext context, BoxConstraints constraints) { | |
final size = constraints.biggest; | |
return Stack( | |
overflow: Overflow.visible, | |
children: <Widget>[ | |
AnimatedBuilder( | |
animation: _controller, | |
builder: (BuildContext context, Widget child) { | |
return Positioned( | |
left: -_animation.value.dx + size.width / 2, | |
top: -_animation.value.dy + size.height / 2, | |
child: child, | |
); | |
}, | |
child: Image(image: widget.mapImage, fit: BoxFit.none), | |
), | |
], | |
); | |
}, | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment