info: custom marker on map

This commit is contained in:
Thibault Deckers 2020-09-26 11:41:53 +09:00
parent 49637ede95
commit 96422e3340
5 changed files with 263 additions and 43 deletions

View file

@ -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,

View file

@ -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)

View file

@ -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) {

View file

@ -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),
), ),
], ],

View 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();
}));
}
}