Skip to content
Closed
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
218 changes: 218 additions & 0 deletions src/AnimatedRegion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
import { Animated, Easing } from 'react-native';
import type { Region } from './types';

type RegionInput = Partial<Region>;

type TimingConfig = RegionInput & {
duration?: number;
easing?: (value: number) => number;
delay?: number;
isInteraction?: boolean;
useNativeDriver: boolean;
};

type SpringConfig = RegionInput & {
overshootClamping?: boolean;
restDisplacementThreshold?: number;
restSpeedThreshold?: number;
velocity?: number;
bounciness?: number;
speed?: number;
tension?: number;
friction?: number;
stiffness?: number;
damping?: number;
mass?: number;
delay?: number;
isInteraction?: boolean;
useNativeDriver: boolean;
};

type DecayConfig = RegionInput & {
velocity?: number;
deceleration?: number;
isInteraction?: boolean;
useNativeDriver: boolean;
};

const REGION_KEYS = [
'latitude',
'longitude',
'latitudeDelta',
'longitudeDelta',
] as const;

type RegionKey = (typeof REGION_KEYS)[number];

/**
* RNM-compatible `AnimatedRegion` — a wrapper around four `Animated.Value`s
* exposing the same surface area as `react-native-maps`. The class itself
* does not currently animate the native `MapView`; M10 will wire it up so
* the values can be driven natively. Today it is fully usable in JS, and
* any `addListener` callback fires with a synthesized `Region` snapshot.
*/
export class AnimatedRegion {
latitude: Animated.Value;
longitude: Animated.Value;
latitudeDelta: Animated.Value;
longitudeDelta: Animated.Value;

private _listeners: Record<
string,
{
callback: (region: Region) => void;
tokens: Record<RegionKey, string>;
}
> = {};
private _listenerCounter = 0;

constructor(valueIn?: RegionInput) {
this.latitude = new Animated.Value(valueIn?.latitude ?? 0);
this.longitude = new Animated.Value(valueIn?.longitude ?? 0);
this.latitudeDelta = new Animated.Value(valueIn?.latitudeDelta ?? 0);
this.longitudeDelta = new Animated.Value(valueIn?.longitudeDelta ?? 0);
}

setValue(value: RegionInput) {
if (value.latitude !== undefined) {
this.latitude.setValue(value.latitude);
}
if (value.longitude !== undefined) {
this.longitude.setValue(value.longitude);
}
if (value.latitudeDelta !== undefined) {
this.latitudeDelta.setValue(value.latitudeDelta);
}
if (value.longitudeDelta !== undefined) {
this.longitudeDelta.setValue(value.longitudeDelta);
}
}

setOffset(offset: RegionInput) {
if (offset.latitude !== undefined) {
this.latitude.setOffset(offset.latitude);
}
if (offset.longitude !== undefined) {
this.longitude.setOffset(offset.longitude);
}
if (offset.latitudeDelta !== undefined) {
this.latitudeDelta.setOffset(offset.latitudeDelta);
}
if (offset.longitudeDelta !== undefined) {
this.longitudeDelta.setOffset(offset.longitudeDelta);
}
}

flattenOffset() {
this.latitude.flattenOffset();
this.longitude.flattenOffset();
this.latitudeDelta.flattenOffset();
this.longitudeDelta.flattenOffset();
}

stopAnimation(callback?: (region: Region) => void) {
this.latitude.stopAnimation();
this.longitude.stopAnimation();
this.latitudeDelta.stopAnimation();
this.longitudeDelta.stopAnimation();
callback?.(this.__getValue());
}

addListener(callback: (region: Region) => void): string {
const id = String(++this._listenerCounter);
const fire = () => callback(this.__getValue());
this._listeners[id] = {
callback,
tokens: {
latitude: this.latitude.addListener(fire),
longitude: this.longitude.addListener(fire),
latitudeDelta: this.latitudeDelta.addListener(fire),
longitudeDelta: this.longitudeDelta.addListener(fire),
},
};
return id;
}

removeListener(id: string) {
const listener = this._listeners[id];
if (!listener) {
return;
}
this.latitude.removeListener(listener.tokens.latitude);
this.longitude.removeListener(listener.tokens.longitude);
this.latitudeDelta.removeListener(listener.tokens.latitudeDelta);
this.longitudeDelta.removeListener(listener.tokens.longitudeDelta);
delete this._listeners[id];
}

removeAllListeners() {
Object.keys(this._listeners).forEach((id) => this.removeListener(id));
this.latitude.removeAllListeners();
this.longitude.removeAllListeners();
this.latitudeDelta.removeAllListeners();
this.longitudeDelta.removeAllListeners();
}

spring(config: SpringConfig): Animated.CompositeAnimation {
const { useNativeDriver, ...rest } = config;
return Animated.parallel(
REGION_KEYS.filter((key) => config[key] !== undefined).map((key) =>
Animated.spring(this[key], {
...rest,
toValue: config[key] as number,
useNativeDriver,
})
)
);
}

timing(config: TimingConfig): Animated.CompositeAnimation {
const { useNativeDriver, easing, duration, delay, ...rest } = config;
return Animated.parallel(
REGION_KEYS.filter((key) => config[key] !== undefined).map((key) =>
Animated.timing(this[key], {
...rest,
toValue: config[key] as number,
duration: duration ?? 500,
easing: easing ?? Easing.inOut(Easing.ease),
delay,
useNativeDriver,
})
)
);
}

decay(config: DecayConfig): Animated.CompositeAnimation {
const { useNativeDriver, velocity, deceleration, isInteraction } = config;
return Animated.parallel(
REGION_KEYS.filter((key) => config[key] !== undefined).map((key) =>
Animated.decay(this[key], {
velocity: velocity ?? 0,
deceleration,
isInteraction,
useNativeDriver,
})
)
);
}

__getValue(): Region {
// `Animated.Value` doesn't expose a public getter, but its private
// `_value` is the established RNM-internal way to read the current
// snapshot. Cast through unknown to avoid the `any` lint.
const read = (v: Animated.Value) =>
(v as unknown as { _value: number })._value;
return {
latitude: read(this.latitude),
longitude: read(this.longitude),
latitudeDelta: read(this.latitudeDelta),
longitudeDelta: read(this.longitudeDelta),
};
}

__getAnimatedValue() {
return this.__getValue();
}
}

export default AnimatedRegion;
18 changes: 18 additions & 0 deletions src/MapCallout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useWarnNotImplemented } from './_warnings';
import type { CalloutProps } from './types';

export type CalloutComponent = ((props: CalloutProps) => null) & {
__MAP_CALLOUT: true;
};

export const Callout: CalloutComponent = function Callout(
_props: CalloutProps
) {
useWarnNotImplemented('Callout');
return null;
} as CalloutComponent;

Callout.__MAP_CALLOUT = true;

export default Callout;
export type { CalloutProps as MapCalloutProps };
18 changes: 18 additions & 0 deletions src/MapCalloutSubview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useWarnNotImplemented } from './_warnings';
import type { CalloutSubviewProps } from './types';

export type CalloutSubviewComponent = ((props: CalloutSubviewProps) => null) & {
__MAP_CALLOUT_SUBVIEW: true;
};

export const CalloutSubview: CalloutSubviewComponent = function CalloutSubview(
_props: CalloutSubviewProps
) {
useWarnNotImplemented('CalloutSubview');
return null;
} as CalloutSubviewComponent;

CalloutSubview.__MAP_CALLOUT_SUBVIEW = true;

export default CalloutSubview;
export type { CalloutSubviewProps as MapCalloutSubviewProps };
16 changes: 16 additions & 0 deletions src/MapCircle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { useWarnNotImplemented } from './_warnings';
import type { CircleProps } from './types';

export type CircleComponent = ((props: CircleProps) => null) & {
__MAP_CIRCLE: true;
};

export const Circle: CircleComponent = function Circle(_props: CircleProps) {
useWarnNotImplemented('Circle');
return null;
} as CircleComponent;

Circle.__MAP_CIRCLE = true;

export default Circle;
export type { CircleProps as MapCircleProps };
18 changes: 18 additions & 0 deletions src/MapGeojson.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useWarnNotImplemented } from './_warnings';
import type { GeojsonProps } from './types';

export type GeojsonComponent = ((props: GeojsonProps) => null) & {
__MAP_GEOJSON: true;
};

export const Geojson: GeojsonComponent = function Geojson(
_props: GeojsonProps
) {
useWarnNotImplemented('Geojson');
return null;
} as GeojsonComponent;

Geojson.__MAP_GEOJSON = true;

export default Geojson;
export type { GeojsonProps as MapGeojsonProps };
18 changes: 18 additions & 0 deletions src/MapHeatmap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useWarnNotImplemented } from './_warnings';
import type { HeatmapProps } from './types';

export type HeatmapComponent = ((props: HeatmapProps) => null) & {
__MAP_HEATMAP: true;
};

export const Heatmap: HeatmapComponent = function Heatmap(
_props: HeatmapProps
) {
useWarnNotImplemented('Heatmap');
return null;
} as HeatmapComponent;

Heatmap.__MAP_HEATMAP = true;

export default Heatmap;
export type { HeatmapProps as MapHeatmapProps };
18 changes: 18 additions & 0 deletions src/MapLocalTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useWarnNotImplemented } from './_warnings';
import type { LocalTileProps } from './types';

export type LocalTileComponent = ((props: LocalTileProps) => null) & {
__MAP_LOCAL_TILE: true;
};

export const LocalTile: LocalTileComponent = function LocalTile(
_props: LocalTileProps
) {
useWarnNotImplemented('LocalTile');
return null;
} as LocalTileComponent;

LocalTile.__MAP_LOCAL_TILE = true;

export default LocalTile;
export type { LocalTileProps as MapLocalTileProps };
18 changes: 18 additions & 0 deletions src/MapOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useWarnNotImplemented } from './_warnings';
import type { OverlayProps } from './types';

export type OverlayComponent = ((props: OverlayProps) => null) & {
__MAP_OVERLAY: true;
};

export const Overlay: OverlayComponent = function Overlay(
_props: OverlayProps
) {
useWarnNotImplemented('Overlay');
return null;
} as OverlayComponent;

Overlay.__MAP_OVERLAY = true;

export default Overlay;
export type { OverlayProps as MapOverlayProps };
18 changes: 18 additions & 0 deletions src/MapPolygon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useWarnNotImplemented } from './_warnings';
import type { PolygonProps } from './types';

export type PolygonComponent = ((props: PolygonProps) => null) & {
__MAP_POLYGON: true;
};

export const Polygon: PolygonComponent = function Polygon(
_props: PolygonProps
) {
useWarnNotImplemented('Polygon');
return null;
} as PolygonComponent;

Polygon.__MAP_POLYGON = true;

export default Polygon;
export type { PolygonProps as MapPolygonProps };
18 changes: 18 additions & 0 deletions src/MapPolyline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useWarnNotImplemented } from './_warnings';
import type { PolylineProps } from './types';

export type PolylineComponent = ((props: PolylineProps) => null) & {
__MAP_POLYLINE: true;
};

export const Polyline: PolylineComponent = function Polyline(
_props: PolylineProps
) {
useWarnNotImplemented('Polyline');
return null;
} as PolylineComponent;

Polyline.__MAP_POLYLINE = true;

export default Polyline;
export type { PolylineProps as MapPolylineProps };
Loading