The Problem
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.
My Experiment
The idea
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:
- I create a hidden
View
(whose width is set to 0 by default) with anActivityIndicator
in it, and insert this View as theListHeaderComponent
into theVirtualizedList
- I listen to the
VirtualizedList
'sonScroll
event and keep saving the latest scroll position - When then scroll position is nearly 0, and the user pulls right (event captured gesture through
PanResponder
), then I animate the width of the hidden View with ActivityIndicator to simulate the RefreshControl behavior
The result
However, 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
?
The Experimental Code
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;
Temporary Workarounds (Not so perfect)
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
toonMoveShouldSetPanResponder
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!)
Perfect Solutions
Waiting for your idea...