react-native-reanimated-carousel icon indicating copy to clipboard operation
react-native-reanimated-carousel copied to clipboard

Is it possible to have onBeforeSnapToItem property?

Open Fodilahy-mena opened this issue 2 years ago • 1 comments

Hi,

I have a custom pagination and the style changes based on the carousel activeIndex. However, there is a 2-3 seconds of delay before it changes.

So I was wondering if it's possible to have onBeforeSnapToItem fallback function that is fired before navigating to an item. This case my pagination changes faster as we would get the slideIndex to use.

Ex:

The property

/**
     * Callback fired before navigating to an item
*/
    onBeforeSnapToItem?(slideIndex: number): void;

Use case:


onBeforeSnapToItem={(slideIndex: number) => {
      setActiveItem(slideIndex)
}}

Fodilahy-mena avatar Feb 07 '23 09:02 Fodilahy-mena

I found a way to accomplish this using customAnimation prop. You set it to run a function when the animation value is above a certain threshold and then set a variable so it only runs once. This allows you to predict the index while the animation is running without having to wait for the onSnapToItem prop to provide a index (though I still use it to validate the predicted index).

import {
  runOnJS,
  useSharedValue,
} from "react-native-reanimated"

// Update index using onSnapToItem prop
const index = useSharedValue(0)

// Set to false using onActivated from panGestureHandlerProps prop
const isRunOnce = useSharedValue(true)

const customAnimation = useCallback((value: number) => {
  'worklet';

  if(!isRunOnce.value) {
    if(value > 0.15) {
      if(index.value + 1 <= props.children.length - 1) {
        runOnJS(onIndexChange)(index.value + 1)
        isRunOnce.value = true
      }
    } else if(value < -0.15) {
      if(index.value - 1 >= 0) {
        runOnJS(onIndexChange)(index.value - 1)
        isRunOnce.value = true
      }
    }
  }

  // animation...

  return  // your custom animation...

})

However, if you don't update with the validated index, if the predicted index is incorrect, then it will stay incorrect for all future index changes.

...Or, if you are okay with editing and patching the carousel library itself and want something more reliable and/or can't get above working properly, then do this.

  1. Go to Carousel.tsx file and pass onSnapToItem to the carouselController.
const carouselController = useCarouselController({
  onSnapToItem,  // < you will get an error warning, but continue to the next step.
  loop,
  size,
  dataLength,
  autoFillData,
  handlerOffset,
  withAnimation,
  defaultIndex,
  onScrollEnd: () => runOnJS(_onScrollEnd)(),
  onScrollBegin: () => !!onScrollBegin && runOnJS(onScrollBegin)(),
  duration: scrollAnimationDuration,
});
  1. Staying within the Carousel.tsx file, comment out or remove where onSnapToItem is called inside _onScrollEnd function.
const _onScrollEnd = React.useCallback(() => {
  const _sharedIndex = Math.round(getSharedIndex());

  const realIndex = computedRealIndexWithAutoFillData({
    index: _sharedIndex,
    dataLength: rawDataLength,
    loop,
    autoFillData,
  });

  /** ------ !!! Comment or remove this code
  if (onSnapToItem)
          onSnapToItem(realIndex);
  **/

  if (onScrollEnd)
    onScrollEnd(realIndex);
}, [
  loop,
  autoFillData,
  rawDataLength,
  getSharedIndex,
  onSnapToItem,
  onScrollEnd,
]);
  1. Now, go to useCarouselController.tsx file and add this code inside the useAnimatedReaction hook.
useAnimatedReaction(
  () => {
    const handlerOffsetValue = handlerOffset.value;
    const toInt = round(handlerOffsetValue / size) % dataInfo.length;
    const isPositive = handlerOffsetValue <= 0;
    const i = isPositive
      ? Math.abs(toInt)
      : Math.abs(toInt > 0 ? dataInfo.length - toInt : 0);

    const newSharedIndexValue = convertToSharedIndex({
      loop,
      rawDataLength: dataInfo.originalLength,
      autoFillData: autoFillData!,
      index: i,
    });

// -----  !!! Add this code below

  if(index.value !== i) {
      const realIndex = computedRealIndexWithAutoFillData({
        index: i,
        dataLength: dataInfo.originalLength,
        loop,
        autoFillData: autoFillData!,
      })
      runOnJS(onSnapToItem)(realIndex)
    }

// -----

  return {
      i,
      newSharedIndexValue,
    };
  },
  ({ i, newSharedIndexValue }) => {
    index.value = i;
    runOnJS(setSharedIndex)(newSharedIndexValue);
  },
  [
    sharedPreIndex,
    sharedIndex,
    size,
    dataInfo,
    index,
    loop,
    autoFillData,
    handlerOffset,
  ],
);

All done! Now the code is changed to call onSnapToItem with the calculated index every time the index changes (which is after touch up event ending the user's swipe gesture). You can even remove runOnJS(onSnapToItem)(realIndex) with just onSnapToItem(realIndex) and call it as a "worklet" function.

OfficialDarkComet avatar May 26 '23 15:05 OfficialDarkComet

See this example for more details: https://reanimated-carousel.dev/usage

dohooo avatar May 05 '24 14:05 dohooo