如何在React Native Android端实现多点同时触摸与多按钮同时按下功能
React Native Android 多按钮同时触摸解决方案
问题根因
- React Native 默认的
Pressable/普通View触摸事件默认是单触点响应逻辑,Android 平台上未显式配置多触点支持时,第一触点未释放的情况下后续触摸事件会被父组件拦截或无法分发到对应子组件 - 你当前使用
FlatList承载按键,默认FlatList为了处理滑动手势会拦截子组件的触摸事件,进一步影响多触点分发
方案1:无需PanResponder的修复方案(适配最多3点触摸场景)
如果不想引入PanResponder,可以按以下步骤修改:
- 首先调整
FlatList配置,禁用触摸拦截(如果你的键盘不需要滚动可以直接禁用滚动):
<FlatList data={data} scrollEnabled={false} // 不需要滚动直接禁用,能解决大部分触摸拦截问题 removeClippedSubviews={false} // 禁用子视图裁剪,避免触摸错位 renderItem={({item}) => <Key title={item.label} name={item.name} />} />
- 修改
Key组件的触摸事件,区分触摸点ID,不要直接依赖单个onTouchStart/onTouchEnd的触发:
const Key = ({label, name, onTouchChange}) => { const [hold, setHold] = useState(false); // 存储当前按下这个按键的触摸点ID const activeTouchId = useRef(null); // 存储当前按键的实际宽高,通过onLayout获取 const keyLayout = useRef({width: 0, height: 0}); const handleLayout = (e) => { const {width, height} = e.nativeEvent.layout; keyLayout.current = {width, height}; }; const handleTouchStart = (e) => { const {locationX, locationY, touches} = e.nativeEvent; // 遍历当前所有触摸点,判断是否落在当前按键范围内 for (let i = 0; i < touches.length; i++) { const touch = touches[i]; if ( locationX >= 0 && locationX <= keyLayout.current.width && locationY >=0 && locationY <= keyLayout.current.height && activeTouchId.current === null ) { activeTouchId.current = touch.identifier; setHold(true); onTouchChange(name, true); break; } } }; const handleTouchEnd = (e) => { const {changedTouches} = e.nativeEvent; for (let i=0; i<changedTouches.length; i++) { const touch = changedTouches[i]; if (touch.identifier === activeTouchId.current) { activeTouchId.current = null; setHold(false); onTouchChange(name, false); break; } } }; // 处理触摸被父组件拦截的场景,避免状态不重置 const handleTouchCancel = handleTouchEnd; return ( <View onLayout={handleLayout} onTouchStart={handleTouchStart} onTouchEnd={handleTouchEnd} onTouchCancel={handleTouchCancel} style={hold ? styles.keyHold : styles.keyUnhold} > <Text>{label}</Text> </View> ); };
方案2:使用PanResponder实现(更稳定,适配复杂交互)
如果方案1依然有异常,推荐用PanResponder在父层统一处理所有触摸事件,分发到对应按键,比每个子组件单独处理更稳定:
import {PanResponder, View, Text, FlatList} from 'react-native'; const Keyboard = ({data}) => { // 存储所有按键的布局位置,用于判断触摸点落在哪个按键上 const keyLayouts = useRef(new Map()); // 存储当前激活的触摸点和对应按键的映射 {[touchId]: keyName} const activeTouches = useRef(new Map()); // 存储按键按压状态,触发渲染 const [holdKeys, setHoldKeys] = useState(new Set()); const panResponder = useRef( PanResponder.create({ onStartShouldSetPanResponder: () => true, onStartShouldSetPanResponderCapture: () => false, onMoveShouldSetPanResponder: () => false, onMoveShouldSetPanResponderCapture: () => false, onPanResponderTerminationRequest: () => false, // 处理触摸开始 onPanResponderGrant: (e) => { const {touches} = e.nativeEvent; const newHoldKeys = new Set(holdKeys); touches.forEach(touch => { const {identifier, pageX, pageY} = touch; // 遍历所有按键布局,判断触摸点落在哪个按键上 for (const [keyName, layout] of keyLayouts.current.entries()) { if ( pageX >= layout.x && pageX <= layout.x + layout.width && pageY >= layout.y && pageY <= layout.y + layout.height ) { activeTouches.current.set(identifier, keyName); newHoldKeys.add(keyName); break; } } }); setHoldKeys(newHoldKeys); }, // 处理触摸结束 onPanResponderRelease: (e) => { const {changedTouches} = e.nativeEvent; const newHoldKeys = new Set(holdKeys); changedTouches.forEach(touch => { const keyName = activeTouches.current.get(touch.identifier); if (keyName) { activeTouches.current.delete(touch.identifier); newHoldKeys.delete(keyName); } }); setHoldKeys(newHoldKeys); }, // 处理触摸被系统拦截场景,比如来电、下拉通知栏 onPanResponderTerminate: (e) => { const {changedTouches} = e.nativeEvent; const newHoldKeys = new Set(holdKeys); changedTouches.forEach(touch => { const keyName = activeTouches.current.get(touch.identifier); if (keyName) { activeTouches.current.delete(touch.identifier); newHoldKeys.delete(keyName); } }); setHoldKeys(newHoldKeys); } }) ).current; // 记录每个按键的全局布局位置 const handleKeyLayout = (keyName, e) => { e.target.measure((x, y, width, height, pageX, pageY) => { keyLayouts.current.set(keyName, {x: pageX, y: pageY, width, height}); }); }; return ( <View {...panResponder.panHandlers} style={styles.keyboardContainer}> <FlatList data={data} scrollEnabled={false} renderItem={({item}) => ( <View onLayout={(e) => handleKeyLayout(item.name, e)} style={holdKeys.has(item.name) ? styles.keyHold : styles.keyUnhold} > <Text>{item.label}</Text> </View> )} /> </View> ); };
额外注意事项
- RN 0.67及以下版本,需要在
AndroidManifest.xml的对应Activity节点添加android:splitMotionEvents="true"属性,开启系统层面的多触点事件分发 - 不要给按键的父组件添加任何触摸拦截逻辑,比如
onStartShouldSetResponder返回true的配置,会导致子组件无法收到触摸事件
内容的提问来源于stack exchange,提问作者Farooq Karimi Zadeh
相关产品推荐
相关产品推荐

