You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Native Modal结合Reanimated回弹动画失效问题求助

React Native Modal下拉回弹动画无效问题修复

问题概述

使用React Native + react-native-gesture-handler 2.14 + react-native-reanimated 3.6.2实现Modal下拉关闭功能,拖拽超过阈值能正常关闭,但拖拽距离不足时,Modal无法通过弹簧动画回弹到初始位置(transformY=0),其他组件的Reanimated插值动画正常,怀疑Modal原生动画冲突。

问题代码

// Drag/Pan gesture handler
const dragGesture = Gesture.Pan()
    .activeOffsetY(100) // 拖拽超过100px才触发手势
    .enabled(isAtTop) // 仅在页面顶部时监听手势
    .onStart(() => {
        isDragging.value = true
    })
    .onUpdate((event) => {
        if (event.translationY >= 0) {
            translateY.value = event.translationY
        }
    })
    .onEnd((event) => {
        // 拖拽超过阈值则关闭Modal
        if (translateY.value > windowHeight / 2 || (event.velocityY > 500 && isAtTop)) {
            runOnJS(setModalVisible)(false)
        } else {
            // 拖拽距离不足时回弹至初始位置(动画无效)
            translateY.value = withSpring(0, {
                damping: 20,
                stiffness: 90,
            })
        }
        isDragging.value = false
    })

const composedGestures = Gesture.Simultaneous(dragGesture, nativeGesture)

const modalAnimatedStyle = useAnimatedStyle(() => {
    console.log(translateY.value)
    return {
        transform: [{ translateY: translateY.value }],
    }
}, [translateY.value])

return (
    <Modal
        animationType="slide"
        transparent={true}
        visible={modalVisible}
        onRequestClose={() => {
            setModalVisible(!modalVisible)
        }}
        onDismiss={() => {
            translateY.value = 0
        }}>
        <Animated.View style={[styles.modalView, modalAnimatedStyle]}>
            <HomeListingsExpanded
                navigation={navigation}
                route={{ params: { listing, timezoneData } }}
                setModalVisible={setModalVisible}
                setIsAtTop={setIsAtTop}
                composedGestures={composedGestures}
            />
        </Animated.View>
    </Modal>
)

修复方案

1. 移除Modal原生动画

Modal的animationType="slide"会启动原生动画,和Reanimated的自定义动画产生冲突,导致回弹动画被覆盖。将animationType改为none或者直接删除该属性:

<Modal
    animationType="none" // 关键修改:禁用原生动画
    transparent={true}
    visible={modalVisible}
    // 其他属性保持不变
>

2. 在UI线程执行弹簧动画

在Gesture的onEnd worklet中,必须通过runOnUI来触发Reanimated动画,否则动画可能无法正确执行:

.onEnd((event) => {
    if (translateY.value > windowHeight / 2 || (event.velocityY > 500 && isAtTop)) {
        runOnJS(setModalVisible)(false)
    } else {
        // 用runOnUI包裹动画执行逻辑
        runOnUI(() => {
            translateY.value = withSpring(0, {
                damping: 20,
                stiffness: 90,
            })
        })()
    }
    isDragging.value = false
})

3. 拖拽开始时暂停现有动画

避免拖拽操作和未完成的回弹动画冲突,在onStart中添加cancelAnimation:

.onStart(() => {
    isDragging.value = true
    cancelAnimation(translateY) // 取消当前可能在运行的动画
})

4. 简化useAnimatedStyle依赖

useAnimatedStyle不需要手动传入translateY.value作为依赖,Reanimated会自动追踪共享值的变化,简化代码:

const modalAnimatedStyle = useAnimatedStyle(() => {
    return {
        transform: [{ translateY: translateY.value }],
    }
})

完整修复后的代码

// Drag/Pan gesture handler
const dragGesture = Gesture.Pan()
    .activeOffsetY(100)
    .enabled(isAtTop)
    .onStart(() => {
        isDragging.value = true
        cancelAnimation(translateY) // 取消现有动画
    })
    .onUpdate((event) => {
        if (event.translationY >= 0) {
            translateY.value = event.translationY
        }
    })
    .onEnd((event) => {
        if (translateY.value > windowHeight / 2 || (event.velocityY > 500 && isAtTop)) {
            runOnJS(setModalVisible)(false)
        } else {
            runOnUI(() => {
                translateY.value = withSpring(0, {
                    damping: 20,
                    stiffness: 90,
                })
            })()
        }
        isDragging.value = false
    })

const composedGestures = Gesture.Simultaneous(dragGesture, nativeGesture)

const modalAnimatedStyle = useAnimatedStyle(() => {
    return {
        transform: [{ translateY: translateY.value }],
    }
})

return (
    <Modal
        animationType="none" // 禁用原生动画
        transparent={true}
        visible={modalVisible}
        onRequestClose={() => {
            setModalVisible(!modalVisible)
        }}
        onDismiss={() => {
            translateY.value = 0
        }}>
        <Animated.View style={[styles.modalView, modalAnimatedStyle]}>
            <HomeListingsExpanded
                navigation={navigation}
                route={{ params: { listing, timezoneData } }}
                setModalVisible={setModalVisible}
                setIsAtTop={setIsAtTop}
                composedGestures={composedGestures}
            />
        </Animated.View>
    </Modal>
)

内容的提问来源于stack exchange,提问作者Deland

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 07:40:37