最近一直在做React-Native相关的事情,需要实现一个下拉刷新,Android集成原生很容易,但iOS似乎比较麻烦,于是搅尽脑汁之后,最后根据Github上的下拉刷新库,但这个库并没有实现SectionList的下拉刷新,于是根据该库的代码自己的下拉刷新,为防止以后有需要时忘记或有迷惑,故记录在此。
首先,是整个UI的布局,也许SectionList有方法只是我不知道,我们知道,在原生开发中,iOS的UITableView可以使用setContentOffset让列表停止在任意的地方,原生下拉刷新的实现也正是依赖于这个属性,通过监听contentOffset,改变对应的状态。但是我在RN里面找不到这样的方法,每次下拉之后只要一放手立马回弹。所以不能直接将下拉刷新控件添加到列表上。之前我也有过这种布局思路,但是不知道怎么拿到列表的偏移量,在发现了这个库以后,我知道了可以用SectionList里面的方法来获取到滚动偏移,其实还有另外一种方法,就是使用onScroll方法。
所以布局如下所示:
render() {
let refreshHeader = <View/>;
if (this.props.RefreshControl !== undefined) {
refreshHeader = this.props.RefreshControl;
} else if (this.props.onRefresh !== undefined) {
refreshHeader =
<MessageRefreshHeader ref={(ref) => this._refreshHeader = ref}
onRefresh={this.props.onRefresh} style={{transform: [{translateY: this.state.headerOffset}]}}/>;
}
let refreshFooter = <View/>;
if (this.props.LoadMoreControl !== undefined) {
refreshFooter = this.props.LoadMoreControl;
} else if (this.props.onLoadMore !== undefined) {
refreshFooter =
<MessageRefreshFooter ref={(ref) => this._refreshFooter = ref} onLoadMore={this.props.onLoadMore}/>;
}
return (
<View style={{flex: 1, flexGrow: 1}} {...this._panResponder.panHandlers}>
<View pointerEvents='box-none' style={{flex: 1}} onLayout={(e) => {
if(e.nativeEvent.layout.width !== this.state.width || e.nativeEvent.layout.height !== this.state.height) {
this.setState({
width: e.nativeEvent.layout.width,
height: e.nativeEvent.layout.height,
});
}
}}>
<Animated.View
style={[{...style.container},
{width: '100%', height: this.state.height + MessageConstant.refreshHeaderHeight, transform:[{translateY: this.state.translateY}]}]}>
{refreshHeader}
<Animated.View ref={(container) => this._scrollContainer = container} style={{flex: 1, transform: [{translateY: this.state.sectionOffset}]}}>
<SectionList
ref={sectionList => this._sectionList = sectionList}
initialNumToRender={3}
keyExtractor={(item, index) => item + index}
sections={this.props.sections}
renderItem={this.props.renderItem}
renderSectionHeader={this.props.renderSectionHeader}
stickySectionHeadersEnabled={this.props.stickySectionHeadersEnabled}
ItemSeparatorComponent={this.props.ItemSeparatorComponent}
scrollEnabled={true}
bounces={true}
onScroll={(e) => {
this._onScroll(e);
if (this.props.onScroll !== undefined) {
this.props.onScroll(e);
}
}}
onEndReachedThreshold={this.props.onEndReachedThreshold === undefined ? 1.0 : this.props.onEndReachedThreshold}
onEndReached={(e) => {
this._onEndReached(e);
if (this.props.onEndReached !== undefined) {
this.props.onEndReached(e);
}
}}
showsHorizontalScrollIndicator={this.props.showsHorizontalScrollIndicator}
showsVerticalScrollIndicator={this.props.showsVerticalScrollIndicator}
SectionSeparatorComponent={this.props.SectionSeparatorComponent}
ListFooterComponent={
<View style={{flex: 1, flexDirection: 'column'}}>
{this.props.ListFooterComponent}
{refreshFooter}
</View>
}
ListHeaderComponent={this.props.ListHeaderComponent}
ListEmptyComponent={this.props.ListEmptyComponent}
/>
</Animated.View>
</Animated.View>
</View>
</View>
);
}
可以看到,以上有四层布局,
根据上述代码可以看到,第三层的flex并不是1,因为如果flex设置为1的话,那么当整体往上移动的时候,下面将留出空白,所以高度设置为剩余高度+头部高度。这样就可以防止这个问题。
这个下拉刷新的实现原理是根据列表滚动的偏移量来决定是否启用滑动手势,当列表滚动到<= 0的位置的时候,便启用滑动手势,此时列表不可滚动。那么如何获取偏移量呢,根据github上那个库的代码,他使用了
this._flatList._listRef._getScrollMetrics().offset
来获取,确实在FlatList下可以获取到,但是SectionList的_listRef是undefined,拿不到这个值,最终一步一步追踪源代码才发现,SectionList正确的获取方法是
this._sectionList._wrapperListRef.getListRef()._getScrollMetrics().offset
中间多了一层_wrapperListRef。
手势处理使用PanResponder
手势什么时候该被启用?显而易见,当我们往下拉的时候,并且列表已经滚动到顶部的时候并且不处在刷新状态的时候,才能启用手势,否则不应启用,代码如下:
_isDownGesture(dx, dy) {
return (dy > 0 && dy > Math.abs(dx));
}
_onStartShouldSetPanResponder(evt, gestureState) {
if(gestureState.dy <= 0 || this._refreshHeader.state.isRefreshing) {
return;
}
this.lastY = this._sectionList._wrapperListRef.getListRef()._getScrollMetrics().offset;
if(this._yContentOffset <= 0 && this._isDownGesture(gestureState.dx, gestureState.dy)) {
return true;
}
return false;
}
手势启用之后,当手指滑动的时候,此时需要修改偏移量,使整个View能向下偏移,这样才能使下拉刷新显示出来,如下所示:
_onPanResponderMove(evt, gestureState) {
if(gestureState.dy <= 0 || this._refreshHeader.state.isRefreshing) {
return;
}
Animated.timing(
this.state.translateY,
{
toValue: -MessageConstant.refreshHeaderHeight + gestureState.dy,
duration: 10.0,
}
).start();
if(this._refreshHeader.onListScroll !== undefined) {
this._refreshHeader.onListScroll(gestureState.dy);
}
当手指释放的时候,需要根据当前的偏移距离来进行判断,如果滑动超过了指定高度,则进入到刷新状态,否则回到原状态
_onPanResponderRelease(evt, gestureState) {
if(this._refreshHeader && this._refreshHeader.state.isRefreshing) {
return ;
}
if(this._refreshHeader && this._refreshHeader.changeRefreshState !== undefined) {
let refresh = this._refreshHeader.changeRefreshState(gestureState.dy);
if(refresh === false) {
this._endHeaderRefreshAnimated();
}else {
Animated.timing(
this.state.translateY,
{
toValue: 0.0,
duration: 200.0,
}
).start();
}
}else {
this._endHeaderRefreshAnimated();
}
}
_endHeaderRefreshAnimated() {
Animated.timing(
this.state.translateY,
{
toValue: -MessageConstant.refreshHeaderHeight,
duration: 500.0,
}
).start();
Animated.timing(
this.state.headerOffset,
{
toValue: 0.0,
duration: 500.0,
}
).start();
Animated.timing(
this.state.sectionOffset,
{
toValue: 0.0,
duration: 500.0,
}
).start();
}
有时候不需要通过手动下拉刷新的方式自动触发下拉刷新,所以需要添加下拉刷新的方法,如下
beginRefresh() {
if(this._refreshHeader && this._refreshHeader.state.refreshState !== this._refreshHeader.RefreshState.REFRESHING) {
Animated.timing(
this.state.translateY,
{
toValue: 0.0,
duration: 200.0,
}
).start();
if(this._refreshHeader.changeRefreshState) {
this._refreshHeader.changeRefreshState(MessageConstant.refreshHeaderHeight);
}
}
}
endRefresh() {
if(this._refreshHeader !== undefined && this._refreshHeader.state.refreshState === this._refreshHeader.RefreshState.REFRESHING) {
setTimeout(() => {
this._endHeaderRefreshAnimated();
this._refreshHeader.endRefresh();
}, 3000);
}
if(this._refreshFooter !== undefined && this._refreshFooter.state.loadMoreState === this._refreshFooter.LoadMoreState.REFRESHING) {
return this._refreshFooter.endRefresh();
}
}
我们的下拉刷新是需要不断的更换图片的,所以一开始先确定好图片在每个位置的图片索引,
constructor(props) {
super(props);
this._imageIndex = 0;
this._pullImage = {index: 1, count: 7};
this._refreshImage = {index: 8, count: 28};
}
这个下拉刷新就是一个图片,所以布局也是十分简单
render() {
return (
<Animated.View
ref = {ref => this._pullRef = ref}
style={[{...style.container}, {...this.props.style}]} onLayout={(e) => {
}}>
<Animated.Image ref={image => this._imageRef = image} source={blackRefreshGif[this._imageIndex % blackRefreshGif.length]} resizeMode={'center'} style={style.refreshImage}/>
</Animated.View>
);
}
const style = StyleSheet.create({
container: {
alignItems: 'center',
justifyContent: 'center',
width: '100%',
zIndex: -9999,
backgroundColor: 'transparent',
},
refreshImage: {
width: 108.0,
height: 108.0,
},
});
重点在下拉时候的图片状态的修改,代码如下:
validImageIndex(index) {
let imgArr = this.props.refreshStyle === 'white' ? whiteRefreshGif : blackRefreshGif;
if(index < 0) {
return 0;
}else if(index >= imgArr.length) {
return imgArr.length - 1;
}
return index;
}
onListScroll(yOffset) {
this._clearInterval();
let refreshThreshold = this.props.refreshThresold === undefined ? 64.0 : this.props.refreshThresold;
let imgArr = this.props.refreshStyle === 'white' ? whiteRefreshGif : blackRefreshGif;
if(yOffset < refreshThreshold) {
this._imageRef.setNativeProps({
['source']: [resolveAssetSource(imgArr[0])],
});
this._imageIndex = this.validImageIndex(this._pullImage.index);
if(this.state.refreshState !== this.RefreshState.IDLE) {
this.setState({
isRefreshing: false,
refreshState: this.RefreshState.IDLE,
});
}
return ;
}else {
if(this.state.refreshState !== this.RefreshState.PULLING) {
this.setState({
isRefreshing: false,
refreshState: this.RefreshState.PULLING,
});
}
if(yOffset > refreshThreshold && yOffset < MessageConstant.refreshHeaderHeight && this._lastYOffset > yOffset) {
this._imageRef.setNativeProps({
['source']: [resolveAssetSource(imgArr[this.validImageIndex(this._imageIndex--)])],
});
}else {
if (this._imageIndex >= (this._pullImage.index + this._pullImage.count)) {
this._imageIndex = this.validImageIndex(this._pullImage.index + this._pullImage.count);
}
this._imageRef.setNativeProps({
['source']: [resolveAssetSource(imgArr[this.validImageIndex(this._imageIndex++)])],
});
}
}
this._lastYOffset = yOffset;
}
Image如果要频繁的修改图片,必须使用setNativeProps来修改source,传递一个图片数组,在Android下该字段为src,iOS下该字段为source, 要使用resolveAssetSource,必须在头部引入
let resolveAssetSource = require('react-native/Libraries/Image/resolveAssetSource');
代码说明如下:
当手指释放的时候,此时我们需要判断下拉刷新的状态,根据偏移量,如果小于阈值的时候,此时下拉刷新回到原位置。否则修改下拉刷新状态为刷新中,并且启动定时器,不断的更新图片,如下所示:
_addInterval(handler, timeout) {
this._clearInterval();
this._refreshTimer = setInterval(handler, timeout);
}
_clearInterval() {
this._refreshTimer && clearInterval(this._refreshTimer);
}
changeRefreshState(yOffset) {
let refreshThreshold = MessageConstant.refreshHeaderHeight;
let imgArr = this.props.refreshStyle === 'white' ? whiteRefreshGif : blackRefreshGif;
if(yOffset < refreshThreshold) {
return false;
}
if(this.state.refreshState !== this.RefreshState.REFRESHING) {
this.setState({
isRefreshing: true,
refreshState: this.RefreshState.REFRESHING,
});
}
this._imageIndex = this._refreshImage.index;
this._addInterval(() => {
if(this._imageIndex >= (this._refreshImage.index + this._refreshImage.count)) {
this._imageIndex = this._refreshImage.index + 18.0;
}
this._imageRef.setNativeProps({
['source']: [resolveAssetSource(imgArr[this._imageIndex++])],
});
}, 40);
if(this.props.onRefresh) {
this.props.onRefresh();
}
return true;
}
下拉刷新完成的时候,此时修改下拉刷新状态为IDLE,延迟一定时间后,恢复组件位置,同时从当前图片索引不断恢复图片至最开始的状态即可。
endRefresh() {
if(this.state.refreshState === this.RefreshState.REFRESHING) {
this.setState({
isRefreshing: false,
refreshState: this.RefreshState.IDLE,
});
}
let imgArr = this.props.refreshStyle === 'white' ? whiteRefreshGif : blackRefreshGif;
this._addInterval(() => {
if(this._imageIndex < 0) {
this._imageIndex = 0;
this._clearInterval();
}
this._imageRef.setNativeProps({
['source']: [resolveAssetSource(imgArr[this._imageIndex--])],
});
}, 40.0);
}
到这里,整个下拉刷新组件的封装就已经全部完成,但是依然有一个遗留的问题,就是当列表先上滑再往下滑的时候,无法出现下拉刷新,这是因为在列表滚动的时候无法启动手势,目前尚无确切的解决办法。看了一下第三方库,也没有办法解决这个问题,因此该问题目前只能暂时留下,待以后有解决方案的时候再进行更新记录。