meliorence/react-native-snap-carousel

Swipes do not register in top half of portrait-oriented Carousel Card ('stack', and 'tinder')

jordangrant opened this issue ยท 19 comments

@bd-arc,

Didn't follow the Template this time around due to time constraints, but I want to put this potential issue on your radar. It's a really weird one..

To reproduce:

  • I set up a Carousel with tall (portrait-oriented) cards.
  • I found that once the card exceeded a rectangular shape, the hitbox for swipes diminished.
  • Did not do much else by way of customization. Occurs with layouts 'stack', and 'tinder'

Works:
screen shot 2018-02-08 at 12 29 05 pm

Seeing Issue:
screen shot 2018-02-08 at 12 29 54 pm

Thanks for taking a look.

Reproduced on iOS device / simulator. Tested in Dev mode.

Hi @jordangrant,

Damn! I've encountered this issue before, but I was secretly hoping that this was a problem with something very specific in the provided example... Apparently not.

So far, this behavior seems linked to the carousel position in the viewport rather than having to do with the height of the item. It looks like approximately the top quarter of the screen is deaf to swipe events on the carousel. You can take a look at this screencast to see what I mean: https://giphy.com/gifs/xThtasOLFCD4A6pW6c

Note that I didn't have any issue on Android.

To be honest, I don't have the slightest clue so far about what's at stake :( I'm going to run a few tests based on the iOS/Android difference and see if I can get a better understanding of the issue.

Any idea on your end?

If I remove the following style rule, it works properly:

zIndex: carouselProps.data.length - index

But then, of course, the layout is completely messed up:
react-native-snap-carousel issue 262

A quick fix would be to use props scrollInterpolator and slideInterpolatedStyle to pass the Android 'stack' custom interpolation. The effect is going to be inverted, but at least there will be no need to specify a zIndex.


Here is the relevant code in case you want a quick fix for the 'stack' effect:

import Carousel, { getInputRangeFromIndexes } from 'react-native-snap-carousel';

function stackScrollInterpolator (index, carouselProps) {
    const range = [1, 0, -1, -2, -3];
    const inputRange = getInputRangeFromIndexes(range, index, carouselProps);
    const outputRange = range;
    return { inputRange, outputRange };
}

function stackAnimatedStyles (index, animatedValue, carouselProps) {
    const sizeRef = carouselProps.vertical ? carouselProps.itemHeight : carouselProps.itemWidth;
    const translateProp = carouselProps.vertical ? 'translateY' : 'translateX';

    const cardOffset = 18;
    const card1Scale = 0.9;
    const card2Scale = 0.8;

    const getTranslateFromScale = (index, scale) => {
        const centerFactor = 1 / scale * index;
        const centeredPosition = -Math.round(sizeRef * centerFactor);
        const edgeAlignment = Math.round((sizeRef - (sizeRef * scale)) / 2);
        const offset = Math.round(cardOffset * Math.abs(index) / scale);

        return centeredPosition - edgeAlignment - offset;
    };

    return {
        opacity: animatedValue.interpolate({
            inputRange: [-3, -2, -1, 0],
            outputRange: [0, 0.5, 0.75, 1],
            extrapolate: 'clamp'
        }),
        transform: [{
            scale: animatedValue.interpolate({
                inputRange: [-2, -1, 0, 1],
                outputRange: [card2Scale, card1Scale, 1, card1Scale],
                extrapolate: 'clamp'
            })
        }, {
            [translateProp]: animatedValue.interpolate({
                inputRange: [-3, -2, -1, 0, 1],
                outputRange: [
                    getTranslateFromScale(-3, card2Scale),
                    getTranslateFromScale(-2, card2Scale),
                    getTranslateFromScale(-1, card1Scale),
                    0,
                    sizeRef * 0.5
                ],
                extrapolate: 'clamp'
            })
        }]
    };
}

const myCarousel = (
    <Carousel
      scrollInterpolator={stackScrollInterpolator}
      slideInterpolatedStyle={stackAnimatedStyles}
      useScrollView={true} // <--- Use this for a better effect or disable it to get performance optimizations
    />
);

=> Note that you're going to get the Android effect on both platforms (inverted compared to the original iOS one).

react-native-snap-carousel stack layout android


Regarding the issue, I am left clueless. I don't understand why:

  • the zIndex rule randomly messes with the swipe events (sometimes you can swipe the 2-3 first items and then you need to swipe from a lower portion of the screen)
  • only the top part of the screen is affected
  • the default effect doesn't have any swipe issue even if we add the zIndex rule.

=> Any insight will be greatly appreciated!

@bd-arc Thanks for the quick fix! No idea why the issue occurs, but this definitely helps me for the time being.

Hello @bd-arc
How I implement the same fix but for the "Tinder" style?

Hi @donnes,

Here is the code in case you're facing the issue and need to fix the "Tinder" layout. I've just adapted the code you can find in /src/utils/animations.js.

import { Platform } from 'react-native';
import Carousel, { getInputRangeFromIndexes } from 'react-native-snap-carousel';

function stackScrollInterpolator (index, carouselProps) {
    const range = [1, 0, -1, -2, -3];
    const inputRange = getInputRangeFromIndexes(range, index, carouselProps);
    const outputRange = range;
    return { inputRange, outputRange };
}

function stackAnimatedStyles (index, animatedValue, carouselProps) {
    const sizeRef = carouselProps.vertical ? carouselProps.itemHeight : carouselProps.itemWidth;
    const mainTranslateProp = carouselProps.vertical ? 'translateY' : 'translateX';
    const secondaryTranslateProp = carouselProps.vertical ? 'translateX' : 'translateY';

    const cardOffset = 9;
    const card1Scale = 0.96;
    const card2Scale = 0.92;
    const card3Scale = 0.88;
    const peekingCardsOpacity = Platform.OS === 'android' ? 0.92 : 1;

    const getMainTranslateFromScale = (cardIndex, scale) => {
        const centerFactor = 1 / scale * cardIndex;
        return -Math.round(sizeRef * centerFactor);
    };

    const getSecondaryTranslateFromScale = (cardIndex, scale) => {
        return Math.round(cardOffset * Math.abs(cardIndex) / scale);
    };

    return {
        opacity: animatedValue.interpolate({
            inputRange: [-3, -2, -1, 0, 1],
            outputRange: [0, peekingCardsOpacity, peekingCardsOpacity, 1, 0],
            extrapolate: 'clamp'
        }),
        transform: [{
            scale: animatedValue.interpolate({
                inputRange: [-3, -2, -1, 0],
                outputRange: [card3Scale, card2Scale, card1Scale, 1],
                extrapolate: 'clamp'
            })
        }, {
            rotate: animatedValue.interpolate({
                inputRange: [0, 1],
                outputRange: ['0deg', '22deg'],
                extrapolate: 'clamp'
            })
        }, {
            [mainTranslateProp]: animatedValue.interpolate({
                inputRange: [-3, -2, -1, 0, 1],
                outputRange: [
                    getMainTranslateFromScale(-3, card3Scale),
                    getMainTranslateFromScale(-2, card2Scale),
                    getMainTranslateFromScale(-1, card1Scale),
                    0,
                    sizeRef * 1.1
                ],
                extrapolate: 'clamp'
            })
        }, {
            [secondaryTranslateProp]: animatedValue.interpolate({
                inputRange: [-3, -2, -1, 0],
                outputRange: [
                    getSecondaryTranslateFromScale(-3, card3Scale),
                    getSecondaryTranslateFromScale(-2, card2Scale),
                    getSecondaryTranslateFromScale(-1, card1Scale),
                    0
                ],
                extrapolate: 'clamp'
            })
        }]
    };
}

const myCarousel = (
    <Carousel
      scrollInterpolator={stackScrollInterpolator}
      slideInterpolatedStyle={stackAnimatedStyles}
      useScrollView={true} // <--- Use this for a better effect or disable it to get performance optimizations
    />
);

=> Note that, as for the stack layout, you'll get the Android effect on both platforms (inverted compared to the original iOS one).

react-native-snap-carousel tinder layout android

Hope this helps!

nkrmr commented

Hello,

I'm facing the same problem, i can't swipe with the entire area of my cards because of zIndex issue on stack layout. I tried your solution but that revert my cards display and into vertical mode it's very not a good display and other think i can't use looping options. Is there another solution to swipe normaly and keep the all the functionalities??

@Soulso Unfortunately, I haven't been able to pinned down the root of the issue yet. Even though I fear a React Native bug, any insight that could point me in the relevant direction would be of great help!

I'm currently considering horrible workarounds like setting pointerEvents={'none'} for non-visible elements for example... I'll let you know if I find anything that works.

For now, unless you create a custom interpolation that doesn't require the zIndex trick (like this one), you may encounter the issue.

nkrmr commented

Hello thanks for the answer, i hope you will found a solution.
How i can know that an element is non-visible?

@Soulso You can use the measure() method in conjunction with prop onSnapToItem(index). For example, you could check if index - 1, index and index + 1 are located in the viewport and then set pointerEvents to none for every item but those three, given that they are currently visible.

Or, you can simply set pointerEvents to auto for those three items, without relying on measure().

Be aware that:

  • it's a hack
  • measure() is asynchronous and its execution usually takes about 500 ms
  • there will almost certainly be side effects
  • I'm guessing that it can solve the issue, but I'm not sure yet.

Anyway, if you decide to give it a try, I'll be interested in your findings ;-)

@bd-arc Thank you very much! It's fixed the problem on iOS using the Android style.

Not sure if this has been already reported, but I noticed that the area where swipe actions are not detected is actually occupied by one or more "ghost" cards.
If you inspect the layout (CMD+I) and tap in those areas, you can see that there is an invisible card partially overlapping the active one.
That said, given that the first two or three cards in the carousel seem to work well, I suppose that old cards that should be way off the screen are instead rendered in that area, messing with the click events.
Hope this could help in fixing or finding a workaround!

I'm also experiencing the same issues. I thought I was going mad (hence the awe-inspiring background colors).
So if this helps at all: In the image below I'm on the 6th slide, and the ghost image is from the 2nd slide.
screen shot 2018-06-22 at 06 44 16

Very interesting! However I confirm that the problem goes away if you use

removeClippedSubviews={false}

If you don't have too many cards it shouldn't be a big deal.

(EDIT: I've corrected my post. If you want a workaround, you need to set it to false)

@brunoci removeClippedSubviews={true} doesn't help me ๐Ÿ˜ข

I encountered an issue on iOS where I am displaying 3 carousels on 1 view and the third one is not showing up, removeClippedSubviews={true} fixed it.

In version 3.8.0 I've set removeClippedSubviews to false by default when using the 'stack' or 'tinder' layouts.

This should help!

7dp commented

If I remove the following style rule, it works properly:

zIndex: carouselProps.data.length - index

But then, of course, the layout is completely messed up:
react-native-snap-carousel issue 262

A quick fix would be to use props scrollInterpolator and slideInterpolatedStyle to pass the Android 'stack' custom interpolation. The effect is going to be inverted, but at least there will be no need to specify a zIndex.

Here is the relevant code in case you want a quick fix for the 'stack' effect:

import Carousel, { getInputRangeFromIndexes } from 'react-native-snap-carousel';

function stackScrollInterpolator (index, carouselProps) {
    const range = [1, 0, -1, -2, -3];
    const inputRange = getInputRangeFromIndexes(range, index, carouselProps);
    const outputRange = range;
    return { inputRange, outputRange };
}

function stackAnimatedStyles (index, animatedValue, carouselProps) {
    const sizeRef = carouselProps.vertical ? carouselProps.itemHeight : carouselProps.itemWidth;
    const translateProp = carouselProps.vertical ? 'translateY' : 'translateX';

    const cardOffset = 18;
    const card1Scale = 0.9;
    const card2Scale = 0.8;

    const getTranslateFromScale = (index, scale) => {
        const centerFactor = 1 / scale * index;
        const centeredPosition = -Math.round(sizeRef * centerFactor);
        const edgeAlignment = Math.round((sizeRef - (sizeRef * scale)) / 2);
        const offset = Math.round(cardOffset * Math.abs(index) / scale);

        return centeredPosition - edgeAlignment - offset;
    };

    return {
        opacity: animatedValue.interpolate({
            inputRange: [-3, -2, -1, 0],
            outputRange: [0, 0.5, 0.75, 1],
            extrapolate: 'clamp'
        }),
        transform: [{
            scale: animatedValue.interpolate({
                inputRange: [-2, -1, 0, 1],
                outputRange: [card2Scale, card1Scale, 1, card1Scale],
                extrapolate: 'clamp'
            })
        }, {
            [translateProp]: animatedValue.interpolate({
                inputRange: [-3, -2, -1, 0, 1],
                outputRange: [
                    getTranslateFromScale(-3, card2Scale),
                    getTranslateFromScale(-2, card2Scale),
                    getTranslateFromScale(-1, card1Scale),
                    0,
                    sizeRef * 0.5
                ],
                extrapolate: 'clamp'
            })
        }]
    };
}

const myCarousel = (
    <Carousel
      scrollInterpolator={stackScrollInterpolator}
      slideInterpolatedStyle={stackAnimatedStyles}
      useScrollView={true} // <--- Use this for a better effect or disable it to get performance optimizations
    />
);

=> Note that you're going to get the Android effect on both platforms (inverted compared to the original iOS one).

react-native-snap-carousel stack layout android

Regarding the issue, I am left clueless. I don't understand why:

  • the zIndex rule randomly messes with the swipe events (sometimes you can swipe the 2-3 first items and then you need to swipe from a lower portion of the screen)
  • only the top part of the screen is affected
  • the default effect doesn't have any swipe issue even if we add the zIndex rule.

=> Any insight will be greatly appreciated!

@bd-arc can i make the inverted version from this effect on "tinder" layout on both android & ios?

Sorry, please allow me to advertise for my open source library! ~
I think this library react-native-reanimated-carousel will solve your problem. It is a high performance and very simple component, complete with React-Native reanimated 2

If I remove the following style rule, it works properly:

zIndex: carouselProps.data.length - index

But then, of course, the layout is completely messed up: react-native-snap-carousel issue 262

A quick fix would be to use props scrollInterpolator and slideInterpolatedStyle to pass the Android 'stack' custom interpolation. The effect is going to be inverted, but at least there will be no need to specify a zIndex.

Here is the relevant code in case you want a quick fix for the 'stack' effect:

import Carousel, { getInputRangeFromIndexes } from 'react-native-snap-carousel';

function stackScrollInterpolator (index, carouselProps) {
    const range = [1, 0, -1, -2, -3];
    const inputRange = getInputRangeFromIndexes(range, index, carouselProps);
    const outputRange = range;
    return { inputRange, outputRange };
}

function stackAnimatedStyles (index, animatedValue, carouselProps) {
    const sizeRef = carouselProps.vertical ? carouselProps.itemHeight : carouselProps.itemWidth;
    const translateProp = carouselProps.vertical ? 'translateY' : 'translateX';

    const cardOffset = 18;
    const card1Scale = 0.9;
    const card2Scale = 0.8;

    const getTranslateFromScale = (index, scale) => {
        const centerFactor = 1 / scale * index;
        const centeredPosition = -Math.round(sizeRef * centerFactor);
        const edgeAlignment = Math.round((sizeRef - (sizeRef * scale)) / 2);
        const offset = Math.round(cardOffset * Math.abs(index) / scale);

        return centeredPosition - edgeAlignment - offset;
    };

    return {
        opacity: animatedValue.interpolate({
            inputRange: [-3, -2, -1, 0],
            outputRange: [0, 0.5, 0.75, 1],
            extrapolate: 'clamp'
        }),
        transform: [{
            scale: animatedValue.interpolate({
                inputRange: [-2, -1, 0, 1],
                outputRange: [card2Scale, card1Scale, 1, card1Scale],
                extrapolate: 'clamp'
            })
        }, {
            [translateProp]: animatedValue.interpolate({
                inputRange: [-3, -2, -1, 0, 1],
                outputRange: [
                    getTranslateFromScale(-3, card2Scale),
                    getTranslateFromScale(-2, card2Scale),
                    getTranslateFromScale(-1, card1Scale),
                    0,
                    sizeRef * 0.5
                ],
                extrapolate: 'clamp'
            })
        }]
    };
}

const myCarousel = (
    <Carousel
      scrollInterpolator={stackScrollInterpolator}
      slideInterpolatedStyle={stackAnimatedStyles}
      useScrollView={true} // <--- Use this for a better effect or disable it to get performance optimizations
    />
);

=> Note that you're going to get the Android effect on both platforms (inverted compared to the original iOS one).

react-native-snap-carousel stack layout android react-native-snap-carousel stack layout android

Regarding the issue, I am left clueless. I don't understand why:

  • the zIndex rule randomly messes with the swipe events (sometimes you can swipe the 2-3 first items and then you need to swipe from a lower portion of the screen)
  • only the top part of the screen is affected
  • the default effect doesn't have any swipe issue even if we add the zIndex rule.

=> Any insight will be greatly appreciated!

I want right stack swipe direction, what should I need to change in animation file?