info: custom marker on map
This commit is contained in:
parent
49637ede95
commit
96422e3340
5 changed files with 263 additions and 43 deletions
|
@ -146,7 +146,7 @@ class _AvesFilterChipState extends State<AvesFilterChip> {
|
||||||
child: Stack(
|
child: Stack(
|
||||||
fit: StackFit.passthrough,
|
fit: StackFit.passthrough,
|
||||||
children: [
|
children: [
|
||||||
if (widget.background != null)
|
if (hasBackground)
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
child: widget.background,
|
child: widget.background,
|
||||||
|
|
|
@ -9,6 +9,7 @@ import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/maps/google_map.dart';
|
import 'package:aves/widgets/fullscreen/info/maps/google_map.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart';
|
import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/maps/marker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class LocationSection extends StatefulWidget {
|
class LocationSection extends StatefulWidget {
|
||||||
|
@ -34,6 +35,9 @@ class LocationSection extends StatefulWidget {
|
||||||
class _LocationSectionState extends State<LocationSection> {
|
class _LocationSectionState extends State<LocationSection> {
|
||||||
String _loadedUri;
|
String _loadedUri;
|
||||||
|
|
||||||
|
static const extent = 48.0;
|
||||||
|
static const pointerSize = Size(8.0, 6.0);
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
CollectionLens get collection => widget.collection;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
@ -85,6 +89,14 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place));
|
if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildMarker(BuildContext context) {
|
||||||
|
return ImageMarker(
|
||||||
|
entry: entry,
|
||||||
|
extent: extent,
|
||||||
|
pointerSize: pointerSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -96,16 +108,19 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
},
|
},
|
||||||
child: settings.infoMapStyle.isGoogleMaps
|
child: settings.infoMapStyle.isGoogleMaps
|
||||||
? EntryGoogleMap(
|
? EntryGoogleMap(
|
||||||
markerId: entry.uri ?? entry.path,
|
|
||||||
latLng: entry.latLng,
|
latLng: entry.latLng,
|
||||||
geoUri: entry.geoUri,
|
geoUri: entry.geoUri,
|
||||||
initialZoom: settings.infoMapZoom,
|
initialZoom: settings.infoMapZoom,
|
||||||
|
markerId: entry.uri ?? entry.path,
|
||||||
|
markerBuilder: buildMarker,
|
||||||
)
|
)
|
||||||
: EntryLeafletMap(
|
: EntryLeafletMap(
|
||||||
latLng: entry.latLng,
|
latLng: entry.latLng,
|
||||||
geoUri: entry.geoUri,
|
geoUri: entry.geoUri,
|
||||||
initialZoom: settings.infoMapZoom,
|
initialZoom: settings.infoMapZoom,
|
||||||
style: settings.infoMapStyle,
|
style: settings.infoMapStyle,
|
||||||
|
markerSize: Size(extent, extent + pointerSize.height),
|
||||||
|
markerBuilder: buildMarker,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (entry.hasGps)
|
if (entry.hasGps)
|
||||||
|
|
|
@ -1,22 +1,28 @@
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
import 'package:aves/widgets/fullscreen/info/maps/common.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/maps/marker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
import 'package:google_maps_flutter/google_maps_flutter.dart';
|
||||||
import 'package:tuple/tuple.dart';
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class EntryGoogleMap extends StatefulWidget {
|
class EntryGoogleMap extends StatefulWidget {
|
||||||
final String markerId;
|
|
||||||
final LatLng latLng;
|
final LatLng latLng;
|
||||||
final String geoUri;
|
final String geoUri;
|
||||||
final double initialZoom;
|
final double initialZoom;
|
||||||
|
final String markerId;
|
||||||
|
final WidgetBuilder markerBuilder;
|
||||||
|
|
||||||
EntryGoogleMap({
|
EntryGoogleMap({
|
||||||
Key key,
|
Key key,
|
||||||
this.markerId,
|
|
||||||
Tuple2<double, double> latLng,
|
Tuple2<double, double> latLng,
|
||||||
this.geoUri,
|
this.geoUri,
|
||||||
this.initialZoom,
|
this.initialZoom,
|
||||||
|
this.markerId,
|
||||||
|
this.markerBuilder,
|
||||||
}) : latLng = LatLng(latLng.item1, latLng.item2),
|
}) : latLng = LatLng(latLng.item1, latLng.item2),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
|
@ -26,6 +32,13 @@ class EntryGoogleMap extends StatefulWidget {
|
||||||
|
|
||||||
class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveClientMixin {
|
class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveClientMixin {
|
||||||
GoogleMapController _controller;
|
GoogleMapController _controller;
|
||||||
|
Completer<Uint8List> _markerLoaderCompleter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_markerLoaderCompleter = Completer<Uint8List>();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(EntryGoogleMap oldWidget) {
|
void didUpdateWidget(EntryGoogleMap oldWidget) {
|
||||||
|
@ -33,6 +46,9 @@ class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveC
|
||||||
if (widget.latLng != oldWidget.latLng && _controller != null) {
|
if (widget.latLng != oldWidget.latLng && _controller != null) {
|
||||||
_controller.moveCamera(CameraUpdate.newLatLng(widget.latLng));
|
_controller.moveCamera(CameraUpdate.newLatLng(widget.latLng));
|
||||||
}
|
}
|
||||||
|
if (widget.markerId != oldWidget.markerId) {
|
||||||
|
_markerLoaderCompleter = Completer<Uint8List>();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -46,6 +62,11 @@ class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveC
|
||||||
super.build(context);
|
super.build(context);
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
|
MarkerGeneratorWidget(
|
||||||
|
key: Key(widget.markerId),
|
||||||
|
markers: [widget.markerBuilder(context)],
|
||||||
|
onComplete: (bitmaps) => _markerLoaderCompleter.complete(bitmaps.first),
|
||||||
|
),
|
||||||
MapDecorator(
|
MapDecorator(
|
||||||
child: _buildMap(),
|
child: _buildMap(),
|
||||||
),
|
),
|
||||||
|
@ -58,7 +79,18 @@ class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveC
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMap() {
|
Widget _buildMap() {
|
||||||
final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue;
|
return FutureBuilder<Uint8List>(
|
||||||
|
future: _markerLoaderCompleter.future,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final markers = <Marker>{};
|
||||||
|
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) {
|
||||||
|
final markerBytes = snapshot.data;
|
||||||
|
markers.add(Marker(
|
||||||
|
markerId: MarkerId(widget.markerId),
|
||||||
|
icon: BitmapDescriptor.fromBytes(markerBytes),
|
||||||
|
position: widget.latLng,
|
||||||
|
));
|
||||||
|
}
|
||||||
return GoogleMap(
|
return GoogleMap(
|
||||||
// GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493
|
// GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493
|
||||||
initialCameraPosition: CameraPosition(
|
initialCameraPosition: CameraPosition(
|
||||||
|
@ -78,14 +110,9 @@ class EntryGoogleMapState extends State<EntryGoogleMap> with AutomaticKeepAliveC
|
||||||
tiltGesturesEnabled: false,
|
tiltGesturesEnabled: false,
|
||||||
myLocationEnabled: false,
|
myLocationEnabled: false,
|
||||||
myLocationButtonEnabled: false,
|
myLocationButtonEnabled: false,
|
||||||
markers: {
|
markers: markers,
|
||||||
Marker(
|
|
||||||
markerId: MarkerId(widget.markerId),
|
|
||||||
icon: BitmapDescriptor.defaultMarkerWithHue(accentHue),
|
|
||||||
position: widget.latLng,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _zoomBy(double amount) {
|
void _zoomBy(double amount) {
|
||||||
|
|
|
@ -15,6 +15,8 @@ class EntryLeafletMap extends StatefulWidget {
|
||||||
final String geoUri;
|
final String geoUri;
|
||||||
final double initialZoom;
|
final double initialZoom;
|
||||||
final EntryMapStyle style;
|
final EntryMapStyle style;
|
||||||
|
final Size markerSize;
|
||||||
|
final WidgetBuilder markerBuilder;
|
||||||
|
|
||||||
EntryLeafletMap({
|
EntryLeafletMap({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -22,6 +24,8 @@ class EntryLeafletMap extends StatefulWidget {
|
||||||
this.geoUri,
|
this.geoUri,
|
||||||
this.initialZoom,
|
this.initialZoom,
|
||||||
this.style,
|
this.style,
|
||||||
|
this.markerBuilder,
|
||||||
|
this.markerSize,
|
||||||
}) : latLng = LatLng(latLng.item1, latLng.item2),
|
}) : latLng = LatLng(latLng.item1, latLng.item2),
|
||||||
super(key: key);
|
super(key: key);
|
||||||
|
|
||||||
|
@ -32,8 +36,6 @@ class EntryLeafletMap extends StatefulWidget {
|
||||||
class EntryLeafletMapState extends State<EntryLeafletMap> with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
|
class EntryLeafletMapState extends State<EntryLeafletMap> with AutomaticKeepAliveClientMixin, TickerProviderStateMixin {
|
||||||
final MapController _mapController = MapController();
|
final MapController _mapController = MapController();
|
||||||
|
|
||||||
static const markerSize = 40.0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didUpdateWidget(EntryLeafletMap oldWidget) {
|
void didUpdateWidget(EntryLeafletMap oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
|
@ -80,16 +82,10 @@ class EntryLeafletMapState extends State<EntryLeafletMap> with AutomaticKeepAliv
|
||||||
options: MarkerLayerOptions(
|
options: MarkerLayerOptions(
|
||||||
markers: [
|
markers: [
|
||||||
Marker(
|
Marker(
|
||||||
width: markerSize,
|
width: widget.markerSize.width,
|
||||||
height: markerSize,
|
height: widget.markerSize.height,
|
||||||
point: widget.latLng,
|
point: widget.latLng,
|
||||||
builder: (ctx) {
|
builder: widget.markerBuilder,
|
||||||
return Icon(
|
|
||||||
Icons.place,
|
|
||||||
size: markerSize,
|
|
||||||
color: Theme.of(context).accentColor,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
anchorPos: AnchorPos.align(AnchorAlign.top),
|
anchorPos: AnchorPos.align(AnchorAlign.top),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
182
lib/widgets/fullscreen/info/maps/marker.dart
Normal file
182
lib/widgets/fullscreen/info/maps/marker.dart
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/widgets/collection/thumbnail/raster.dart';
|
||||||
|
import 'package:aves/widgets/collection/thumbnail/vector.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class ImageMarker extends StatelessWidget {
|
||||||
|
final ImageEntry entry;
|
||||||
|
final double extent;
|
||||||
|
final Size pointerSize;
|
||||||
|
|
||||||
|
static const double outerBorderRadiusDim = 8;
|
||||||
|
static const double outerBorderWidth = 1.5;
|
||||||
|
static const double innerBorderWidth = 2;
|
||||||
|
static const outerBorderColor = Colors.white30;
|
||||||
|
static final innerBorderColor = Colors.grey[900];
|
||||||
|
|
||||||
|
const ImageMarker({
|
||||||
|
@required this.entry,
|
||||||
|
@required this.extent,
|
||||||
|
this.pointerSize = Size.zero,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final thumbnail = entry.isSvg
|
||||||
|
? ThumbnailVectorImage(
|
||||||
|
entry: entry,
|
||||||
|
extent: extent,
|
||||||
|
)
|
||||||
|
: ThumbnailRasterImage(
|
||||||
|
entry: entry,
|
||||||
|
extent: extent,
|
||||||
|
);
|
||||||
|
|
||||||
|
final outerBorderRadius = BorderRadius.circular(outerBorderRadiusDim);
|
||||||
|
final innerBorderRadius = BorderRadius.circular(outerBorderRadiusDim - outerBorderWidth);
|
||||||
|
|
||||||
|
return CustomPaint(
|
||||||
|
foregroundPainter: MarkerPointerPainter(
|
||||||
|
color: innerBorderColor,
|
||||||
|
outlineColor: outerBorderColor,
|
||||||
|
outlineWidth: outerBorderWidth,
|
||||||
|
size: pointerSize,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: pointerSize.height),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: outerBorderColor,
|
||||||
|
width: outerBorderWidth,
|
||||||
|
),
|
||||||
|
borderRadius: outerBorderRadius,
|
||||||
|
),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: innerBorderColor,
|
||||||
|
width: innerBorderWidth,
|
||||||
|
),
|
||||||
|
borderRadius: innerBorderRadius,
|
||||||
|
),
|
||||||
|
position: DecorationPosition.foreground,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: innerBorderRadius,
|
||||||
|
child: thumbnail,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MarkerPointerPainter extends CustomPainter {
|
||||||
|
final Color color, outlineColor;
|
||||||
|
final double outlineWidth;
|
||||||
|
final Size size;
|
||||||
|
|
||||||
|
const MarkerPointerPainter({
|
||||||
|
this.color,
|
||||||
|
this.outlineColor,
|
||||||
|
this.outlineWidth,
|
||||||
|
this.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final pointerWidth = this.size.width;
|
||||||
|
final pointerHeight = this.size.height;
|
||||||
|
|
||||||
|
final bottomCenter = Offset(size.width / 2, size.height);
|
||||||
|
final topLeft = bottomCenter + Offset(-pointerWidth / 2, -pointerHeight);
|
||||||
|
final topRight = bottomCenter + Offset(pointerWidth / 2, -pointerHeight);
|
||||||
|
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(bottomCenter.dx, bottomCenter.dy)
|
||||||
|
..lineTo(topRight.dx, topRight.dy)
|
||||||
|
..lineTo(topLeft.dx, topLeft.dy)
|
||||||
|
..close(),
|
||||||
|
Paint()..color = outlineColor);
|
||||||
|
|
||||||
|
canvas.translate(0, -outlineWidth.ceilToDouble());
|
||||||
|
canvas.drawPath(
|
||||||
|
Path()
|
||||||
|
..moveTo(bottomCenter.dx, bottomCenter.dy)
|
||||||
|
..lineTo(topRight.dx, topRight.dy)
|
||||||
|
..lineTo(topLeft.dx, topLeft.dy)
|
||||||
|
..close(),
|
||||||
|
Paint()..color = color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate bitmap from widget, for Google Maps
|
||||||
|
class MarkerGeneratorWidget extends StatefulWidget {
|
||||||
|
final List<Widget> markers;
|
||||||
|
final Duration delay;
|
||||||
|
final Function(List<Uint8List> bitmaps) onComplete;
|
||||||
|
|
||||||
|
const MarkerGeneratorWidget({
|
||||||
|
Key key,
|
||||||
|
@required this.markers,
|
||||||
|
this.delay = Duration.zero,
|
||||||
|
@required this.onComplete,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_MarkerGeneratorWidgetState createState() => _MarkerGeneratorWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MarkerGeneratorWidgetState extends State<MarkerGeneratorWidget> {
|
||||||
|
final _globalKeys = <GlobalKey>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
if (widget.delay > Duration.zero) {
|
||||||
|
await Future.delayed(widget.delay);
|
||||||
|
}
|
||||||
|
widget.onComplete(await _getBitmaps(context));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Transform.translate(
|
||||||
|
offset: Offset(MediaQuery.of(context).size.width, 0),
|
||||||
|
child: Material(
|
||||||
|
type: MaterialType.transparency,
|
||||||
|
child: Stack(
|
||||||
|
children: widget.markers.map((i) {
|
||||||
|
final key = GlobalKey();
|
||||||
|
_globalKeys.add(key);
|
||||||
|
return RepaintBoundary(
|
||||||
|
key: key,
|
||||||
|
child: i,
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Uint8List>> _getBitmaps(BuildContext context) async {
|
||||||
|
final pixelRatio = MediaQuery.of(context).devicePixelRatio;
|
||||||
|
return Future.wait(_globalKeys.map((key) async {
|
||||||
|
RenderRepaintBoundary boundary = key.currentContext.findRenderObject();
|
||||||
|
final image = await boundary.toImage(pixelRatio: pixelRatio);
|
||||||
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
return byteData.buffer.asUint8List();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue