在React Native中实现可滑动动画图片堆叠交互
React Native 可循环滑动图片堆叠交互实现(已解决)
问题背景
需要实现以下交互:用户滑动图片堆叠,每次滑动时顶部卡片移至堆叠底部,其余卡片向前移动,同一时间仅显示3张图片,支持任意长度的图片数组循环浏览。使用react-native-reanimated 3和react-native-gesture-handler开发时遇到两个问题:
- 动画运行时更新堆叠导致图片闪烁
- 卡片循环正常但手势结束时无动画
解决说明
问题已解决,根源在于:
- 组件树上层传递了不必要的
key属性,触发了不必要的重渲染 - 代码中存在冗余逻辑,干扰了卡片状态的正确维护
最终代码
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)
内容的提问来源于stack exchange,提问作者resistor
相关产品推荐
相关产品推荐

