Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gesture swipe on images with desktop? #685

Open
shawnmclean opened this issue Sep 29, 2024 · 1 comment
Open

Gesture swipe on images with desktop? #685

shawnmclean opened this issue Sep 29, 2024 · 1 comment
Assignees
Labels
bug Something isn't working

Comments

@shawnmclean
Copy link

I'm looking at your examples, especially the normal one here: https://reanimated-carousel.dev/Examples/normal

Repro:

When you do a long press on the item and it changes to an image, how can we get it to swipe left/right when using the mouse on an image?

It works on mobile but desktop, it tries to drag the image itself.

@shawnmclean shawnmclean added the bug Something isn't working label Sep 29, 2024
Copy link

dosubot bot commented Sep 29, 2024

To enable swipe left/right functionality with the mouse on an image using react-native-reanimated-carousel on desktop, without dragging the image itself, you can use the onConfigurePanGesture prop to disable the default pan gesture and then implement a custom gesture handler for mouse events. Here is how you can modify the provided code:

  1. Disable the default pan gesture by setting enabled(false) in onConfigurePanGesture.
  2. Add a custom gesture handler using react-native-gesture-handler to detect mouse swipes.

Here is the modified code:

import * as React from "react";
import type { ICarouselInstance } from "react-native-reanimated-carousel";
import Carousel from "react-native-reanimated-carousel";
import { SafeAreaView } from "react-native-safe-area-context";

import { SBItem } from "../../components/SBItem";
import { window } from "../../constants";
import { Button, Image, ImageSourcePropType, ViewStyle, useWindowDimensions } from "react-native";
import Animated, { Easing, Extrapolate, FadeIn, interpolate, runOnJS, useAnimatedReaction, useAnimatedStyle, useSharedValue, withDecay, withSpring, withTiming } from "react-native-reanimated";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import * as Haptics from 'expo-haptics';
import { getImages } from "./images";

const PAGE_WIDTH = window.width;
const data = getImages().slice(0, 68);

function Index() {
  const windowWidth = useWindowDimensions().width;
  const scrollOffsetValue = useSharedValue<number>(0);
  const ref = React.useRef<ICarouselInstance>(null);

  const baseOptions = {
    vertical: false,
    width: windowWidth,
    height: PAGE_WIDTH / 2,
  } as const

  const gesture = React.useMemo(() => Gesture
    .Pan()
    .onUpdate((event) => {
      if (event.translationX > 0) {
        ref.current?.scrollTo({ index: Math.max(0, ref.current?.getCurrentIndex() - 1), animated: true });
      } else {
        ref.current?.scrollTo({ index: Math.min(data.length - 1, ref.current?.getCurrentIndex() + 1), animated: true });
      }
    }),
    [ref]
  );

  return (
    <SafeAreaView
      edges={["bottom"]}
      style={{
        flex: 1,
        backgroundColor: 'black',
        justifyContent: "center",
      }}
    >
      <GestureDetector gesture={gesture}>
        <Carousel
          {...baseOptions}
          loop={false}
          enabled={false} // Disable default pan gesture
          ref={ref}
          defaultScrollOffsetValue={scrollOffsetValue}
          testID={"xxx"}
          style={{ width: "100%" }}
          autoPlay={false}
          autoPlayInterval={1000}
          data={data}
          onConfigurePanGesture={g => g.enabled(false)}
          pagingEnabled
          onSnapToItem={index => console.log("current index:", index)}
          windowSize={2}
          renderItem={({ index, item }) => {
            return <Animated.View
              key={index}
              style={{ flex: 1 }}
            >
              <SBItem
                showIndex={false}
                img={item}
              />
            </Animated.View>
          }}
        />
      </GestureDetector>
      <ThumbnailPagination
        style={{ marginVertical: 9 }}
        onIndexChange={(index) => {
          ref.current?.scrollTo({ index, animated: false })
          Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light)
        }}
      />
    </SafeAreaView>
  );
}

const ThumbnailPagination: React.FC<{
  style?: ViewStyle
  onIndexChange?: (index: number) => void
}> = ({ style, onIndexChange }) => {
  const [_containerWidth, setContainerWidth] = React.useState<number>(0);
  const inactiveWidth = 30;
  const activeWidth = inactiveWidth * 2;
  const itemGap = 5;
  const totalWidth = inactiveWidth * (data.length - 1) + activeWidth + itemGap * (data.length - 1);
  const swipeProgress = useSharedValue<number>(0);
  const activeIndex = useSharedValue<number>(0);

  const containerWidth = React.useMemo(() => {
    if (totalWidth < _containerWidth) {
      return totalWidth
    }

    return _containerWidth
  }, [
    _containerWidth,
    totalWidth,
  ])

  const gesture = React.useMemo(() => Gesture
    .Pan()
    .onUpdate((event) => {
      swipeProgress.value = Math.min(
        Math.max(event.x, 0),
        containerWidth
      )
    }),
    [
      activeWidth,
      inactiveWidth,
      containerWidth,
    ]
  )

  const animStyles = useAnimatedStyle(() => {
    if (containerWidth <= 0) {
      return {}
    }

    const isOverScroll = totalWidth > containerWidth

    if (!isOverScroll) {
      return {
        transform: [
          {
            translateX: 0
          }
        ],
      }
    }

    return {
      transform: [
        {
          translateX: -interpolate(
            swipeProgress.value,
            [0, containerWidth],
            [0, totalWidth - containerWidth],
            Extrapolate.CLAMP
          )
        }
      ],
    }
  }, [
    containerWidth,
    totalWidth,
    containerWidth
  ])

  useAnimatedReaction(
    () => activeIndex.value,
    (activeIndex) => onIndexChange && runOnJS(onIndexChange)(activeIndex),
    [onIndexChange]
  )

  return <GestureDetector gesture={gesture}>
    <Animated.View style={{ width: '100%', overflow: "hidden" }}>
      <Animated.View style={[{ flexDirection: "row" }, style, animStyles]} onLayout={e => setContainerWidth(e.nativeEvent.layout.width)}>
        {
          containerWidth > 0 && data.map((item, index) => {
            return <ThumbnailPaginationItem
              key={index}
              source={item}
              totalItems={data.length}
              swipeProgress={swipeProgress}
              containerWidth={containerWidth}
              activeIndex={activeIndex}
              activeWidth={activeWidth}
              itemGap={itemGap}
              inactiveWidth={inactiveWidth}
              totalWidth={totalWidth}
              index={index}
              style={{ marginRight: itemGap }}
              onSwipe={() => {
                console.log(`${item} swiped`)
              }}
            />
          })
        }
      </Animated.View>
    </Animated.View>
  </GestureDetector >
}

const ThumbnailPaginationItem: React.FC<{
  source: ImageSourcePropType;
  containerWidth: number;
  totalItems: number;
  activeIndex: Animated.SharedValue<number>;
  swipeProgress: Animated.SharedValue<number>;
  activeWidth: number;
  totalWidth: number;
  inactiveWidth: number;
  itemGap: number;
  index: number;
  onSwipe?: () => void;
  style?: ViewStyle
}> = ({
  source,
  containerWidth,
  totalItems,
  swipeProgress,
  index,
  itemGap = 0,
  activeIndex,
  activeWidth,
  totalWidth,
  inactiveWidth,
  style
}) => {
    const isActive = useSharedValue(0);

    useAnimatedReaction(
      () => {
        const onTheRight = index >= activeIndex.value
        const extraWidth = onTheRight ? activeWidth - inactiveWidth : 0
        const inputRange = [
          index * (inactiveWidth + itemGap) + (index === activeIndex.value ? 0 : extraWidth) - 0.1,
          index * (inactiveWidth + itemGap) + (index === activeIndex.value ? 0 : extraWidth),
          (index + 1) * (inactiveWidth + itemGap) + extraWidth,
          (index + 1) * (inactiveWidth + itemGap) + extraWidth + 0.1,
        ]

        return interpolate(
          swipeProgress.value / containerWidth * totalWidth,
          inputRange,
          [
            0,
            1,
            1,
            0
          ],
          Extrapolate.CLAMP
        )

      },
      (_isActiveAnimVal) => {
        isActive.value = _isActiveAnimVal
      },
      [
        containerWidth,
        totalItems,
        index,
        activeIndex,
        activeWidth,
        inactiveWidth,
        itemGap,
      ]
    )

    useAnimatedReaction(
      () => {
        return isActive.value
      },
      (isActiveVal) => {
        if (isActiveVal === 1) {
          activeIndex.value = index
        }
      },
      []
    )

    const animStyles = useAnimatedStyle(() => {
      const widthAnimVal = interpolate(
        isActive.value,
        [
          0, 1, 1, 0
        ],
        [
          inactiveWidth,
          activeWidth,
          activeWidth,
          inactiveWidth
        ],
        Extrapolate.CLAMP
      )

      return {
        width: withTiming(widthAnimVal, { duration: 100, easing: Easing.bounce }),
        height: 30,
        borderRadius: 5,
        overflow: "hidden",
      }
    }, [
      isActive,
      activeWidth,
      inactiveWidth,
    ])

    return <Animated.View style={[animStyles, style]}>
      <Image source={source} style={{ width: '100%', height: "100%" }} />
    </Animated.View>
  }

export default Index;

This code sets up a custom pan gesture to handle mouse swipes and scrolls the carousel accordingly [1][2].

To continue talking to Dosu, mention @dosu.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants