maplibre / maplibre-react-native

A MapLibre react native module for creating custom maps
Other
172 stars 39 forks source link

Animate the MarkerView #334

Closed timweii closed 1 month ago

timweii commented 1 month ago

Hello, I'd like to request an enhancement for animating the MarkerView using react-native-reanimated. This feature has been successfully implemented in rnmapbox/maps. That would be great if we can also have this feature available in maplibre-react-native as well. Could this enhancement be considered?

Thanks!

Reference: Discussion: https://github.com/rnmapbox/maps/discussions/3097 Commit: https://github.com/jleprinc/rnmapbox-animation/commit/71819c75d045df0e5f87ed85a706b6d55dba4204

tyrauber commented 1 month ago

Hi @timweii, I have done this in rnmapbox/maps without modifying the library, and the same should work in maplibre-react-native using react-native-reanimated createAnimatedComponent and useAnimatedProps.

Something like this should work:

import Animated, { useAnimatedProps } from 'react-native-reanimated';
import { createAnimatedPropAdapter } from 'react-native-reanimated/src/reanimated2/PropAdapters';

 const [followPoint, setFollowPoint] = useState(null);
const AnimatedShape = Animated.createAnimatedComponent(Maplibre.ShapeSource);

  const animatedProps = useAnimatedProps(
    () => {
      const shape = {
              type: 'FeatureCollection',
              features: [
                {
                  type: 'Feature',
                  properties: {
                    type: 'generalMarker',
                    icon: 'example',
                    id: 'generalMarker'
                  },
                  geometry: {
                    type: 'Point',
                    coordinates: followPoint
                  },
                },
              ],
            }
      return { shape };
    },
    null,
    shapeAdapter
  );
      <AnimatedShape id="exampleShapeSource" animatedProps={animatedProps}>
        <MapLibre.CircleLayer id="singlePoint" style={layerStyle.singlePoint} />
      </AnimatedShape>
timweii commented 1 month ago

Hi @tyrauber, thank you for getting back to me. I've created a basic animation component to try out your code (see attached below). However, it throws an error when the second followPoint is passed. Exception in HostFunction: com.facebook.react.bridge.JSApplicationIllegalArgumentException: Error while updating property 'shape' of a view managed by: RCTMGLShapeSource

I am wondering if it encounters a similar issue when attempting to animate a marker view: In the android/rctmgl/src/main/java/com/mapbox/rctmgl/components/annotation/RCTMGLMarkerViewManager.java file, the setCoordinate method expects a geoJSON string. However, in the current version of rnmapbox/maps, it's expecting an array - so the animation can update the props success because they are the same format? Could this difference potentially cause failure?

Similarly, in components/ShapeSource.tsx, the RCTMGLShapeSource expects shape props as a JSONString. but during animation, it updates the shape with a geoJSON object instead of a JSONString, which could lead to failures?

import Animated, { useAnimatedProps, useSharedValue } from 'react-native-reanimated';
import { ShapeSource, CircleLayer } from '@app/lib/map';
import { useEffect } from 'react';

const layerStyle = {
  singlePoint: {
    circleRadius: 5,
    circleColor: '#FF6347',
    circleStrokeWidth: 1,
    circleStrokeColor: '#FFFFFF',
  },
};
const AnimatedShape = Animated.createAnimatedComponent(ShapeSource);

export default ({ followPoint }) => {
  const animatedFollowPoint = useSharedValue(followPoint);

  useEffect(() => {
    animatedFollowPoint.value = followPoint;
  }, [followPoint]);

  const animatedProps = useAnimatedProps(
    () => ({
      shape: {
        type: 'FeatureCollection',
        features: [
          {
            type: 'Feature',
            properties: {
              type: 'generalMarker',
              id: 'generalMarker',
            },
            geometry: {
              type: 'Point',
              coordinates: animatedFollowPoint.value,
            },
          },
        ],
      },
    }),
    [animatedFollowPoint]
  );

  return (
    <AnimatedShape id='exampleShapeSource' animatedProps={animatedProps}>
      <CircleLayer id='singlePoint' style={layerStyle.singlePoint} />
    </AnimatedShape>
  );
};
tyrauber commented 1 month ago

Hi @timweii, You are correct. The native libraries are expecting JSON, but the JS API should handle the conversion.

If you look at ShapeSource, it says it accepts a GeoJSON geometry, a feature, or a feature colllection and setNativeProps converts the GeoJSON to a JSON string before passing it to the native library, so that should be ok.

But, try whitelisting the native props:

const AnimatedShape = Animated.createAnimatedComponent(ShapeSource);
//Animated.addWhitelistedUIProps({ shape: true });
Animated.addWhitelistedNativeProps({ shape: true });

If that doesn't work, try converting the shape prop to JSON manually:

const shapeAdapter = createAnimatedPropAdapter((props) => {
  props.shape = JSON.stringify(props.shape);
})

Let me know what you find!

timweii commented 1 month ago

Thank you @tyrauber! Animated.addWhitelistedNativeProps({ shape: true }) works perfectly. Closing this issue now.