I am developing a horizontal scrollable list which always scroll one entire screen (i.e. one "page") on each swipe. When it is on the first page and the user pulls to right again, I need to refresh the entire list.
As for a vertical ScrollView
, a standard RefreshControl
would appear if a user pulls down when the view is at its topmost position. But for the horizontal version, there is no standard solution.
I have tried to add a PanResponder
to capture the touching and moving events. Note that I am using a VirtualizedList
in my application. The basic logic is that:
View
(whose width is set to 0 by default) with an ActivityIndicator
in it, and insert this View as the ListHeaderComponent
into the VirtualizedList
VirtualizedList
's onScroll
event and keep saving the latest scroll positionPanResponder
), then I animate the width of the hidden View with ActivityIndicator to simulate the RefreshControl behaviorHowever, the PanResponder
does not properly capture the desired event. Most of the time, when I pull right at the beginning of the VirtualizedList
(on an Android device), only the standard hitting edge animation appears (a gradient blue layer shows up).
By adding some logs, I found that onPanResponderGrant
can occasionally be triggered, but none of onPanResponderMove
, onPanResponderRelease
, onPanResponderTerminate
or onPanResponderTerminationRequest
is ever fired.
When onPanResponderGrant
is triggered, the hidden ActivityIndicator
occasionally appears paritally. The "pull to right" effect only shows up shortly and always stops instantly with only a very narrow part of the hidden View appears.
I guess that the underlying ScrollView
steals the event and prevents my code from responding. But even if I set onPanResponderTerminationRequest
to always reture false
still won't help. Even if I directly subsitute the VirtualizedList
with a ScrollView
also yield the same result.
Does anyone have any idea how to properly capture the touch and move event when ScrollView scrolls beyond is top? Or is there another viable way to implement a horizontal version of a RefreshControl
?
import React from 'react'
import {
Text, View, ActivityIndicator, ScrollView, VirtualizedList,
Dimensions, PanResponder, Animated, Easing
} from 'react-native'
let DATA = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20];
class Test extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
refreshIndicatorWidth: new Animated.Value(0),
};
this.onScroll = evt => this._scrollPos = evt.nativeEvent.contentOffset.x;
this.onPullToRefresh = dist => Animated.timing(this.state.refreshIndicatorWidth, {
toValue: dist,
duration: 0,
easing: Easing.linear,
}).start();
this.resetRefreshStatus = () => {
Animated.timing(this.state.refreshIndicatorWidth, {
toValue: 0,
}).start();
};
const shouldRespondPan = ({dx, dy}) => (this._scrollPos||0) < 50 && dx > 10 && Math.abs(dx) > Math.abs(dy);
this._panResponder = PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => {
return shouldRespondPan(gestureState)
},
onMoveShouldSetPanResponderCapture: (evt, gestureState) => {
return shouldRespondPan(gestureState)
},
onPanResponderGrant: (evt, gestureState) => {
console.warn('----pull-Grant: ' + JSON.stringify(gestureState));
},
onPanResponderMove: (evt, gestureState) => {
console.warn('----pull-Move: ' + JSON.stringify(gestureState));
this.onPullToRefresh(gestureState.dx)
},
onPanResponderTerminationRequest: () => {
console.warn('----pull-TerminationRequest: ' + JSON.stringify(gestureState));
return false
},
onPanResponderRelease: (evt, gestureState) => {
console.warn('----pull-Release: ' + JSON.stringify(gestureState));
this.props.refreshMoodList();
},
onPanResponderTerminate: () => {
console.warn('----pull-Terminate: ' + JSON.stringify(gestureState));
this.resetRefreshStatus();
},
});
this.renderRow = this.renderRow.bind(this);
}
componentDidUpdate(prevProps, prevState, prevContext) {
console.warn('----updated: ' + JSON.stringify(this.props));
this.resetRefreshStatus();
}
renderRow({item, index}) {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center', width: Dimensions.get('window').width, height: '100%'}}>
<Text>{item}</Text>
</View>
);
}
render() {
/******* Test with a VirtualizedList *******/
return (
<VirtualizedList
{...this._panResponder.panHandlers}
data={DATA}
ListEmptyComponent={<View/>}
getItem={getItem}
getItemCount={getItemCount}
keyExtractor={keyExtractor}
renderItem={this.renderRow}
horizontal={true}
pagingEnabled={true}
showsHorizontalScrollIndicator={false}
ListHeaderComponent={(
<Animated.View style={{flex: 1, height: '100%', width: this.state.refreshIndicatorWidth, justifyContent: 'center', alignItems: 'center'}}>
<ActivityIndicator size="large"/>
</Animated.View>
)}
onScroll={this.onScroll}
/>
);
/******* Test with a ScrollView *******/
return (
<View style={{height:Dimensions.get('window').height}}>
<ScrollView
{...this._panResponder.panHandlers}
horizontal={true}
>
<View style={{width: Dimensions.get('window').width * 2, height: 100, backgroundColor: 'green'}}>
<Text>123</Text>
</View>
</ScrollView>
</View>
);
}
}
const getItem = (data, index) => data[index];
const getItemCount = data => data.length;
const keyExtractor = (item, index) => 'R' + index;
export default Test;
Fire the refresh event (as well as the animation) directly inside onPanResponderGrant
: This way, I'm still not able to track touch moves. So the scrolling pane cannot follow the touch point.
Move the operations in onPanResponderMove
to onMoveShouldSetPanResponder
with additional conditions (e.g. adding my own "grant" condition judgment): This way, I can track the touch movements, but since I haven't got the real "grant", I'm not able to capture the *Release
/*Terminate
/*End
event (i.e., I won't know when the touch event ends, therefore never actually know when to fire the refreshing event!)
Waiting for your idea...
You can use the contentOffset
to determine if a user has "pulled to refresh" on a horizontal list. The following should work on both a FlatList
and a ScrollView
:
onScroll={(e) => {
const xOffset = e.nativeEvent.contentOffset.x;
// logic goes here to handle a negative value
}}
scrollEventThrottle={16}
You can conditinally do something or set a flag when the xOffset hits a certain value. Hopefully that will get you started!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With