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

Can't swipe right in tinder example in docs

Open jony89 opened this issue 1 year ago • 16 comments

See the following tinder example of the latest docs:

https://rn-carousel.dev/Examples/custom-animations/tinder

Can't really swipe right. only left.

jony89 avatar Jan 25 '25 22:01 jony89

Hey @jony89! I'm here to help you with any bugs, questions, or contributions you have in mind. Let's tackle this together!

To enable swiping right in the Tinder example from the react-native-reanimated-carousel documentation, you need to modify the fixedDirection property in the Carousel component. The current setup uses fixedDirection="negative", which restricts swiping to the left. To allow swiping right, you should remove the fixedDirection property or set it to null. Here's the relevant part of the code you need to adjust:

<Carousel
    loop={false}
    style={{
        width: PAGE_WIDTH,
        height: PAGE_HEIGHT,
        justifyContent: "center",
        alignItems: "center",
    }}
    defaultIndex={0}
    vertical={false}
    width={PAGE_WIDTH}
    height={PAGE_HEIGHT}
    data={data}
    onConfigurePanGesture={(g) => {
        g.onChange((e) => {
            "worklet";
            directionAnimVal.value = Math.sign(e.translationX);
        });
    }}
    // Remove or set fixedDirection to null to enable swiping right
    fixedDirection={null}
    renderItem={({ index, item }) => <Item key={index} img={item} />}
    customAnimation={animationStyle}
    windowSize={5}
/>

This change will allow the carousel to support swiping in both directions [1].

To continue talking to Dosu, mention @dosu.


Help Dosu learn! Give it feedback: Great Response | Irrelevant Answer | Incorrect Sources | Too Verbose | Hallucination | Other

dosubot[bot] avatar Jan 25 '25 22:01 dosubot[bot]

Nope changing the fixedDirection to undefined / null does not help

@dosu

jony89 avatar Jan 25 '25 22:01 jony89

@dosu the snap does not work when swiping right

jony89 avatar Jan 28 '25 17:01 jony89

To address the issue with swiping right and the snapping not working, you might want to check the onConfigurePanGesture function. Ensure that it is not disabling the gesture or affecting the swipe direction. For example, make sure that the gesture is enabled:

onConfigurePanGesture={(g) => {
    "worklet";
    g.enabled(true); // Ensure the gesture is enabled
    g.onChange((e) => {
        directionAnimVal.value = Math.sign(e.translationX);
    });
}}

Additionally, verify that the snapEnabled property is set to true, as it controls the snapping behavior:

<Carousel
    ...
    snapEnabled={true} // Ensure snapping is enabled
    ...
/>

These adjustments should help in enabling the swipe right functionality and ensure that snapping works as expected [1][2].

To continue talking to Dosu, mention @dosu.


Help Dosu learn! Give it feedback: Great Response | Irrelevant Answer | Incorrect Sources | Too Verbose | Hallucination | Other

dosubot[bot] avatar Jan 28 '25 17:01 dosubot[bot]

@dosu this does not change a thing

jony89 avatar Jan 28 '25 17:01 jony89

Ensure that the fixedDirection property is not set, as it can restrict swipe directions. Additionally, check the pagingEnabled and snapEnabled properties. Both should be set to true to ensure proper snapping behavior:

<Carousel
    ...
    pagingEnabled={true} // Ensure paging is enabled
    snapEnabled={true}   // Ensure snapping is enabled
    ...
/>

If the issue persists, verify that there are no other configurations or custom animations affecting the swipe direction or snapping behavior. Also, ensure that the onConfigurePanGesture function is correctly enabling gestures and not interfering with the swipe logic.

To continue talking to Dosu, mention @dosu.


Help Dosu learn! Give it feedback: Great Response | Irrelevant Answer | Incorrect Sources | Too Verbose | Hallucination | Other

dosubot[bot] avatar Jan 28 '25 17:01 dosubot[bot]

me too I just copy and parse all code from example and try to do what bot say still happended

zoom2009 avatar Feb 11 '25 15:02 zoom2009

Hey, I've released the v4. Could you try with the latest version and let me know what happened?

dohooo avatar Feb 11 '25 20:02 dohooo

I am having the same problem. Thank you for all your maintenance. It would be helpful if you could provide us with a solution.

My code is as follows.

import * as React from 'react';
import { StyleSheet, View } from 'react-native';
import { Extrapolation, interpolate, SharedValue, useSharedValue } from 'react-native-reanimated';
import Carousel, { ICarouselInstance, TAnimationStyle } from 'react-native-reanimated-carousel';
import { Layout } from '@/constants/Layout';
import { Spot } from '@/types/Spot';
import { CarouselTinderItem } from './CarouselTinderItem';

type Props = {
  carouselRef: React.RefObject<ICarouselInstance>;
  progress: SharedValue<number>;
  data: Spot[];
  onSwipe: (index: number, isLike: boolean, item: Spot) => void;
};

export const CarouselTinder = (props: Props) => {
  const { carouselRef, progress, data, onSwipe } = props;
  const directionAnimVal = useSharedValue(0);

  const animationStyle: TAnimationStyle = React.useCallback(
    (value: number) => {
      'worklet';
      const translateY = interpolate(value, [0, 1], [0, -18]);
      const translateX = interpolate(value, [-1, 0], [PAGE_WIDTH, 0], Extrapolation.CLAMP) * directionAnimVal.value;
      const rotateZ = interpolate(value, [-1, 0], [15, 0], Extrapolation.CLAMP) * directionAnimVal.value;
      const zIndex = interpolate(
        value,
        [0, 1, 2, 3, 4],
        [0, 1, 2, 3, 4].map((v) => (data.length - v) * 10),
        Extrapolation.CLAMP,
      );
      const scale = interpolate(value, [0, 1], [1, 0.95]);
      const opacity = interpolate(value, [-1, -0.8, 0, 1], [0, 0.9, 1, 0.85], Extrapolation.EXTEND);

      return {
        transform: [{ translateY }, { translateX }, { rotateZ: `${rotateZ}deg` }, { scale }],
        zIndex,
        opacity,
      };
    },
    [data.length, directionAnimVal.value],
  );

  return (
    <View>
      <Carousel
        ref={carouselRef}
        loop={false}
        style={styles.container}
        defaultIndex={0}
        vertical={false}
        width={PAGE_WIDTH}
        height={PAGE_HEIGHT}
        data={[...data, undefined]}
        pagingEnabled={true}
        snapEnabled={true}
        onConfigurePanGesture={(g) => {
          g.onChange((e) => {
            'worklet';
            directionAnimVal.value = Math.sign(e.translationX);
          });
        }}
        // fixedDirection="negative"
        renderItem={({ index, item }) => (item ? <CarouselTinderItem key={index} img={item.image.uri} /> : <></>)}
        customAnimation={animationStyle}
        windowSize={5}
        onProgressChange={progress}
      />
    </View>
  );
};

const PAGE_WIDTH = Layout.window.width;
const PAGE_HEIGHT = Layout.window.height;

const styles = StyleSheet.create({
  container: {
    width: PAGE_WIDTH,
    height: PAGE_HEIGHT,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

remove fixedDirection

Removing fixedDirection does not allow swiping to the right.

https://github.com/user-attachments/assets/3d3134d9-5b1f-4405-a241-46dc2148bf13

fixedDirection="negative"

If fixedDirection=“negative”, you can swipe to the right, but it does not move to the next card.

https://github.com/user-attachments/assets/81d3a21b-4980-4d4b-b607-2f50f7e66578

nakapon9517 avatar Feb 28 '25 13:02 nakapon9517

After giving this a lot of my time, my conclusion was that carousel-like component does not fit to the tinder like style for the main reason that tinder requires cards "stack" as opposed to carousel.

For example if you would move the card on the top very far outside the screen the second card will start moving as well under the same gesture.

My solution was eventually to implement my own cards stack with react animation style. actually it was not a lot of work.

I've used the mix of the following tutorials:

https://www.youtube.com/watch?v=m3KqkvqfI48&ab_channel=WilliamCandillon https://github.com/Rakha112/react-native-animation/tree/main/season1/src/29-React-Native-Card-Swipe

jony89 avatar Mar 02 '25 06:03 jony89

I have the same issue as @nakapon9517.

ruk91 avatar Mar 11 '25 08:03 ruk91

same issue +1 @dohooo

zeeshan-shabbir avatar Mar 20 '25 02:03 zeeshan-shabbir

+1

also running into weird opacity / zindex issues when i just run the example out of the box

bryanhpchiang avatar Mar 31 '25 01:03 bryanhpchiang

can anybody provide a working solution ? @dohooo

pascalito007 avatar Apr 10 '25 14:04 pascalito007

temporary solution downgrade the version to 4.0.0-canary.18

omarabualhija avatar Apr 20 '25 10:04 omarabualhija

Try my code I use AI to help me create this it not perfect for look like stack card, you can change line const translateY = index * 0 from 0 to other like 10 for make look like stack (this is tricky because animation seem little flash that why I use 0 for smooth)

this is seem ok for me I try 100 card it working well

Image

import React, { useState } from 'react'
import { View, StyleSheet, Dimensions, Image, Text } from 'react-native'
import { GestureHandlerRootView, PanGestureHandler } from 'react-native-gesture-handler'
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  useAnimatedGestureHandler,
  withSpring,
  interpolate,
  Extrapolate,
  runOnJS,
} from 'react-native-reanimated'

// Sample data - replace with your actual data source if needed
const data: { id: number, uri: string }[] = Array(1000) // Increased to 1000
  .fill({})
  .map((_, index) => ({
    id: index,
    uri: `https://picsum.photos/id/${index + 1}/400/400`, // Using picsum.photos for example
  }))

const { width: screenWidth, height: screenHeight } = Dimensions.get('window')
const cardWidth = screenWidth * 0.9
const cardHeight = screenHeight * 0.6
const swipeThreshold = screenWidth * 0.4 // Distance to trigger a swipe
const swipeVelocityThreshold = 500 // Velocity threshold to trigger a swipe

const TinderStack = () => {
  const [currentIndex, setCurrentIndex] = useState(0)
  const [cardStack, setCardStack] = useState(() => data.slice(0, 3).reverse()) // Start with first 3 cards, reversed for stacking
  const [isAnimatingOut, setIsAnimatingOut] = useState(false) // <-- เพิ่ม state นี้

  const translateX = useSharedValue(0)
  const translateY = useSharedValue(0)
  const rotate = useSharedValue(0)

  // Function to update state on the JS thread after animation
  const handleSwipeComplete = (swipedRight: boolean) => {
    setTimeout(() => {
      console.log(`Swiped ${swipedRight ? 'Right' : 'Left'} on card ${cardStack[cardStack.length - 1]?.id}`)

      // Reset animations for the new top card *immediately*
      // This runs on the JS thread via runOnJS, but before state updates
      translateX.value = 0
      translateY.value = 0
      rotate.value = 0

      // Update current index and card stack
      setCurrentIndex((prevIndex) => {
        const nextIndex = prevIndex + 1
        setCardStack((prevStack) => {
          const newStack = prevStack.slice(0, -1) // Remove the swiped card
          // Simplified check and calculation for the next card index
          const nextCardIndexInData = nextIndex + newStack.length
          if (nextCardIndexInData < data.length) { // Check if there's a next card in the original data
            newStack.unshift(data[nextCardIndexInData]) // Add the next card to the beginning of the stack array (bottom visually)
          }
          return newStack
        })
        return nextIndex
      })

      // Re-enable gestures AFTER state update and reset are complete
      setIsAnimatingOut(false) // <-- This still happens last
    }, 250)
  }


  const gestureHandler = useAnimatedGestureHandler({
    onStart: (_, ctx: any) => {
      ctx.startX = translateX.value
      ctx.startY = translateY.value
    },
    onActive: (event, ctx: any) => {
      translateX.value = ctx.startX + event.translationX
      translateY.value = ctx.startY + event.translationY
      // Calculate rotation based on horizontal movement
      rotate.value = interpolate(
        translateX.value,
        [-screenWidth / 2, screenWidth / 2],
        [-15, 15], // Rotate up to 15 degrees
        Extrapolate.CLAMP
      )
    },
    onEnd: (event) => {
      const currentX = translateX.value
      const velocityX = event.velocityX

      const shouldSwipe =
        Math.abs(currentX) > swipeThreshold ||
        Math.abs(velocityX) > swipeVelocityThreshold

      if (shouldSwipe) {
        const swipeDirection = Math.abs(currentX) > swipeThreshold
          ? Math.sign(currentX)
          : Math.sign(velocityX)

        // Disable gestures immediately
        runOnJS(setIsAnimatingOut)(true)

        // Start the animations *without* the callback on translateX
        translateX.value = withSpring(
          swipeDirection * screenWidth * 1.5,
          { damping: 100, stiffness: 100 }
          // Remove the callback from here
        )
        rotate.value = withSpring(
          swipeDirection * 45,
          { damping: 100, stiffness: 100 }
        )

        // Run the state update immediately after starting the animation
        runOnJS(handleSwipeComplete)(swipeDirection > 0)

      } else {
        // Return to center
        translateX.value = withSpring(0)
        translateY.value = withSpring(0)
        rotate.value = withSpring(0)
      }
    },
  })

  const animatedCardStyle = useAnimatedStyle(() => {
    return {
      transform: [
        { translateX: translateX.value },
        { translateY: translateY.value },
        { rotateZ: `${rotate.value}deg` },
      ],
    }
  })

  // Style for cards underneath the top card
  const getCardStyle = (index: number) => {
    // const scale = 1 - index * 0.05 // Remove scaling
    const translateY = index * 0 // Slightly lower position for stacking effect
    return {
      transform: [{ translateY }], // Only apply vertical translation
      // opacity: 1 - index * 0.1, // Remove opacity change
    }
  }


  if (!cardStack.length) {
    return (
      <View style={styles.container}>
        <Text>No more cards!</Text>
      </View>
    )
  }

  return (
    // Important: Wrap your app or screen in GestureHandlerRootView
    <GestureHandlerRootView style={{ flex: 1 }}>
      <View style={styles.container}>
        {cardStack.map((item, index) => {
          const isTopCard = index === cardStack.length - 1
          const cardStyle = isTopCard ? animatedCardStyle : getCardStyle(cardStack.length - 1 - index) // Apply base style to non-top cards

          if (isTopCard) {
            return (
              <PanGestureHandler
                key={item.id}
                onGestureEvent={gestureHandler}
                enabled={!isAnimatingOut} // <-- เพิ่ม prop enabled ที่นี่
              >
                <Animated.View style={[styles.card, styles.animatedCard, cardStyle]}>
                  <Image source={{ uri: item.uri }} style={styles.image} />
                </Animated.View>
              </PanGestureHandler>
            )
          } else {
            // Render non-interactive cards underneath
            return (
              <Animated.View key={item.id} style={[styles.card, cardStyle]}>
                <Image source={{ uri: item.uri }} style={styles.image} />
              </Animated.View>
            )
          }
        })}
      </View>
    </GestureHandlerRootView>
  )
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f0f0f0', // Background color for the screen
  },
  card: {
    width: cardWidth,
    height: cardHeight,
    borderRadius: 15,
    backgroundColor: 'white',
    position: 'absolute', // Important for stacking
    overflow: 'hidden', // Ensure image stays within bounds
    // Shadow for iOS
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.25,
    shadowRadius: 3.84,
    // Elevation for Android
    elevation: 5,
  },
  animatedCard: {
    // Top card specific styles if needed, zIndex might be useful if issues arise
    zIndex: 100, // Ensure the draggable card is on top
  },
  image: {
    width: '100%',
    height: '100%',
    borderRadius: 15, // Match card's border radius
  },
})

export default TinderStack

zoom2009 avatar Apr 22 '25 18:04 zoom2009

Hi, @jony89. I'm Dosu, and I'm helping the react-native-reanimated-carousel team manage their backlog. I'm marking this issue as stale.

Issue Summary:

  • The issue involves a problem with the Tinder example where users are unable to swipe right.
  • Suggestions to modify properties like fixedDirection, pagingEnabled, and snapEnabled did not resolve the issue.
  • Multiple users, including zoom2009 and nakapon9517, reported similar problems.
  • Dohooo suggested trying the latest version (v4), but issues persisted.
  • You concluded that the component might not be suitable for a Tinder-like card stack and implemented a custom solution.

Next Steps:

  • Please let me know if this issue is still relevant to the latest version of the react-native-reanimated-carousel repository. If so, you can keep the discussion open by commenting on the issue.
  • Otherwise, this issue will be automatically closed in 7 days.

Thank you for your understanding and contribution!

dosubot[bot] avatar Jul 22 '25 16:07 dosubot[bot]