Swipes do not register in top half of portrait-oriented Carousel Card ('stack', and 'tinder')
jordangrant opened this issue ยท 19 comments
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'
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:
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).
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.
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).
Hope this helps!
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.
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!
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)
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!
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:
A quick fix would be to use props
scrollInterpolator
andslideInterpolatedStyle
to pass the Android 'stack' custom interpolation. The effect is going to be inverted, but at least there will be no need to specify azIndex
.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).
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:
A quick fix would be to use props
scrollInterpolator
andslideInterpolatedStyle
to pass the Android 'stack' custom interpolation. The effect is going to be inverted, but at least there will be no need to specify azIndex
.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).
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?