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