问题
I am trying to use the PanResponder on a View. The onStartShouldSetPanResponder
and onMoveShouldSetPanResponder
but onPanResponderMove
, onPanResponderGrant
and onPanResponderRelease
does not get triggered at all. My react and react native versions are:
"react": "^15.2.1",
"react-native": "^0.30.0",
Below is the code
'use strict'
import React from 'react'
const Icon = require('react-native-vector-icons/Ionicons')
let THUMB_URLS = require('../Statics/ListingsData.js')
let SidePanelComponent = require('./common/SidePanel.js')
let RecentSearches = require('./Views/RecentSearches/RecentSearches.js')
let TimerMixin = require('react-timer-mixin')
const Loader = require('./common/LoadingState.js')
import { getImageURL, getUserImageURL } from './_helpers/images'
const config = require('../config')
import GoogleAnalytics from 'react-native-google-analytics-bridge'
GoogleAnalytics.setTrackerId(config.google_analytics_id)
const windowSize = require('Dimensions').get('window')
const deviceWidth = windowSize.width
const deviceHeight = windowSize.height
import {
Image,
Text,
View,
TouchableOpacity,
TouchableWithoutFeedback,
ScrollView,
StyleSheet,
Platform,
Animated,
PanResponder
} from 'react-native'
let LISTINGS = []
const ListingsViewComponent = React.createClass({
mixins: [TimerMixin],
getInitialState: function () {
return {
listings: [],
dataSource: [],
showSearchIcon: false,
showSidePanel: false,
photo: {},
componentloading: true,
showHeartIcon: [],
startX: 0,
startY: 0,
showWishlistMenu: false,
wishlistCurrentY: 0,
showNewWishlistTextInput: false,
currentRowdata: {},
wishlistOptions: [],
showrecentsearches: false,
isDataLoading: true,
scrolling: false,
_listViewDirtyPressEnabled: true,
scrollAnimationEnd: false,
scrollStates: [],
goingtonextview: false,
heroImageContainerHeight: deviceWidth,
searchbar: new Animated.ValueXY()
}
},
_panListingsResponder: {},
componentWillMount: function () {
this._panListingsResponder = PanResponder.create({
onStartShouldSetPanResponder: (e, g) => {
this.setState({
startX: e.nativeEvent.pageX,
startY: e.nativeEvent.pageY
})
},
onStartShouldSetPanResponderCapture: (e, g) => {
},
onMoveShouldSetPanResponder: (e, g) => {
this.setState({
heroImageContainerHeight: deviceWidth - (e.nativeEvent.pageY - this.state.startY)
})
},
onMoveShouldSetPanResponderCapture: (e, g) => {},
onPanResponderGrant: (e, g) => {},
onPanResponderMove: (e, g) => {
this.setState({
heroImageContainerHeight: deviceWidth - (e.nativeEvent.pageY - this.state.startY)
})
},
onPanResponderTerminationRequest: (e, g) => {
console.log('onPanResponderTerminationRequest', e.nativeEvent)
return false
},
onPanResponderRelease: (e, g) => {
console.log('_onResponderRelease', e.nativeEvent)
},
onPanResponderTerminate: (e, g) => {
console.log('onPanResponderTerminate', e.nativeEvent)
},
onShouldBlockNativeResponder: (e, g) => true
})
let listingsendpoint = 'http://faithstay-staging.herokuapp.com/api/listings'
this.setState({
isDataLoading: true
})
fetch(listingsendpoint)
.then((response) => response.json())
.then((listingsData) => {
const listings = listingsData
LISTINGS = []
LISTINGS.push(THUMB_URLS[0])
LISTINGS.push(THUMB_URLS[1])
LISTINGS.push(THUMB_URLS[2])
listings.map((listing) => {
LISTINGS.push(listing)
})
this.setState({
isDataLoading: false,
listings: LISTINGS
})
})
.catch((error) => {
console.warn(error)
})
},
componentDidMount: function () {
GoogleAnalytics.trackScreenView('Faithstay-Listings-Page')
},
_showSidePanel: function () {
this.setState({
showSidePanel: true
})
},
_closeSidePanel: function () {
this.setState({
showSidePanel: false
})
},
_showRecentSearches: function () {
this.setState({
showrecentsearches: true
})
},
_closeRecentSearches: function () {
this.setState({
showrecentsearches: false
})
},
componentWillReceiveProps: function () {
this.setState({
goingtonextview: false
})
},
getSearchBarStyle: function () {
return [
styles.searchbar, {
top: this.state.heroImageContainerHeight
}
]
},
render: function () {
let sidePanelViewContainer
if (this.state.showSidePanel) {
sidePanelViewContainer = (<SidePanelComponent {...this.props} imageuri={this.state.photo} onClose={this._closeSidePanel} />)
}
let searchIconContainer = <Animated.View style={this.getSearchBarStyle()}>
<TouchableOpacity style={styles.searchBarInner} onPress={this._showRecentSearches}>
<Text style={styles.searchtext}>
{'Where do you want to go?'}
</Text>
<Icon
name={'ios-search'}
size={30}
color={'#cfcfcf'}
style={styles.searchicon}
/>
</TouchableOpacity>
</Animated.View>
if (!this.state.showrecentsearches) {
if (this.state.isDataLoading) {
return (<Loader />)
} else {
return (
<View style={styles.container} {...this._panListingsResponder.panHandlers}>
<View style={[styles.heroImageContainer, { height: this.state.heroImageContainerHeight }]}>
<Image source={{uri: 'https://faithstay-statics.imgix.net/images/homepage_carousel_4.jpg'}} style={[styles.heroImage, { height: this.state.heroImageContainerHeight }]} />
<View style={[styles.scrimLayer, { height: this.state.heroImageContainerHeight }]} />
<View style={styles.logoContainer}>
<Image source={require('../Statics/images/anchor_3x.png')} style={styles.logoImage} />
<Text style={styles.logoText}>{'FaithStay'}</Text>
</View>
<View style={styles.horizontalDivider} />
<View style={styles.betaVersionContainer}>
<Text style={styles.betaVersionText}>{'Beta Version'}</Text>
</View>
<View style={[styles.pageTitleContainer, {top: this.state.heroImageContainerHeight - 85}]}>
<Text style={styles.pageTitle}>{'Home'}</Text>
</View>
<View style={[styles.movableScrim, {backgroundColor: `rgba(0, 0, 0, ${(deviceWidth - this.state.heroImageContainerHeight) / deviceWidth})`}]} />
</View>
{searchIconContainer}
<ScrollView style={styles.listView}>
{this.getListingsView()}
</ScrollView>
{sidePanelViewContainer}
</View>
)
}
}
return (<RecentSearches {...this.props} closeRecentSearches={this._closeRecentSearches} />)
},
_gotoUserProfilePage: function (user) {
this.props.navigator.push({
id: 15,
passProps: {
user
}
})
},
getListingsView: function () {
let listings = this.state.listings
const listingsArray = []
listings.map((listing, i) => {
let currentlisting = listing
let type = currentlisting.type
if (type !== 'NOT_A_LISTING') {
let imgSource = {
uri: getImageURL(currentlisting.images[0])
}
let profileimg = {
uri: getUserImageURL(currentlisting.host)
}
let title = currentlisting.title
let reviews = '18'
let address_values = currentlisting.google_place.formatted_address ? currentlisting.google_place.formatted_address.split(',') : []
let listing_address = {}
if (address_values.length > 0) {
listing_address = {
country: address_values[address_values.length - 1].trim(),
state: address_values[address_values.length - 2].trim(),
city: address_values[address_values.length - 3].trim()
}
}
let city = listing_address.city + ', ' + listing_address.state
let baseprice = currentlisting.base_price ? '$' + currentlisting.base_price : '0'
listingsArray.push(<View>
<TouchableWithoutFeedback onPress={() => this._pressRow(currentlisting)}>
<View>
<View style={styles.row}>
<Image style={styles.thumb} source={imgSource} >
<View style={styles.priceconatiner}>
<Text style={styles.pricetext}>{baseprice}</Text>
</View>
</Image>
</View>
<TouchableOpacity style={styles.profileImgContainer} onPress={() => this._gotoUserProfilePage(currentlisting.host)}>
<Image style={styles.profileimg} source={profileimg} />
</TouchableOpacity>
<View style={styles.listingtextcontainer}>
<Text style={styles.listingtexttitle}>{title}</Text>
<Text style={styles.listingtexttdescription}>{'Entire Home' + ' - ' + reviews + ' Reviews' + ' - ' + city}</Text>
</View>
</View>
</TouchableWithoutFeedback>
</View>)
} else {
let listing_title = listing.title
let listing_description = listing.description
let imageuri = listing.image;
listingsArray.push(<View><TouchableWithoutFeedback onPress={() => this._pressNonListingRow(currentlisting)}>
<View>
<View style={styles.rowNotListing}>
<Image style={styles.thumbNotListing} source={{uri: imageuri}}>
<View style={styles.thumbNotListing, {position: 'absolute', left:0, top: 0, right:0, bottom:0, backgroundColor: 'rgba(0,0,0,0.2)'}} >
</View>
<View style={styles.thumbNotListingSubContainer}>
<Text style={styles.listingtitle_notlisting}>{listing_title}</Text>
<Text style={styles.listingdescription_notlisting}>{listing_description}</Text>
</View>
</Image>
</View>
</View>
</TouchableWithoutFeedback>
</View>)
}
})
return listingsArray
},
_pressRow: function (listing) {
this.props.navigator.push({
id: 4,
passProps: {
listingdata: listing
}
})
},
_pressNonListingRow: function (listing) {
this.props.navigator.push({
id: 9,
passProps: {
filterData: listing
}
})
}
})
const paddingHorizontal = 15
const paddingVertical = 10
const distanceBetweenIcons = (deviceWidth - 115) / 3
const statusBarHeight = (Platform.OS === 'ios') ? 20 : 0
const isAndroid = Platform.OS === 'android'
const styles = StyleSheet.create({
listView: {
height: deviceHeight - 70,
top: (Platform.OS === 'ios') ? 40 : 0,
left: 0
},
scrimLayer: {
position: 'absolute',
top: 0,
left: 0,
width: deviceWidth,
height: deviceWidth,
backgroundColor: 'rgba(0, 0, 0, 0.2)'
},
movableScrim: {
position: 'absolute',
top: 0,
left: 0,
width: deviceWidth,
height: deviceWidth
},
container: {
flex: 1,
paddingTop: statusBarHeight,
width: deviceWidth,
height: deviceHeight
},
row: {
flexDirection: 'row',
justifyContent: 'center',
backgroundColor: '#f5f5f5',
width: deviceWidth,
height: deviceHeight / 2
},
separator: {
height: 1,
backgroundColor: '#CCCCCC'
},
thumb: {
width: deviceWidth,
height: deviceHeight / 2 - 80
},
thumbNotListing: {
width: deviceWidth,
height: deviceHeight / 2,
justifyContent: 'center'
},
thumbNotListingSubContainer: {
alignSelf: 'center',
justifyContent: 'center'
},
listingtitle_notlisting: {
textAlign: 'center',
alignSelf: 'center',
fontSize: 24,
fontWeight: 'bold',
color: '#ffffff'
},
listingdescription_notlisting: {
textAlign: 'center',
alignSelf: 'center',
fontSize: 16,
marginTop: 10,
color: '#ffffff'
},
text: {
flex: 1,
},
tabbar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
width: deviceWidth,
height: 49,
backgroundColor: '#f5f5f5',
justifyContent: 'space-between',
borderTopWidth: 1,
borderTopColor: '#dce0e0'
},
searchbar: {
width: deviceWidth - 30,
height: 50,
left: 15,
top: deviceWidth - 5,
position: 'absolute',
justifyContent: 'center',
backgroundColor: '#f5f5f5',
shadowOpacity: 0.5
},
searchBarInner: {
width: deviceWidth - 30,
height: 50,
justifyContent: 'center',
backgroundColor: '#f5f5f5',
shadowOpacity: 0.5
},
searchonlyicon: {
width: 50,
height: 50,
borderRadius: 25,
left: 20,
top: 40,
position: 'absolute',
justifyContent: 'center',
backgroundColor: '#f5f5f5',
shadowOpacity: 0.5
},
searchtext: {
width: 160,
position: 'absolute',
fontSize: 15,
color: '#565a5c',
left: (deviceWidth - 30) / 2 - 80,
top: 15,
fontFamily: 'RobotoCondensed-Regular'
},
searchicon: {
width: 30,
height: 30,
position: 'absolute',
top: 8,
left: 12
},
homeicon: {
width: 30,
height: 30,
position: 'absolute',
top: paddingVertical - 2,
left: paddingHorizontal,
justifyContent: 'center',
},
hearticon: {
width: 40,
height: 30,
position: 'absolute',
top: paddingVertical,
justifyContent: 'center',
left: distanceBetweenIcons
},
emailicon: {
width: 45,
height: 30,
position: 'absolute',
top: paddingVertical,
justifyContent: 'center',
left: 2 * distanceBetweenIcons
},
bagicon: {
width: 35,
height: 20,
position: 'absolute',
top: paddingVertical + 6,
justifyContent: 'center',
left: 3 * distanceBetweenIcons
},
personicon: {
width: 30,
height: 30,
position: 'absolute',
top: paddingVertical,
justifyContent: 'center',
right: paddingHorizontal
},
priceconatiner: {
position: 'absolute',
top: deviceHeight / 2 - 150,
left: 0,
width: 60,
height: 40,
backgroundColor: 'rgba(60,63,64,0.9)',
justifyContent: 'center'
},
pricetext: {
fontSize: 20,
color: '#fff',
fontWeight: 'bold',
textAlign: 'center',
width: 60,
fontFamily: 'HelveticaNeue'
},
profileImgContainer: {
position: 'absolute',
top: deviceHeight / 2 - 108,
right: isAndroid ? 0 : 20, // NOTE: add to width, vs pushing it with position values
width: isAndroid ? 70 : 50, // NOTE: on android, the view must be as big as the image, otherwise the image will be cut off
height: 50,
paddingLeft: paddingHorizontal,
justifyContent: 'center'
},
profileimg: {
width: 50,
height: 50,
borderRadius: 25
},
listingtextcontainer: {
position: 'absolute',
top: deviceHeight / 2 - 70,
left: paddingHorizontal,
justifyContent: 'space-between',
height: 50
},
listingtexttitle: {
paddingTop: 5,
fontSize: 16,
fontFamily: 'HelveticaNeue',
color: '#565a5c',
fontWeight: 'bold'
},
listingtexttdescription: {
fontSize: 14,
fontFamily: 'HelveticaNeue',
color: '#82888a',
paddingBottom: 5
},
wishlistIcon: {
position: 'absolute',
right: 20,
top: 20
},
hearticonwishlist: {
width: 30,
height: 30
},
wishlistScrollView: {
position: 'absolute',
right: 20,
width: deviceWidth - 60,
height: 80,
backgroundColor: '#fff'
},
scrollRow: {
width: 180,
height: 40,
justifyContent: 'center',
padding: 5,
borderBottomWidth: 1,
borderBottomColor: '#f5f5f5'
},
wishlistScrollViewContainer: {
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
width: deviceWidth,
height: deviceHeight,
backgroundColor: 'rgba(255,255,255,0.1)'
},
touchableScrollViewContainer: {
width: deviceWidth,
height: deviceHeight,
position: 'absolute',
top: 0,
bottom: 0,
left: 0
},
fontWishlistScroller: {
color: '#565a5c',
fontSize: 14
},
rowNotListing: {
flexDirection: 'row',
justifyContent: 'center',
width: deviceWidth,
height: deviceHeight / 2
},
heroImageContainer: {
width: deviceWidth,
height: deviceWidth
},
heroImage: {
width: deviceWidth,
height: deviceWidth
},
logoContainer: {
width: 120,
position: 'absolute',
left: (deviceWidth / 2) - 60,
top: 19,
flexDirection: 'row',
justifyContent: 'center',
backgroundColor: 'transparent'
},
logoImage: {
width: 18,
height: 30,
top: 3
},
logoText: {
fontFamily: 'RobotoCondensed-Regular',
fontSize: 25,
fontWeight: '400',
textAlign: 'center',
color: '#fffff0',
marginLeft: 7.7
},
horizontalDivider: {
width: 32,
position: 'absolute',
left: deviceWidth / 2 - 16,
top: 59,
borderBottomWidth: 1,
borderColor: '#ffffff'
},
betaVersionContainer: {
width: 120,
position: 'absolute',
left: deviceWidth / 2 - 60,
top: 79,
justifyContent: 'center',
backgroundColor: 'transparent'
},
betaVersionText: {
fontFamily: 'RobotoCondensed-Regular',
fontSize: 14,
fontStyle: 'italic',
fontWeight: '300',
textAlign: 'center',
color: '#ffffff',
alignSelf: 'center'
},
pageTitleContainer: {
position: 'absolute',
top: deviceWidth - 85,
left: 20,
backgroundColor: 'transparent'
},
pageTitle: {
fontSize: 34,
fontFamily: 'RobotoCondensed-Bold',
color: '#ffffff'
}
})
module.exports = ListingsViewComponent
回答1:
I got it working properly by using onPanResponderEnd
instead of onPanResponderRelease
.
Also if we still want to use onPanResponderRelease
then we should allow termination request by:
onPanResponderTerminationRequest: () => true
回答2:
You need to make sure the following handlers return true
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
The only different between onStart...
and onMove...
is that the PanResponder will be created when you start rendering the component for onStart...
, it will be created (lazy) when user start tab or move for onMove
.
On android side, you may still find that onPanResponderRelease
will not be triggered, an issue reported here as well https://github.com/facebook/react-native/issues/9447
I ended up using onPanResponderTerminate
to handle this case. Hopefully you can get more insights about it.
来源:https://stackoverflow.com/questions/39187214/onpanresponderrelease-not-being-triggered