React Native:将 Pan Responder 事件从视图传播到内部滚动视图
React Native: Propagate Pan Responder event from view to inner scroll view
我在具有平移功能的动画视图中有一个 ScrollView。
<Animated.View {...this.panResponder.panHandlers}>
<ScrollView>
...
</ScrollView>
<Animated.View>
这是我的屏幕示例视图:
用户应该能够向上滑动并且可拖动区域应该向上对齐,如下所示:
现在我的问题是滚动视图。我希望用户能够滚动里面的内容。
用户看完里面的内容后
并一直向上滚动(通过执行向下滑动动作)并尝试进一步滑动,可拖动区域应向下移动到其原始位置。
我尝试了各种方法,主要是关闭和开启ScrollView的滚动,防止其干扰平移。
我目前的方案并不理想。
我的主要问题是这两种方法:
onStartShouldSetPanResponder
onStartShouldSetPanResponderCapture
不确定我的假设是否正确,但这些方法决定了 View 是否应该捕获触摸事件。我要么允许平移,要么让 ScrollView 捕获事件。
我的问题是我需要以某种方式知道用户打算在其他 pan 处理程序启动之前执行的操作。但是在用户向下或向上移动之前我无法知道。要知道方向,我需要将事件传递给 onPanResponderMove 处理程序。
所以本质上,我需要在知道用户滑动的方向之前决定是否允许拖动我的视图。目前这是不可能的。
希望我在这里遗漏了一些简单的东西。
编辑: 发现一个类似的问题(没有答案):
Drag up a ScrollView then continue scroll in React Native
也许,这和你的问题一样
https://github.com/rome2rio/react-native-touch-through-view
我认为更好的叉子
https://github.com/simonhoss/react-native-touch-through-view/issues/5
我认为您可以创建一个 bottomsheetlayout 来满足您的要求。或者您可以使用 iOS 中的操作表。考虑下面的库。可能对你有帮助
https://github.com/cesardeazevedo/react-native-bottom-sheet-behavior
或
显然是 Native 层的问题。
https://github.com/facebook/react-native/issues/9545#issuecomment-245014488
I found that onterminationrequest not being triggerred is caused by
Native Layer.
Modify
react-native\ReactAndroid\src\main\java\com\facebook\react\views\scroll\ReactScrollView.java
, comment Line NativeGestureUtil.notifyNativeGestureStarted(this, ev);
and then build from the source, you will see your
PanResponder outside ScrollView takes the control as expected now.
PS:我还不能从源代码构建。从源代码构建显然比我想象的要难得多。
编辑 1:
是的,它起作用了。我从 node_modules 中删除了 react-native 文件夹,然后 git 将 react-native 存储库直接克隆到 node_modules
中。并检查到版本 0.59.1
。然后,按照 this 说明进行操作。
对于此示例,我不必将任何 PanReponder 或 Responder 设置为 ScrollView
.
但是,它当然没有按预期工作。我不得不上下按住按压手势。如果一直向上滚动,然后尝试将其向下移动,它将平移响应以向下捕捉蓝色区域。内容将保持不变。
结论: 即使在从 ScrollView 中移除强锁定之后,实现完整的所需行为也相当复杂。现在我们必须将 onMoveShouldSetPanResponder
与 ScrollView 的 onScroll
结合起来,并处理初始按下事件,以获取增量 Y,以便我们最终可以正确移动父视图,一旦它到达顶部。
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow
*/
import React, { Component } from 'react';
import { Platform, StyleSheet, Text, View, Dimensions, PanResponder, Animated, ScrollView } from 'react-native';
const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
android:
'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});
export default class App extends Component {
constructor(props) {
super(props);
const {height, width} = Dimensions.get('window');
const initialPosition = {x: 0, y: height - 70}
const position = new Animated.ValueXY(initialPosition);
const parentResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: (e, gestureState) => {
return false
},
onStartShouldSetPanResponder: () => false,
onMoveShouldSetPanResponder: (e, gestureState) => {
if (this.state.toTop) {
return gestureState.dy > 6
} else {
return gestureState.dy < -6
}
},
onPanResponderTerminationRequest: () => false,
onPanResponderMove: (evt, gestureState) => {
let newy = gestureState.dy
if (this.state.toTop && newy < 0 ) return
if (this.state.toTop) {
position.setValue({x: 0, y: newy});
} else {
position.setValue({x: 0, y: initialPosition.y + newy});
}
},
onPanResponderRelease: (evt, gestureState) => {
if (this.state.toTop) {
if (gestureState.dy > 50) {
this.snapToBottom(initialPosition)
} else {
this.snapToTop()
}
} else {
if (gestureState.dy < -90) {
this.snapToTop()
} else {
this.snapToBottom(initialPosition)
}
}
},
});
this.offset = 0;
this.parentResponder = parentResponder;
this.state = { position, toTop: false };
}
snapToTop = () => {
Animated.timing(this.state.position, {
toValue: {x: 0, y: 0},
duration: 300,
}).start(() => {});
this.setState({ toTop: true })
}
snapToBottom = (initialPosition) => {
Animated.timing(this.state.position, {
toValue: initialPosition,
duration: 150,
}).start(() => {});
this.setState({ toTop: false })
}
hasReachedTop({layoutMeasurement, contentOffset, contentSize}){
return contentOffset.y == 0;
}
render() {
const {height} = Dimensions.get('window');
return (
<View style={styles.container}>
<Text style={styles.welcome}>Welcome to React Native!</Text>
<Text style={styles.instructions}>To get started, edit App.js</Text>
<Text style={styles.instructions}>{instructions}</Text>
<Animated.View style={[styles.draggable, { height }, this.state.position.getLayout()]} {...this.parentResponder.panHandlers}>
<Text style={styles.dragHandle}>=</Text>
<ScrollView style={styles.scroll}>
<Text style={{fontSize:44}}>Lorem Ipsum</Text>
<Text style={{fontSize:44}}>dolor sit amet</Text>
<Text style={{fontSize:44}}>consectetur adipiscing elit.</Text>
<Text style={{fontSize:44}}>In ut ullamcorper leo.</Text>
<Text style={{fontSize:44}}>Sed sed hendrerit nulla,</Text>
<Text style={{fontSize:44}}>sed ullamcorper nisi.</Text>
<Text style={{fontSize:44}}>Mauris nec eros luctus</Text>
<Text style={{fontSize:44}}>leo vulputate ullamcorper</Text>
<Text style={{fontSize:44}}>et commodo nulla.</Text>
<Text style={{fontSize:44}}>Nullam id turpis vitae</Text>
<Text style={{fontSize:44}}>risus aliquet dignissim</Text>
<Text style={{fontSize:44}}>at eget quam.</Text>
<Text style={{fontSize:44}}>Nulla facilisi.</Text>
<Text style={{fontSize:44}}>Vivamus luctus lacus</Text>
<Text style={{fontSize:44}}>eu efficitur mattis</Text>
</ScrollView>
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
draggable: {
position: 'absolute',
right: 0,
backgroundColor: 'skyblue',
alignItems: 'center'
},
dragHandle: {
fontSize: 22,
color: '#707070',
height: 60
},
scroll: {
paddingLeft: 10,
paddingRight: 10
}
});
我在具有平移功能的动画视图中有一个 ScrollView。
<Animated.View {...this.panResponder.panHandlers}>
<ScrollView>
...
</ScrollView>
<Animated.View>
这是我的屏幕示例视图:
用户应该能够向上滑动并且可拖动区域应该向上对齐,如下所示:
现在我的问题是滚动视图。我希望用户能够滚动里面的内容。
用户看完里面的内容后 并一直向上滚动(通过执行向下滑动动作)并尝试进一步滑动,可拖动区域应向下移动到其原始位置。
我尝试了各种方法,主要是关闭和开启ScrollView的滚动,防止其干扰平移。
我目前的方案并不理想。
我的主要问题是这两种方法:
onStartShouldSetPanResponder
onStartShouldSetPanResponderCapture
不确定我的假设是否正确,但这些方法决定了 View 是否应该捕获触摸事件。我要么允许平移,要么让 ScrollView 捕获事件。
我的问题是我需要以某种方式知道用户打算在其他 pan 处理程序启动之前执行的操作。但是在用户向下或向上移动之前我无法知道。要知道方向,我需要将事件传递给 onPanResponderMove 处理程序。
所以本质上,我需要在知道用户滑动的方向之前决定是否允许拖动我的视图。目前这是不可能的。
希望我在这里遗漏了一些简单的东西。
编辑: 发现一个类似的问题(没有答案): Drag up a ScrollView then continue scroll in React Native
也许,这和你的问题一样
https://github.com/rome2rio/react-native-touch-through-view
我认为更好的叉子
https://github.com/simonhoss/react-native-touch-through-view/issues/5
我认为您可以创建一个 bottomsheetlayout 来满足您的要求。或者您可以使用 iOS 中的操作表。考虑下面的库。可能对你有帮助
https://github.com/cesardeazevedo/react-native-bottom-sheet-behavior
或
显然是 Native 层的问题。
https://github.com/facebook/react-native/issues/9545#issuecomment-245014488
I found that onterminationrequest not being triggerred is caused by Native Layer.
Modify react-native\ReactAndroid\src\main\java\com\facebook\react\views\scroll\ReactScrollView.java , comment Line
NativeGestureUtil.notifyNativeGestureStarted(this, ev);
and then build from the source, you will see your PanResponder outside ScrollView takes the control as expected now.
PS:我还不能从源代码构建。从源代码构建显然比我想象的要难得多。
编辑 1:
是的,它起作用了。我从 node_modules 中删除了 react-native 文件夹,然后 git 将 react-native 存储库直接克隆到 node_modules
中。并检查到版本 0.59.1
。然后,按照 this 说明进行操作。
对于此示例,我不必将任何 PanReponder 或 Responder 设置为 ScrollView
.
但是,它当然没有按预期工作。我不得不上下按住按压手势。如果一直向上滚动,然后尝试将其向下移动,它将平移响应以向下捕捉蓝色区域。内容将保持不变。
结论: 即使在从 ScrollView 中移除强锁定之后,实现完整的所需行为也相当复杂。现在我们必须将 onMoveShouldSetPanResponder
与 ScrollView 的 onScroll
结合起来,并处理初始按下事件,以获取增量 Y,以便我们最终可以正确移动父视图,一旦它到达顶部。
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow
*/
import React, { Component } from 'react';
import { Platform, StyleSheet, Text, View, Dimensions, PanResponder, Animated, ScrollView } from 'react-native';
const instructions = Platform.select({
ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
android:
'Double tap R on your keyboard to reload,\n' +
'Shake or press menu button for dev menu',
});
export default class App extends Component {
constructor(props) {
super(props);
const {height, width} = Dimensions.get('window');
const initialPosition = {x: 0, y: height - 70}
const position = new Animated.ValueXY(initialPosition);
const parentResponder = PanResponder.create({
onMoveShouldSetPanResponderCapture: (e, gestureState) => {
return false
},
onStartShouldSetPanResponder: () => false,
onMoveShouldSetPanResponder: (e, gestureState) => {
if (this.state.toTop) {
return gestureState.dy > 6
} else {
return gestureState.dy < -6
}
},
onPanResponderTerminationRequest: () => false,
onPanResponderMove: (evt, gestureState) => {
let newy = gestureState.dy
if (this.state.toTop && newy < 0 ) return
if (this.state.toTop) {
position.setValue({x: 0, y: newy});
} else {
position.setValue({x: 0, y: initialPosition.y + newy});
}
},
onPanResponderRelease: (evt, gestureState) => {
if (this.state.toTop) {
if (gestureState.dy > 50) {
this.snapToBottom(initialPosition)
} else {
this.snapToTop()
}
} else {
if (gestureState.dy < -90) {
this.snapToTop()
} else {
this.snapToBottom(initialPosition)
}
}
},
});
this.offset = 0;
this.parentResponder = parentResponder;
this.state = { position, toTop: false };
}
snapToTop = () => {
Animated.timing(this.state.position, {
toValue: {x: 0, y: 0},
duration: 300,
}).start(() => {});
this.setState({ toTop: true })
}
snapToBottom = (initialPosition) => {
Animated.timing(this.state.position, {
toValue: initialPosition,
duration: 150,
}).start(() => {});
this.setState({ toTop: false })
}
hasReachedTop({layoutMeasurement, contentOffset, contentSize}){
return contentOffset.y == 0;
}
render() {
const {height} = Dimensions.get('window');
return (
<View style={styles.container}>
<Text style={styles.welcome}>Welcome to React Native!</Text>
<Text style={styles.instructions}>To get started, edit App.js</Text>
<Text style={styles.instructions}>{instructions}</Text>
<Animated.View style={[styles.draggable, { height }, this.state.position.getLayout()]} {...this.parentResponder.panHandlers}>
<Text style={styles.dragHandle}>=</Text>
<ScrollView style={styles.scroll}>
<Text style={{fontSize:44}}>Lorem Ipsum</Text>
<Text style={{fontSize:44}}>dolor sit amet</Text>
<Text style={{fontSize:44}}>consectetur adipiscing elit.</Text>
<Text style={{fontSize:44}}>In ut ullamcorper leo.</Text>
<Text style={{fontSize:44}}>Sed sed hendrerit nulla,</Text>
<Text style={{fontSize:44}}>sed ullamcorper nisi.</Text>
<Text style={{fontSize:44}}>Mauris nec eros luctus</Text>
<Text style={{fontSize:44}}>leo vulputate ullamcorper</Text>
<Text style={{fontSize:44}}>et commodo nulla.</Text>
<Text style={{fontSize:44}}>Nullam id turpis vitae</Text>
<Text style={{fontSize:44}}>risus aliquet dignissim</Text>
<Text style={{fontSize:44}}>at eget quam.</Text>
<Text style={{fontSize:44}}>Nulla facilisi.</Text>
<Text style={{fontSize:44}}>Vivamus luctus lacus</Text>
<Text style={{fontSize:44}}>eu efficitur mattis</Text>
</ScrollView>
</Animated.View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
draggable: {
position: 'absolute',
right: 0,
backgroundColor: 'skyblue',
alignItems: 'center'
},
dragHandle: {
fontSize: 22,
color: '#707070',
height: 60
},
scroll: {
paddingLeft: 10,
paddingRight: 10
}
});