RN下拉刷新:使用JavaScript实现

Posted 人生如梦91

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RN下拉刷新:使用JavaScript实现相关的知识,希望对你有一定的参考价值。

文章目录

最近一直在做React-Native相关的事情,需要实现一个下拉刷新,android集成原生很容易,但ios似乎比较麻烦,于是搅尽脑汁之后,最后根据Github上的下拉刷新库,但这个库并没有实现SectionList的下拉刷新,于是根据该库的代码自己的下拉刷新,为防止以后有需要时忘记或有迷惑,故记录在此。

效果展示

实现步骤

UI布局

首先,是整个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>
        );
    

可以看到,以上有四层布局,

  • 最外层的View用于添加滑动手势,
  • 第二层View的作用并不大,主要是通过onLayout方法获取到宽高,
  • 第三层的Animated.View主要是用于移动,因为最外的两层是不移动的,只处理相关手势,真正移动的是第三层,
  • 第四层Animated.View用于调整SectionList的偏移量,可以看到,第三层的flex并不是1,因为在移动的时候,如果高度只占据剩余高度的话

根据上述代码可以看到,第三层的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以上是关于RN下拉刷新:使用JavaScript实现的主要内容,如果未能解决你的问题,请参考以下文章

RN下拉刷新:使用JavaScript实现

RN下拉刷新:使用JavaScript实现

移动端touch实现下拉刷新

移动端实现下拉刷新

Android自定义控件--下拉刷新的实现

RN-第三方之react-native-pull 下拉刷新上拉加载