dohooo / react-native-reanimated-carousel

🎠 React Native swiper/carousel component, fully implemented using reanimated v2, support to iOS/Android/Web. (Swiper/Carousel)
https://react-native-reanimated-carousel.vercel.app
MIT License
2.86k stars 329 forks source link

Sliding Carousel will trigger the Press event of the RenderItem. #624

Open pdb3616109 opened 5 months ago

pdb3616109 commented 5 months ago

When using Carousel in combination with expo-router, sliding Carousel (horizontal) will trigger the Press event of the RenderItem.

To Reproduce Steps to reproduce the behavior:

  1. yarn create expo-app --template ==> navigation Router ==> ...
  2. Replace app/(tabs)/index.tsx image

Versions (please complete the following information):

Smartphone (please complete the following information):

Additional context Add any other context about the problem here.

dosubot[bot] commented 5 months ago

Hey there, @pdb3616109! 👋 I'm here to help you with any bugs, questions, or contributions you have while we wait for a human maintainer. Let's tackle this together!

To prevent the Press event of the TouchableOpacity in the renderItem of the Carousel component from being triggered when sliding the carousel horizontally, you can use the onScrollStart and onScrollEnd callbacks to manage a flag that indicates whether a scroll gesture is in progress. You can then conditionally handle the Press event based on this flag.

Here's an example of how you can implement this:

  1. Add a state variable to track whether a scroll gesture is in progress.
  2. Update this state in the onScrollStart and onScrollEnd callbacks.
  3. Conditionally handle the Press event in the renderItem function.

Here's the modified code:

import React, { useState, useCallback } from "react";
import { StyleSheet, TouchableOpacity, Text, View } from "react-native";
import Carousel from "react-native-reanimated-carousel";
import Animated, { useSharedValue, withTiming, interpolate, useAnimatedStyle } from "react-native-reanimated";

const PAGE_WIDTH = 300; // Example page width

const MyCarousel = () => {
  const [isScrolling, setIsScrolling] = useState(false);
  const pressAnim = useSharedValue(0);

  const handlePress = useCallback(() => {
    if (!isScrolling) {
      // Handle the press event
      console.log("Item pressed");
    }
  }, [isScrolling]);

  const renderItem = useCallback(
    ({ item }) => (
      <TouchableOpacity onPress={handlePress}>
        <View style={styles.item}>
          <Text>{item}</Text>
        </View>
      </TouchableOpacity>
    ),
    [handlePress]
  );

  const animationStyle = useCallback(
    (value) => {
      "worklet";
      const zIndex = interpolate(value, [-1, 0, 1], [-1000, 0, 1000]);
      const translateX = interpolate(value, [-1, 0, 1], [-PAGE_WIDTH, 0, PAGE_WIDTH]);
      return {
        transform: [{ translateX }],
        zIndex,
      };
    },
    []
  );

  return (
    <View style={{ flex: 1 }}>
      <Carousel
        loop={true}
        style={{ width: PAGE_WIDTH, height: 240 }}
        width={PAGE_WIDTH}
        data={["Item 1", "Item 2", "Item 3"]}
        onScrollStart={() => {
          pressAnim.value = withTiming(1);
          setIsScrolling(true);
        }}
        onScrollEnd={() => {
          pressAnim.value = withTiming(0);
          setIsScrolling(false);
        }}
        renderItem={renderItem}
        customAnimation={animationStyle}
        scrollAnimationDuration={1200}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  item: {
    width: PAGE_WIDTH,
    height: 240,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#ccc",
  },
});

export default MyCarousel;

In this code, the isScrolling state is used to track whether a scroll gesture is in progress. The handlePress function checks this state before handling the press event. The renderItem function wraps the item content with a TouchableOpacity that uses the handlePress function. This ensures that the Press event is only triggered when the carousel is not being scrolled. The onScrollStart and onScrollEnd callbacks are used to update the isScrolling state [1][2][3].

To continue talking to Dosu, mention @dosu.

pdb3616109 commented 5 months ago

Hey there, @pdb3616109! 👋 I'm here to help you with any bugs, questions, or contributions you have while we wait for a human maintainer. Let's tackle this together!嘿, ! 👋 在等待人类维护人员期间,我会帮助您解决任何错误、问题或贡献。让我们一起解决这个问题吧!

To prevent the Press event of the TouchableOpacity in the renderItem of the Carousel component from being triggered when sliding the carousel horizontally, you can use the onScrollStart and onScrollEnd callbacks to manage a flag that indicates whether a scroll gesture is in progress. You can then conditionally handle the Press event based on this flag.为了防止水平滑动轮播时触发 Carousel 组件的 renderItem 中的 TouchableOpacityPress 事件,可以使用 onScrollStartonScrollEnd 回调来管理指示滚动手势是否正在进行的标志。然后,您可以根据此标志有条件地处理 Press 事件。

Here's an example of how you can implement this:以下是如何实现这一点的示例:

  1. Add a state variable to track whether a scroll gesture is in progress.添加一个状态变量来跟踪滚动手势是否正在进行。
  2. Update this state in the onScrollStart and onScrollEnd callbacks.在 onScrollStartonScrollEnd 回调中更新此状态。
  3. Conditionally handle the Press event in the renderItem function.有条件地处理 renderItem 函数中的 Press 事件。

Here's the modified code:这是修改后的代码:

import React, { useState, useCallback } from "react";
import { StyleSheet, TouchableOpacity, Text, View } from "react-native";
import Carousel from "react-native-reanimated-carousel";
import Animated, { useSharedValue, withTiming, interpolate, useAnimatedStyle } from "react-native-reanimated";

const PAGE_WIDTH = 300; // Example page width

const MyCarousel = () => {
  const [isScrolling, setIsScrolling] = useState(false);
  const pressAnim = useSharedValue(0);

  const handlePress = useCallback(() => {
    if (!isScrolling) {
      // Handle the press event
      console.log("Item pressed");
    }
  }, [isScrolling]);

  const renderItem = useCallback(
    ({ item }) => (
      <TouchableOpacity onPress={handlePress}>
        <View style={styles.item}>
          <Text>{item}</Text>
        </View>
      </TouchableOpacity>
    ),
    [handlePress]
  );

  const animationStyle = useCallback(
    (value) => {
      "worklet";
      const zIndex = interpolate(value, [-1, 0, 1], [-1000, 0, 1000]);
      const translateX = interpolate(value, [-1, 0, 1], [-PAGE_WIDTH, 0, PAGE_WIDTH]);
      return {
        transform: [{ translateX }],
        zIndex,
      };
    },
    []
  );

  return (
    <View style={{ flex: 1 }}>
      <Carousel
        loop={true}
        style={{ width: PAGE_WIDTH, height: 240 }}
        width={PAGE_WIDTH}
        data={["Item 1", "Item 2", "Item 3"]}
        onScrollStart={() => {
          pressAnim.value = withTiming(1);
          setIsScrolling(true);
        }}
        onScrollEnd={() => {
          pressAnim.value = withTiming(0);
          setIsScrolling(false);
        }}
        renderItem={renderItem}
        customAnimation={animationStyle}
        scrollAnimationDuration={1200}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  item: {
    width: PAGE_WIDTH,
    height: 240,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#ccc",
  },
});

export default MyCarousel;

In this code, the isScrolling state is used to track whether a scroll gesture is in progress. The handlePress function checks this state before handling the press event. The renderItem function wraps the item content with a TouchableOpacity that uses the handlePress function. This ensures that the Press event is only triggered when the carousel is not being scrolled. The onScrollStart and onScrollEnd callbacks are used to update the isScrolling state [1][2][3].在此代码中, isScrolling 状态用于跟踪滚动手势是否正在进行。 handlePress 函数在处理按下事件之前检查此状态。 renderItem 函数通过使用 handlePress 函数的 TouchableOpacity 包装项目内容。这确保了 Press 事件仅在轮播未滚动时触发。 onScrollStartonScrollEnd 回调用于更新 isScrolling 状态 [1] [2] [3] .

To continue talking to Dosu, mention @dosu.要继续与 Dosu 交谈,请提及 。

not work

dosubot[bot] commented 5 months ago

To prevent the Press event of the TouchableOpacity in the renderItem from being triggered when sliding the Carousel component horizontally, you can use the onScrollStart and onScrollEnd events provided by the react-native-reanimated-carousel component to manage the swipe state effectively.

Here's an example of how you can achieve this:

  1. Track the scroll start and end events to determine if a swipe gesture is happening.
  2. Use a flag to disable the Press event if a swipe gesture is detected.
import React, { useState, useCallback } from 'react';
import { TouchableOpacity, View, Text } from 'react-native';
import Carousel from 'react-native-reanimated-carousel';

const MyCarousel = () => {
  const [isSwiping, setIsSwiping] = useState(false);

  const handleScrollStart = useCallback(() => {
    setIsSwiping(true);
  }, []);

  const handleScrollEnd = useCallback(() => {
    setIsSwiping(false);
  }, []);

  const renderItem = ({ item }) => (
    <TouchableOpacity
      onPress={() => {
        if (!isSwiping) {
          console.log('Item pressed:', item);
        }
      }}
    >
      <View>
        <Text>{item}</Text>
      </View>
    </TouchableOpacity>
  );

  return (
    <Carousel
      data={['Item 1', 'Item 2', 'Item 3']}
      renderItem={renderItem}
      onScrollStart={handleScrollStart}
      onScrollEnd={handleScrollEnd}
    />
  );
};

export default MyCarousel;

In this example:

This approach leverages the onScrollStart and onScrollEnd events provided by the react-native-reanimated-carousel component to manage the swipe state seamlessly [1][2][3].

To continue talking to Dosu, mention @dosu.

devi-prsd commented 5 months ago

@dosu This code is not yet released, and handleScrollStart turns true on press not scroll

kruzikruz commented 4 months ago

@dosu

I did it this way, but I still have a problem. When I scroll, it takes me to /news[id], which makes no sense :( Can anyone advise?

import axios from 'axios'; import React, { useState, useCallback, useEffect } from "react"; import { View, StyleSheet, Dimensions, Image, ImageSourcePropType, Text, TouchableOpacity } from "react-native"; import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel"; import { SafeAreaView } from "react-native-safe-area-context"; import Animated, { useSharedValue, withTiming, interpolate, useAnimatedStyle } from "react-native-reanimated"; import { LinearGradient } from 'expo-linear-gradient'; import { useRouter } from "expo-router";

const { width: screenWidth, height: screenHeight } = Dimensions.get('window'); const carouselHeight = screenHeight * 0.6;

type SliderItem = { image: ImageSourcePropType; title: string; description: string; id: number; };

// Initial empty state for images and texts const initialSliderState: SliderItem[] = [ { image: require('../assets/images/amp.jpg'), title: 'Loading...', description: '', id: 0 }, { image: require('../assets/images/amp.jpg'), title: 'Loading...', description: '', id: 1 }, { image: require('../assets/images/amp.jpg'), title: 'Loading...', description: '', id: 2 }, { image: require('../assets/images/amp.jpg'), title: 'Loading...', description: '', id: 3 }, ];

const CarouselStandard = () => { const [sliderData, setSliderData] = useState<SliderItem[]>(initialSliderState); const progress = useSharedValue(0); const ref = React.useRef(null); const [isScrolling, setIsScrolling] = useState(false); const pressAnim = useSharedValue(0); const router = useRouter();

const handlePress = (id: number) => { const url = /news/${id}; console.log(Navigating to URL: ${url}); router.push(url); };

useEffect(() => { const fetchData = async () => { try { const config = { method: 'get', maxBodyLength: Infinity, url: 'xxx', headers: { 'Authorization': 'Bearer xxx' } };

    const response = await axios.request(config);

    const posts = response.data.data;
    const updatedSliderData: SliderItem[] = posts.map((post: any) => ({
      title: post.attributes.categories.data[0].attributes.Name,
      description: post.attributes.Title,
      image: { uri: `https://strapi.fobos-one.com.pl${post.attributes.Cover_picture.data.attributes.url}` },
      id: post.id // Ensure the ID is included in the slider data
    }));

    setSliderData(updatedSliderData);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
};

fetchData();

}, []);

const renderItem = ({ item, index }: { item: SliderItem, index: number }) => { return ( <TouchableOpacity key={index} style={styles.item} onPress={() => handlePress(item.id)} activeOpacity={1}

<LinearGradient colors={['transparent', 'rgba(0, 0, 0, 0.5)', 'rgba(0, 0, 0, 0.6)', 'rgba(0, 0, 0, 0.8)']} style={styles.gradient} />

{item.title} {item.description}

); };

return ( <SafeAreaView edges={["bottom"]} style={{ flex: 1 }}> <Carousel data={sliderData} renderItem={renderItem} width={screenWidth} height={carouselHeight} autoPlay={false} loop={true} onScrollStart={() => { pressAnim.value = withTiming(1); setIsScrolling(true); }} onScrollEnd={() => { pressAnim.value = withTiming(0); setIsScrolling(false); }} onProgressChange={(offsetProgress, absoluteProgress) => { progress.value = absoluteProgress; const currentIndex = Math.round(absoluteProgress) % sliderData.length; }} panGestureHandlerProps={{ activeOffsetX: [-10, 10], }} ref={ref} />

{sliderData.map((item, index) => { const animatedStyle = useAnimatedStyle(() => { const opacity = interpolate( progress.value, [index - 1, index, index + 1], [0.5, 1, 0.5], 'clamp' ); return { opacity, }; }); return ( ref.current?.scrollTo({ index, animated: true })}> ); })}
</SafeAreaView>

); };

const styles = StyleSheet.create({ container: { alignItems: 'center', }, item: { justifyContent: 'center', alignItems: 'center', padding: 10, }, image: { width: '100%', height: '100%', resizeMode: 'cover', borderRadius: 15, }, gradient: { position: 'absolute', left: 10, right: 10, bottom: 10, height: '40%', borderBottomLeftRadius: 15, borderBottomRightRadius: 15, padding: 10, }, textContainer: { position: 'absolute', bottom: 15, left: 15, right: 15, padding: 10, }, headerText: { color: 'white', fontSize: 16, marginBottom: 5, }, descriptionText: { color: 'white', fontSize: 20, fontWeight: 'bold', }, paginationContainer: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', marginTop: 10, }, dot: { width: 50, height: 3, backgroundColor: 'white', margin: 5, }, });

export default CarouselStandard;

MuNitCat commented 3 months ago

一个比较简单的处理方法 导入 import {TouchableOpacity} from 'react-native-gesture-handler'; 将renderItem中的事件组件替换一下 但是需要主要 添加 width: '100%', height: '100%', 如果是 flex:1 可能会不显示出来

      <TouchableOpacity
        activeOpacity={0.5}
        onPress={() => {
          handleCarouse(item);
        }}
        style={styles.carouselItem}>
        <Image
          source={{uri: item.cover}}
          style={styles.image}
          resizeMode="stretch"
        />
      </TouchableOpacity>
pdb3616109 commented 3 months ago

一个比较简单的处理方法 导入 import {TouchableOpacity} from 'react-native-gesture-handler'; 将renderItem中的事件组件替换一下 但是需要主要 添加 width: '100%', height: '100%', 如果是 flex:1 可能会不显示出来

      <TouchableOpacity
        activeOpacity={0.5}
        onPress={() => {
          handleCarouse(item);
        }}
        style={styles.carouselItem}>
        <Image
          source={{uri: item.cover}}
          style={styles.image}
          resizeMode="stretch"
        />
      </TouchableOpacity>

谢谢,我有空去试试

jvgeee commented 3 months ago

Still facing this issue on the latest release version. Upgrading to 4.0.0-alpha.12 (and using setState for:

<Carousel
 ...{...rest}
 onScrollStart={() => {
    setAllowRouteChange(false)
}}
onScrollEnd={() => setAllowRouteChange(true)}

and then in my renderItem:

const handlePress = React.useCallback(() => {
    if (!allowRouteChange) return
    console.log("Do something here!)
  }, [allowRouteChange])

fixes it for me

sahilparikh98 commented 3 months ago

Using TouchableOpacity from react-native-gesture-handler instead of from react-native on my items fixed it for me

https://docs.swmansion.com/react-native-gesture-handler/docs/components/touchables/

import { TouchableOpacity } from 'react-native-gesture-handler';
tahaipek commented 1 month ago

Using TouchableOpacity from react-native-gesture-handler instead of from react-native on my items fixed it for me

https://docs.swmansion.com/react-native-gesture-handler/docs/components/touchables/

import { TouchableOpacity } from 'react-native-gesture-handler';

I was experiencing this on iOS, and I can confirm that it was solved when I tried this method on the iOS emulator.

dosubot[bot] commented 1 month ago

To prevent the handlePress function from being triggered on scroll in the Carousel component, you need to ensure that the touch events are properly distinguished between a scroll and a press. This can be achieved by using a combination of touch event handlers and a threshold to determine if a touch event should be considered a scroll or a press.

Here is an example of how you can modify your CarouselStandard component to include this logic:

  1. Add state to track the touch start position.
  2. Add a threshold to determine if the touch movement is significant enough to be considered a scroll.
  3. Only trigger the handlePress function if the touch movement is below the threshold.

Here's the modified code:

import axios from 'axios';
import React, { useState, useCallback, useEffect } from "react";
import { View, StyleSheet, Dimensions, Image, ImageSourcePropType, Text, TouchableOpacity, GestureResponderEvent } from "react-native";
import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel";
import { SafeAreaView } from "react-native-safe-area-context";
import Animated, { useSharedValue, withTiming, interpolate, useAnimatedStyle } from "react-native-reanimated";
import { LinearGradient } from 'expo-linear-gradient';
import { useRouter } from "expo-router";

const { width: screenWidth, height: screenHeight } = Dimensions.get('window');
const carouselHeight = screenHeight * 0.6;

type SliderItem = {
  image: ImageSourcePropType;
  title: string;
  description: string;
  id: number;
};

const initialSliderState: SliderItem[] = [
  { image: require('../assets/images/amp.jpg'), title: 'Loading...', description: '', id: 0 },
  { image: require('../assets/images/amp.jpg'), title: 'Loading...', description: '', id: 1 },
  { image: require('../assets/images/amp.jpg'), title: 'Loading...', description: '', id: 2 },
  { image: require('../assets/images/amp.jpg'), title: 'Loading...', description: '', id: 3 },
];

const CarouselStandard = () => {
  const [sliderData, setSliderData] = useState<SliderItem[]>(initialSliderState);
  const progress = useSharedValue(0);
  const ref = React.useRef<ICarouselInstance>(null);
  const [isScrolling, setIsScrolling] = useState(false);
  const pressAnim = useSharedValue(0);
  const router = useRouter();
  const [touchStart, setTouchStart] = useState({ x: 0, y: 0 });
  const SCROLL_THRESHOLD = 10;

  const handlePress = (id: number) => {
    const url = `/news/${id}`;
    console.log(`Navigating to URL: ${url}`);
    router.push(url);
  };

  useEffect(() => {
    const fetchData = async () => {
      try {
        const config = {
          method: 'get',
          maxBodyLength: Infinity,
          url: 'xxx',
          headers: {
            'Authorization': 'Bearer xxx'
          }
        };

        const response = await axios.request(config);

        const posts = response.data.data;
        const updatedSliderData: SliderItem[] = posts.map((post: any) => ({
          title: post.attributes.categories.data[0].attributes.Name,
          description: post.attributes.Title,
          image: { uri: `https://strapi.fobos-one.com.pl${post.attributes.Cover_picture.data.attributes.url}` },
          id: post.id
        }));

        setSliderData(updatedSliderData);
      } catch (error) {
        console.error('Error fetching data:', error);
      }
    };

    fetchData();
  }, []);

  const handleTouchStart = (event: GestureResponderEvent) => {
    const { pageX, pageY } = event.nativeEvent;
    setTouchStart({ x: pageX, y: pageY });
    setIsScrolling(true);
  };

  const handleTouchEnd = (event: GestureResponderEvent, id: number) => {
    const { pageX, pageY } = event.nativeEvent;
    const dx = Math.abs(pageX - touchStart.x);
    const dy = Math.abs(pageY - touchStart.y);

    if (dx < SCROLL_THRESHOLD && dy < SCROLL_THRESHOLD) {
      handlePress(id);
    }

    setIsScrolling(false);
  };

  const renderItem = ({ item, index }: { item: SliderItem, index: number }) => {
    return (
      <TouchableOpacity 
        key={index} 
        style={styles.item}
        onPress={(event) => handleTouchEnd(event, item.id)}
        activeOpacity={1} 
        onTouchStart={handleTouchStart}
      >
        <Image source={item.image} style={styles.image} />
        <LinearGradient
          colors={['transparent', 'rgba(0, 0, 0, 0.5)', 'rgba(0, 0, 0, 0.6)', 'rgba(0, 0, 0, 0.8)']}
          style={styles.gradient}
        />
        <View style={styles.textContainer}>
          <Text style={styles.headerText}>{item.title}</Text>
          <Text style={styles.descriptionText}>{item.description}</Text>
        </View>
      </TouchableOpacity>
    );
  };

  return (
    <SafeAreaView edges={["bottom"]} style={{ flex: 1 }}>
      <Carousel
        data={sliderData}
        renderItem={renderItem}
        width={screenWidth}
        height={carouselHeight}
        autoPlay={false}
        loop={true}
        onScrollStart={() => {
          pressAnim.value = withTiming(1);
          setIsScrolling(true);
        }}
        onScrollEnd={() => {
          pressAnim.value = withTiming(0);
          setIsScrolling(false);
        }}
        onProgressChange={(offsetProgress, absoluteProgress) => {
          progress.value = absoluteProgress;
          const currentIndex = Math.round(absoluteProgress) % sliderData.length;
        }}
        panGestureHandlerProps={{
          activeOffsetX: [-10, 10],
        }}
        ref={ref}
      />
      <View style={styles.paginationContainer}>
        {sliderData.map((item, index) => {
          const animatedStyle = useAnimatedStyle(() => {
            const opacity = interpolate(
              progress.value,
              [index - 1, index, index + 1],
              [0.5, 1, 0.5],
              'clamp'
            );

            return {
              opacity,
            };
          });

          return (
            <TouchableOpacity key={index} onPress={() => ref.current?.scrollTo({ index, animated: true })}>
              <Animated.View style={[styles.dot, animatedStyle]} />
            </TouchableOpacity>
          );
        })}
      </View>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    alignItems: 'center',
  },
  item: {
    justifyContent: 'center',
    alignItems: 'center',
    padding: 10,
  },
  image: {
    width: '100%',
    height: '100%',
    resizeMode: 'cover',
    borderRadius: 15,
  },
  gradient: {
    position: 'absolute',
    left: 10,
    right: 10,
    bottom: 10,
    height: '40%',
    borderBottomLeftRadius: 15,
    borderBottomRightRadius: 15,
    padding: 10,
  },
  textContainer: {
    position: 'absolute',
    bottom: 15,
    left: 15,
    right: 15,
    padding: 10,
  },
  headerText: {
    color: 'white',
    fontSize: 16,
    marginBottom: 5,
  },
  descriptionText: {
    color: 'white',
    fontSize: 20,
    fontWeight: 'bold',
  },
  paginationContainer: {
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    marginTop: 10,
  },
  dot: {
    width: 50,
    height: 3,
    backgroundColor: 'white',
    margin: 5,
  },
});

export default CarouselStandard;

This code adds touch event handlers to track the start position of the touch and determine if the touch movement is significant enough to be considered a scroll. If the movement is below the threshold, it is considered a press, and the handlePress function is triggered accordingly [1].

To continue talking to Dosu, mention @dosu.