我正试图在Reaction Native中创建一个交互,用户可以在其中滑动一堆图像.图像应该是动画的,这样每次刷卡时,顶部的卡都会移到堆叠的底部,其他卡都会向前移动.它应该类似于下面的内容:
在我的用例中,图像数组的长度是任意的,但一次只能看到3张图像.用户应该能够通过连续滑动循环通过所有的图像.
我用的是react-native-reanimated
3和react-native-gesture-handler
.我try 了几种不同的方法,所有方法都很接近,但都有不同的问题.当我更新堆栈时,要么动画运行但图像闪烁,要么图像正确循环但在手势结束时没有动画(这就是我目前的方法所发生的情况).
有人能把我带到正确的轨道上来吗?
我想我解出来了.我将key
props 传递给树中更高的组件,这会导致不必要的重新渲染,而我的代码有一些不必要的复杂性,这会扰乱卡片状态.
其中Video here个都能达到预期效果.
以下是代码,省略了一些不相关的部分:
// day-card.js
const CARD_MARGIN = 20
const CARD_OFFSET = 50
function DayCard({
selectedId,
dateString,
creationTime,
isShowingStack,
toggleStack,
allImages,
}) {
const dispatch = useDispatch()
const insets = useSafeAreaInsets()
const numImages = allImages.length
const [imageStack, setImageStack] = useState(allImages)
const windowWidth = Dimensions.get('window').width
const topCardTranslateX = useSharedValue(0)
const topCardTranslateY = useSharedValue(0)
const bottomCardTranslateX = useSharedValue(0)
const bottomCardTranslateY = useSharedValue(0)
const middleCardTranslateX = useSharedValue(0)
const middleCardTranslateY = useSharedValue(0)
const topCardMargin = useSharedValue(CARD_MARGIN)
const middleCardMargin = useSharedValue(CARD_MARGIN)
const bottomCardMargin = useSharedValue(CARD_MARGIN)
const topCardZIndex = useSharedValue(4)
const middleCardZIndex = useSharedValue(3)
const bottomCardZIndex = useSharedValue(2)
const topCardOpacity = useSharedValue(1)
const middleCardOpacity = useSharedValue(1)
const bottomCardOpacity = useSharedValue(1)
const middleCardRotation = useSharedValue(0)
const bottomCardRotation = useSharedValue(0)
const topCardRotation = useSharedValue(0)
const topCardBrightness = useSharedValue(1)
const middleCardBrightness = useSharedValue(0.75)
const bottomCardBrightness = useSharedValue(0.5)
const animationIdx = useSharedValue(0)
const cardStyles = [
useAnimatedStyle(() => ({
transform: [
{
translateX: topCardTranslateX.value,
},
{
translateY: topCardTranslateY.value,
},
{
rotate: `${topCardRotation.value}deg`,
},
],
width: windowWidth - topCardMargin.value * 2,
marginHorizontal: topCardMargin.value,
zIndex: topCardZIndex.value,
opacity: topCardOpacity.value,
})),
useAnimatedStyle(() => ({
transform: [
{
translateX: middleCardTranslateX.value,
},
{
translateY: middleCardTranslateY.value,
},
{
rotate: `${middleCardRotation.value}deg`,
},
],
width: windowWidth - middleCardMargin.value * 2,
marginHorizontal: middleCardMargin.value,
zIndex: middleCardZIndex.value,
opacity: middleCardOpacity.value,
})),
useAnimatedStyle(() => ({
transform: [
{
translateX: bottomCardTranslateX.value,
},
{
translateY: bottomCardTranslateY.value,
},
{
rotate: `${bottomCardRotation.value}deg`,
},
],
width: windowWidth - bottomCardMargin.value * 2,
marginHorizontal: bottomCardMargin.value,
zIndex: bottomCardZIndex.value,
opacity: bottomCardOpacity.value,
})),
]
const imageIndices = [
[0, 1, 2],
[2, 0, 1],
[1, 2, 0],
]
const Cards = [
<Animated.View style={[styles.card, cardStyles[0]]}>
<SingleCard
dateString={dateString}
uri={imageStack[imageIndices[animationIdx.value][0]].uri}
id={imageStack[imageIndices[animationIdx.value][0]].id}
brightness={topCardBrightness}
/>
</Animated.View>,
numImages > 1 ? (
<Animated.View style={[styles.card, cardStyles[1]]}>
<SingleCard
uri={imageStack[imageIndices[animationIdx.value][1]].uri}
id={imageStack[imageIndices[animationIdx.value][1]].id}
dateString={dateString}
brightness={middleCardBrightness}
/>
</Animated.View>
) : null,
numImages > 2 ? (
<Animated.View style={[styles.card, cardStyles[2]]}>
<SingleCard
uri={imageStack[imageIndices[animationIdx.value][2]].uri}
id={imageStack[imageIndices[animationIdx.value][2]].id}
dateString={dateString}
brightness={bottomCardBrightness}
/>
</Animated.View>
) : null,
]
const dispatchNewState = () => {
dispatch(selectImageForDate(imageStack[0]))
}
const handleToggle = () => {
if (isShowingStack) {
topCardMargin.value = withTiming(CARD_MARGIN)
middleCardMargin.value = withTiming(CARD_MARGIN)
bottomCardMargin.value = withTiming(CARD_MARGIN)
middleCardTranslateY.value = withTiming(0)
bottomCardTranslateY.value = withTiming(0)
topCardTranslateY.value = withTiming(0)
dispatchNewState()
} else {
setCardState()
}
toggleStack()
}
const DEFAULT_WIDTH = windowWidth - CARD_MARGIN * 2
const MIDDLE_WIDTH = windowWidth - CARD_MARGIN
const styleMap = {
translateY: [CARD_OFFSET, CARD_OFFSET / 2, 0],
brightness: [1, 0.75, 0.5],
margin: [0, CARD_MARGIN / 2, CARD_MARGIN],
width: [windowWidth, MIDDLE_WIDTH, DEFAULT_WIDTH],
zIndex: [4, 3, 2],
}
const endGesture = () => {
'worklet'
topCardTranslateX.value = withSpring(0)
topCardRotation.value = withTiming(0)
topCardOpacity.value = withTiming(1)
middleCardTranslateX.value = withSpring(0)
middleCardRotation.value = withTiming(0)
middleCardOpacity.value = withTiming(1)
bottomCardTranslateX.value = withSpring(0)
bottomCardRotation.value = withTiming(0)
bottomCardOpacity.value = withTiming(1)
}
const setCardState = () => {
'worklet'
const topCardIdx = imageIndices[animationIdx.value][0]
const middleCardIdx = imageIndices[animationIdx.value][1]
const bottomCardIdx = imageIndices[animationIdx.value][2]
topCardTranslateY.value = withSpring(styleMap.translateY[topCardIdx])
middleCardTranslateY.value = withSpring(
styleMap.translateY[middleCardIdx]
)
bottomCardTranslateY.value = withSpring(
styleMap.translateY[bottomCardIdx]
)
topCardMargin.value = withSpring(styleMap.margin[topCardIdx])
middleCardMargin.value = withSpring(styleMap.margin[middleCardIdx])
bottomCardMargin.value = withSpring(styleMap.margin[bottomCardIdx])
topCardZIndex.value = styleMap.zIndex[topCardIdx]
middleCardZIndex.value = styleMap.zIndex[middleCardIdx]
bottomCardZIndex.value = styleMap.zIndex[bottomCardIdx]
middleCardBrightness.value = withTiming(
styleMap.brightness[middleCardIdx]
)
topCardBrightness.value = withTiming(styleMap.brightness[topCardIdx])
bottomCardBrightness.value = withTiming(
styleMap.brightness[bottomCardIdx]
)
}
const stackGesture = Gesture.Pan()
.onChange(({ translationX }) => {
const rotation = interpolate(
translationX,
[-windowWidth, windowWidth],
[-45, 45]
)
const opacity = interpolate(
Math.abs(translationX),
[0, windowWidth],
[1, 0.5]
)
if (animationIdx.value === 0) {
topCardRotation.value = `${rotation}deg`
topCardTranslateX.value = translationX
topCardOpacity.value = opacity
} else if (animationIdx.value === 1) {
middleCardRotation.value = `${rotation}deg`
middleCardTranslateX.value = translationX
middleCardOpacity.value = opacity
} else if (animationIdx.value === 2) {
bottomCardRotation.value = `${rotation}deg`
bottomCardTranslateX.value = translationX
bottomCardOpacity.value = opacity
}
})
.onEnd(() => {
endGesture()
if (
(animationIdx.value === 0 &&
Math.abs(topCardTranslateX.value) > 150) ||
(animationIdx.value === 1 &&
Math.abs(middleCardTranslateX.value) > 150) ||
(animationIdx.value === 2 &&
Math.abs(bottomCardTranslateX.value) > 150)
) {
animationIdx.value = (animationIdx.value + 1) % 3
runOnJS(setImageStack)([...imageStack.slice(1), imageStack[0]])
setCardState()
}
})
return (
<BlurView
intensity={10}
alignContent="center"
justifyContent="center"
flex={1}
tint="dark"
>
<GestureDetector
gesture={isShowingStack ? stackGesture : null}
>
<View>
{Cards[0]}
{Cards[1]}
{Cards[2]}
</View>
</GestureDetector>
</BlurView>
)
}
const styles = StyleSheet.create({
card: {
position: 'absolute',
top: 0,
width: '100%',
},
})
export default memo(DayCard)
// single-card.js
function SingleCard({ uri, dateString, id, brightness }) {
const animatedStyle = useAnimatedStyle(() =>
brightness
? {
opacity: 1 - brightness.value,
}
: {}
)
return (
<SharedElement id={id}>
<ImageBackground
style={styles.image}
source={{ uri }}
alt={`A photo taken on ${dateString}`}
>
{brightness && (
<Animated.View
style={[
{
backgroundColor: 'black',
flex: 1,
},
animatedStyle,
]}
/>
)}
</ImageBackground>
</SharedElement>
)
}
const styles = StyleSheet.create({
image: {
width: '100%',
aspectRatio: 3 / 4,
position: 'relative',
},
})
export default memo(SingleCard)