React Native返回时Android与iOS触摸交互差异及iOS适配需求
使用navigation.goBack()执行带动画的栈返回操作(从当前页面切换回之前页面)时,Android平台中之前页面的react-native-gesture-handler的Touchable组件在当前页面卸载前即可被触摸;但iOS平台需等待当前页面完全卸载后,之前页面的Touchable组件才可被触摸。请问造成该差异的原因是什么?是否有办法让iOS平台在当前页面卸载前就能触摸之前页面的Touchable组件?
相关代码
Home.tsx
import {useNavigation} from '@react-navigation/native'; import React, {useEffect} from 'react'; import {StyleSheet, Text, View} from 'react-native'; import {TouchableHighlight, TouchableWithoutFeedback} from 'react-native-gesture-handler'; const Home = () => { const navigation = useNavigation(); const goUpper = () => { console.log('goupper click'); navigation.navigate('upper'); }; return ( <View style={styles.container}> <TouchableWithoutFeedback onPress={() => console.log('clickable')}> <Text>Home</Text> <TouchableHighlight onPress={goUpper}> <Text>goUpper</Text> </TouchableHighlight> </TouchableWithoutFeedback> </View> ); }; export default Home; const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, });
Upper.tsx
import React, { useEffect } from 'react'; import {StyleSheet, View, Text, Button} from 'react-native'; import {useNavigation} from '@react-navigation/native'; const Upper = () => { const navigation = useNavigation(); const goBack = () => { console.log('back click'); navigation.goBack(); }; useEffect(() => { console.log('upper render'); return () => console.log('upper unmount'); }, []); return ( <View style={styles.container}> <Text>Upper</Text> <Button onPress={goBack} title="goBack" /> </View> ); }; export default Upper; const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', justifyContent: 'center', }, });
Navigator.tsx
import React from 'react'; import {createStackNavigator, CardStyleInterpolators} from '@react-navigation/stack'; import {Home, Upper} from './views'; type RootStackParamList = { home: undefined; upper: undefined; }; const Stack = createStackNavigator<RootStackParamList>(); function MyStack() { return ( <Stack.Navigator screenOptions={{ cardStyleInterpolator: CardStyleInterpolators.forModalPresentationIOS, }}> <Stack.Screen name="home" component={Home} /> <Stack.Screen name="upper" component={Upper} /> </Stack.Navigator> ); } export default MyStack;
测试结果
iOS平台在返回动画过程中点击之前页面的Touchable组件无响应,需等待动画结束(当前页面完全卸载)后才会响应;Android平台在返回动画过程中即可正常触发之前页面Touchable组件的触摸事件。
差异原因
原生导航容器的触摸拦截逻辑不同
Android原生栈导航容器在页面返回动画期间,不会拦截底层页面的触摸事件;而iOS的UINavigationController默认在模态转场动画过程中,会将触摸事件拦截在上层页面容器,直到动画完成、页面完全移除后才会把事件传递给底层页面。React Navigation动画的原生依赖
你使用的CardStyleInterpolators.forModalPresentationIOS是模拟iOS原生模态转场的动画逻辑,会严格遵循iOS原生的触摸拦截规则;而Android端的栈导航则沿用Android原生的事件传递机制,没有这个拦截限制。react-native-gesture-handler的事件处理限制
在iOS上,Gesture Handler的触摸识别会受到原生容器事件拦截的影响,上层页面容器在动画期间会阻止底层页面的Gesture Handler触摸事件触发;Android端则不存在这个层级的拦截。
解决方案
要让iOS在返回动画期间允许触摸底层页面的Touchable组件,可尝试以下方案:
方案1:自定义导航选项,关闭上层页面的触摸拦截
修改Navigator.tsx中的screenOptions,配置pointerEvents让上层页面容器不拦截触摸事件,同时自定义转场动画(避免原生模态动画的拦截逻辑):
screenOptions={{ cardStyleInterpolator: ({ current, layouts }) => { return { cardStyle: { transform: [ { translateY: current.progress.interpolate({ inputRange: [0, 1], outputRange: [layouts.screen.height, 0], }), }, ], }, }; }, // 允许底层页面接收触摸事件 cardStyle: { pointerEvents: 'box-none' }, }}
注意:pointerEvents: 'box-none'会让上层页面的容器不拦截触摸,但上层页面的内容仍可能被触摸到,若需要避免这种情况,可给上层页面的根容器添加pointerEvents: 'auto'。
方案2:编写iOS原生代码修改导航容器的触摸逻辑
通过自定义UINavigationController子类,重写hitTest(_:with:)方法,在转场动画期间让底层视图可以接收触摸事件。示例原生代码:
class CustomNavigationController: UINavigationController { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let view = super.hitTest(point, with: event) // 如果当前正在转场,且触摸的是底层页面的视图,允许事件传递 if transitionCoordinator?.isInteractive == true || transitionCoordinator?.isAnimating == true { return view ?? viewControllers.first?.view.hitTest(convert(point, to: viewControllers.first?.view), with: event) } return view } }
之后在React Native的导航配置中使用这个自定义导航容器。
方案3:替换为React Native原生Touchable组件
如果使用React Native原生的TouchableOpacity、TouchableHighlight而非react-native-gesture-handler的对应组件,iOS上的触摸事件可能不会被原生容器拦截,但这需要调整你的技术栈依赖。
内容的提问来源于stack exchange,提问作者user20821745

