Search code examples
react-nativereact-native-gesture-handlerreact-native-reanimated-v2

Panning an Animated.View, after being scaled, inside boundaries of parent View


I am using react-native-reanimated and react-native-gesture-handler to create a view that allows you to "explore" what's inside of it (even if it exceeds its width and height).

Here's my gestureHandler that updates translationX & translationY variables, later used in useAnimatedStyle to "move" the <Animated.View>:

const gestureHandler = useAnimatedGestureHandler({
  onStart: (_, ctx) => {
    ctx.startX = translationX.value;
    ctx.startY = translationY.value;
  },
  onActive: (event, ctx) => {
    'worklet';
    translationX.value = ctx.startX + event.translationX;
    translationY.value = ctx.startY + event.translationY;

    // update state to view values on the screen as they change
    runOnJS(setPosition)({ x: translationX.value, y: translationY.value });
  },
  onEnd: () => {
    'worklet';
    const boundedX = clamp(
      translationX.value,
      (BOX_WIDTH - container?.width) * -1,
      0
    );
    const boundedY = clamp(
      translationY.value,
      (BOX_HEIGHT - container?.height) * -1,
      0
    );

    // create "bounce-y" effect when moving the box back inside the bounds
    translationX.value = withTiming(boundedX);
    translationY.value = withTiming(boundedY);

    // update state to view values on the screen as they change
    runOnJS(setPosition)({ x: boundedX, y: boundedY });
  },
});

This code "works" in the following case:

  • I have a "visible area" of width: 100, height: 100
  • "box" (element that is being panned) of width: 160, height: 160

Here's a gif (click to view in full size):

I created an example as a Expo Snack displaying my problem. If you change INITIAL_SCALE in Transforms.js to 0.5 or just tap the pink box (it changes its scale to NEW_SCALE, see onPress()), panning in boundaries no longer works.


Solution

  • I found the problem, it was scaling the box using its origin (the center), so before applying the scale transform, I had to translate it to "fake" setting its origin to the top left corner.

    { translateX: -BOX_WIDTH / 2 },
    { translateY: -BOX_HEIGHT / 2 },
    { scale: scale.value }, // <- NOW I am changing the scale of the box
    { translateX: BOX_WIDTH / 2 },
    { translateY: BOX_HEIGHT / 2},
    

    I also made a function for calculating the "edges" of the outer box that have in mind the pink box's scale:

    const getEdges = () => {
      'worklet';
      const pointX =
        (BOX_WIDTH * scale.value - VISIBLE_AREA_WIDTH) * -1;
      const pointY =
        (BOX_HEIGHT * scale.value - VISIBLE_AREA_HEIGHT) * -1;
      return {
        x: {
          min: Math.min(pointX, 0),
          max: Math.max(0, pointX),
        },
        y: {
          min: Math.min(pointY, 0),
          max: Math.max(0, pointY),
        },
      };
    };
    

    Here's a working Snack with these fixes.

    Helpful information: