Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/content/docs/components/map-view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,11 @@ mounting many live maps is expensive:
not support cloud-based styling, so `mapId` is ignored on lite static maps.
Lite mode caps its bitmap at ~2048px per dimension, so larger views (e.g.
full-screen maps) fall back to a full map with all gestures disabled.
- **iOS (Apple)** - the base map is rendered with `MKMapSnapshotter`, which
loads entirely off the main thread (no live map view is ever created), so
static maps keep loading while scrolling. Markers stay live views
- **iOS (Apple)** - `MKMapSnapshotter` renders the base map off the main
thread, so static maps keep loading while scrolling. With nonzero
`edgeInsets`, a temporary `MKMapView` renders the image to keep Apple Maps
attribution inside the inset viewport. The map view is released after capture.
Markers stay live views
positioned over the base map; polylines, polygons, circles, and ground
overlays are drawn onto it. Tile overlays are not supported.
- **iOS (Google)** - markers and shapes render as live overlay views,
Expand Down Expand Up @@ -98,7 +100,8 @@ Notes:
at the final camera (iOS) or re-centers (Android). Map-setting prop
updates (e.g. `mapType`) after the snapshot are still ignored on iOS.
- `edgeInsets` shift the visible center like on a live map, so the
coordinate centers in the inset viewport. Changing insets after the
coordinate centers in the inset viewport without changing the zoom.
On iOS, attribution also respects these insets. Changing insets after the
render re-renders the base map (iOS) or re-centers the camera (Android).
- For taps, wrap the map in a `Pressable` with `pointerEvents="none"` on the
map container (as above) instead of relying on `onPress`.
Expand Down
3 changes: 3 additions & 0 deletions docs/content/docs/usage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ Mounting many live maps is expensive. Use `staticMode` to render lightweight, no
</MapView>
```

The example app uses the same markers and polyline in each static list map and its detail screen.
On iOS, both screens include a button to switch between Apple Maps and Google Maps.

Read more in [Static Maps](./components/map-view.mdx#static-maps).

## Next steps
Expand Down
55 changes: 48 additions & 7 deletions example/shared/src/screens/MarkerDetailScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import { StyleSheet, View } from 'react-native';
import { MapProvider, MapView, Marker } from '@lugg/maps';
import { useState } from 'react';
import { Platform, StyleSheet, View, useWindowDimensions } from 'react-native';
import { MapProvider, MapView, Marker, type MapProviderType } from '@lugg/maps';

import { ThemedText } from '../components';
import { Button, ThemedText } from '../components';
import { INITIAL_MARKERS } from '../markers';
import { CIRCLE_CENTER } from '../mapData';
import { sizes, useTheme } from '../theme';
import { PLACES } from './StaticMapsScreen';
import {
PLACES,
PlaceConstellation,
PlaceMarkers,
fittedCamera,
placeCoordinates,
} from './StaticMapsScreen';

interface MarkerDetailScreenProps {
name: string;
Expand All @@ -18,7 +25,11 @@ const formatCoordinate = (value: number) => value.toFixed(4);

export const MarkerDetailScreen = ({ name }: MarkerDetailScreenProps) => {
const { colors } = useTheme();
const { width, height } = useWindowDimensions();
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
const [provider, setProvider] = useState<MapProviderType>(
Platform.OS === 'ios' ? 'apple' : 'google'
);

const marker = INITIAL_MARKERS.find((m) => m.name === name);
const place = PLACES.find((p) => p.name === name);
Expand All @@ -27,26 +38,51 @@ export const MarkerDetailScreen = ({ name }: MarkerDetailScreenProps) => {
const title = marker?.title ?? place?.name ?? name;
const description =
marker?.description ?? place?.description ?? 'Marker detail screen';
const camera = place
? fittedCamera(
placeCoordinates(place),
provider,
{ width, height: height - CARD_BOTTOM - CARD_HEIGHT },
sizes.xl
)
: { coordinate, zoom: 15 };

return (
<View style={styles.container}>
<MapProvider apiKey={apiKey}>
<MapView
key={provider}
provider={provider}
style={StyleSheet.absoluteFill}
staticMode
staticKey={name}
initialCoordinate={coordinate}
initialZoom={15}
initialCoordinate={camera.coordinate}
initialZoom={camera.zoom}
edgeInsets={{
top: 0,
left: 0,
right: 0,
bottom: CARD_BOTTOM + CARD_HEIGHT,
}}
>
<Marker coordinate={coordinate} />
{place ? (
<>
<PlaceMarkers place={place} />
<PlaceConstellation place={place} />
</>
) : (
<Marker coordinate={coordinate} />
)}
</MapView>
</MapProvider>
<Button
style={styles.providerButton}
title={provider === 'google' ? 'Apple Maps' : 'Google Maps'}
disabled={Platform.OS !== 'ios'}
onPress={() =>
setProvider((p) => (p === 'google' ? 'apple' : 'google'))
}
/>
<View
style={[
styles.overlay,
Expand Down Expand Up @@ -76,6 +112,11 @@ const styles = StyleSheet.create({
container: {
flex: 1,
},
providerButton: {
position: 'absolute',
right: sizes.lg,
bottom: CARD_BOTTOM + CARD_HEIGHT + sizes.lg,
},
overlay: {
position: 'absolute',
left: sizes.lg,
Expand Down
8 changes: 4 additions & 4 deletions example/shared/src/screens/StaticMapsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ const multipleMarkerCoordinates = (coordinate: Coordinate): Coordinate[] => [

// Every coordinate rendered on a place's map (markers + constellation),
// used to fit the camera around the content
const placeCoordinates = (place: StaticPlace): Coordinate[] => {
export const placeCoordinates = (place: StaticPlace): Coordinate[] => {
const coordinates = [
place.coordinate,
...constellationPoints(place.coordinate, seedFromId(place.id)),
Expand All @@ -162,7 +162,7 @@ const mercatorY = (latitude: number) =>
// Matches the native static framing: Google shows the world 256 * 2^zoom
// points wide; Apple fits a square span rect to the view's short side
// (see LuggStaticFittedMapRect)
const fittedCamera = (
export const fittedCamera = (
coordinates: Coordinate[],
provider: MapProviderType,
size: { width: number; height: number },
Expand Down Expand Up @@ -202,7 +202,7 @@ const fittedCamera = (
return { coordinate, zoom };
};

const PlaceConstellation = ({ place }: { place: StaticPlace }) => {
export const PlaceConstellation = ({ place }: { place: StaticPlace }) => {
const points = constellationPoints(place.coordinate, seedFromId(place.id));
return (
<>
Expand All @@ -220,7 +220,7 @@ const PlaceConstellation = ({ place }: { place: StaticPlace }) => {
);
};

const PlaceMarkers = ({ place }: { place: StaticPlace }) => {
export const PlaceMarkers = ({ place }: { place: StaticPlace }) => {
const { coordinate, markerType, markerText, imageUrl } = place;

switch (markerType) {
Expand Down
23 changes: 7 additions & 16 deletions ios/core/AppleMapProvider.mm
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#import "../LuggTileOverlayView.h"
#import "../extensions/MKMapView+Zoom.h"
#import "LuggAnnotationView.h"
#import "MapEdgeInsets.h"
#import "MKPolylineAnimator.h"

@interface AppleMarkerAnnotation : NSObject <MKAnnotation>
Expand Down Expand Up @@ -465,13 +466,11 @@ - (void)setMaxZoom:(double)maxZoom {

- (void)setEdgeInsets:(UIEdgeInsets)edgeInsets
oldEdgeInsets:(UIEdgeInsets)oldEdgeInsets {
CGFloat oldOffsetX = (oldEdgeInsets.left - oldEdgeInsets.right) / 2.0;
CGFloat oldOffsetY = (oldEdgeInsets.top - oldEdgeInsets.bottom) / 2.0;
CGFloat newOffsetX = (edgeInsets.left - edgeInsets.right) / 2.0;
CGFloat newOffsetY = (edgeInsets.top - edgeInsets.bottom) / 2.0;
CGPoint oldOffset = LuggMapInsetOffset(oldEdgeInsets);
CGPoint newOffset = LuggMapInsetOffset(edgeInsets);

CGFloat deltaX = newOffsetX - oldOffsetX;
CGFloat deltaY = newOffsetY - oldOffsetY;
CGFloat deltaX = newOffset.x - oldOffset.x;
CGFloat deltaY = newOffset.y - oldOffset.y;

_mapView.layoutMargins = edgeInsets;
_edgeInsetsCurrent = edgeInsets;
Expand Down Expand Up @@ -514,16 +513,8 @@ - (void)edgeInsetsAnimationTick:(CADisplayLink *)displayLink {
CFTimeInterval elapsed = CACurrentMediaTime() - _edgeInsetsAnimationStart;
CGFloat progress = MIN(elapsed / _edgeInsetsAnimationDuration, 1.0);

// Ease out cubic
CGFloat t = 1.0 - (1.0 - progress) * (1.0 - progress) * (1.0 - progress);

UIEdgeInsets current = UIEdgeInsetsMake(
_edgeInsetsFrom.top + (_edgeInsetsTo.top - _edgeInsetsFrom.top) * t,
_edgeInsetsFrom.left + (_edgeInsetsTo.left - _edgeInsetsFrom.left) * t,
_edgeInsetsFrom.bottom +
(_edgeInsetsTo.bottom - _edgeInsetsFrom.bottom) * t,
_edgeInsetsFrom.right +
(_edgeInsetsTo.right - _edgeInsetsFrom.right) * t);
UIEdgeInsets current =
LuggMapInsetsAtProgress(_edgeInsetsFrom, _edgeInsetsTo, progress);

[self setEdgeInsets:current oldEdgeInsets:_edgeInsetsCurrent];

Expand Down
74 changes: 74 additions & 0 deletions ios/core/AppleStaticMapProvider.mm
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using facebook::react::LuggMapViewTheme;

#import "../extensions/MKMapView+Zoom.h"
#import "../extensions/UIView+Snapshot.h"
#import "AppleMapProvider.h"
#import "LuggAnnotationView.h"

Expand Down Expand Up @@ -42,8 +43,13 @@ static MKMapRect LuggStaticFittedMapRect(CLLocationCoordinate2D center,
return rect;
}

@interface AppleStaticMapProvider () <MKMapViewDelegate>
@end

@implementation AppleStaticMapProvider {
MKMapSnapshotter *_snapshotter;
MKMapView *_warmupMapView;
BOOL _snapshotReady;

BOOL _poiEnabled;
LuggMapViewPoiFilterMode _poiFilterMode;
Expand Down Expand Up @@ -106,6 +112,12 @@ - (UITraitCollection *)snapshotTraitCollection {
}

- (void)renderBaseMap {
// MKMapSnapshotter bakes attribution into the image without inset control.
if (!UIEdgeInsetsEqualToEdgeInsets(self.edgeInsets, UIEdgeInsetsZero)) {
[self renderInsetBaseMap];
return;
}

if (_snapshotter)
return;

Expand Down Expand Up @@ -140,6 +152,68 @@ - (void)renderBaseMap {
- (void)cancelBaseRender {
[_snapshotter cancel];
_snapshotter = nil;
_warmupMapView.delegate = nil;
[_warmupMapView removeFromSuperview];
_warmupMapView = nil;
_snapshotReady = NO;
}

#pragma mark - Inset snapshots

- (void)renderInsetBaseMap {
if (_warmupMapView) {
if (!_snapshotReady || !_warmupMapView.window)
return;

UIImageView *imageView = [_warmupMapView lugg_snapshotImageView];
if (!imageView)
return;

[self displayBaseImage:imageView.image fromCache:NO];
[self cancelBaseRender];
return;
}

MKMapView *mapView =
[[MKMapView alloc] initWithFrame:self.wrapperView.bounds];
mapView.userInteractionEnabled = NO;
mapView.showsCompass = NO;
mapView.insetsLayoutMarginsFromSafeArea = NO;
mapView.layoutMargins = self.edgeInsets;
mapView.mapType = LuggMKMapTypeFromMapType(self.mapType);
mapView.pointOfInterestFilter = LuggPointOfInterestFilter(
_poiEnabled, _poiFilterMode, _poiFilterCategories);
mapView.overrideUserInterfaceStyle =
[self snapshotTraitCollection].userInterfaceStyle;
[mapView setVisibleMapRect:self.mapRect
edgePadding:UIEdgeInsetsZero
animated:NO];
_warmupMapView = mapView;
mapView.delegate = self;
[self.wrapperView insertSubview:mapView atIndex:0];
}

- (void)mapViewWillStartRenderingMap:(MKMapView *)mapView {
_snapshotReady = NO;
}

- (void)mapViewDidFinishRenderingMap:(MKMapView *)mapView
fullyRendered:(BOOL)fullyRendered {
_snapshotReady = fullyRendered;
[self revealOverlays];
if (!fullyRendered)
return;

__weak AppleStaticMapProvider *weakSelf = self;
__weak MKMapView *weakMapView = mapView;
[[NSRunLoop mainRunLoop]
performInModes:@[ NSDefaultRunLoopMode ]
block:^{
AppleStaticMapProvider *strongSelf = weakSelf;
if (strongSelf && weakMapView &&
strongSelf->_warmupMapView == weakMapView)
[strongSelf renderInsetBaseMap];
}];
}

#pragma mark - Props
Expand Down
13 changes: 3 additions & 10 deletions ios/core/GoogleMapProvider.mm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
#import "../LuggPolylineView.h"
#import "../LuggTileOverlayView.h"
#import "GMSPolylineAnimator.h"
#import "MapEdgeInsets.h"
#import "PolylineAnimatorBase.h"

static NSString *const kDemoMapId = @"DEMO_MAP_ID";
Expand Down Expand Up @@ -296,16 +297,8 @@ - (void)edgeInsetsAnimationTick:(CADisplayLink *)displayLink {
CFTimeInterval elapsed = CACurrentMediaTime() - _edgeInsetsAnimationStart;
CGFloat progress = MIN(elapsed / _edgeInsetsAnimationDuration, 1.0);

// Ease out cubic
CGFloat t = 1.0 - (1.0 - progress) * (1.0 - progress) * (1.0 - progress);

UIEdgeInsets current = UIEdgeInsetsMake(
_edgeInsetsFrom.top + (_edgeInsetsTo.top - _edgeInsetsFrom.top) * t,
_edgeInsetsFrom.left + (_edgeInsetsTo.left - _edgeInsetsFrom.left) * t,
_edgeInsetsFrom.bottom +
(_edgeInsetsTo.bottom - _edgeInsetsFrom.bottom) * t,
_edgeInsetsFrom.right +
(_edgeInsetsTo.right - _edgeInsetsFrom.right) * t);
UIEdgeInsets current =
LuggMapInsetsAtProgress(_edgeInsetsFrom, _edgeInsetsTo, progress);

_mapView.padding = current;

Expand Down
18 changes: 18 additions & 0 deletions ios/core/MapEdgeInsets.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once

#import <UIKit/UIKit.h>

static inline CGPoint LuggMapInsetOffset(UIEdgeInsets insets) {
return CGPointMake((insets.left - insets.right) / 2.0,
(insets.top - insets.bottom) / 2.0);
}

static inline UIEdgeInsets
LuggMapInsetsAtProgress(UIEdgeInsets from, UIEdgeInsets to, CGFloat progress) {
// Ease out cubic
CGFloat t = 1.0 - (1.0 - progress) * (1.0 - progress) * (1.0 - progress);
return UIEdgeInsetsMake(from.top + (to.top - from.top) * t,
from.left + (to.left - from.left) * t,
from.bottom + (to.bottom - from.bottom) * t,
from.right + (to.right - from.right) * t);
}
Loading
Loading