美文网首页React Native学习React NativeiOS备忘录
React Native支持任意组件实现下拉刷新功能,并且可以自

React Native支持任意组件实现下拉刷新功能,并且可以自

作者: 这真不是玩笑 | 来源:发表于2017-06-22 16:46 被阅读5284次

    1.背景

    无论是 Androi 还是 ios,下拉刷新都是一个很有必要也很重要的功能。那么在 RN(以下用 RN 表示 React Native )之中,我们该如何实现下拉刷新功能呢?RN 官方提供了一个用于 ScrollView , ListView 等带有滑动功能组件的下拉刷新组件 RefreshControl。查看 RefreshControl 相关源码可以发现,其实它是对原生下拉刷新组件的一个封装,好处是使用方便快捷。但缺点也很明显,就是它不可以进行自定义下拉刷新头部,并且只能使用与 ScrollView,ListView 这种带有滚动功能的组件之中。那么我们该如何去解决这两个问题呢?
    先看下最终实现的效果,这里借助了 ScrollableTabView

    ios.gif
    android.gif

    2.实现原理分析

    对于下拉刷新功能,其实它的原理很简单。就是对要操作的组件进行 y 轴方向的位置进行判断。当滚动到顶部的时候,此时如果下拉的话,那么就进行下拉刷新的操作,如果上拉的话,那么就进行原本组件的滚动操作。基于这个原理,找了一些第三方实现的框架,基本上实现方式都是通过 ScrollView,ListView 等的 onScroll 方法进行监听回调。然后设置 Enable 属性来控制其是否可以滚动。但在使用的过程中有两个问题,一个是 onScroll 回调的频率不够,很多时候在滚动到了顶部的时候不能正确回调数值。另外一个问题就是 Enable 属性的问题,当在修改 Enable 数值的时候,当前的手势操作会停止。具体反映到 UI 上的效果就是,完成一次下拉刷新之后,第一次向上滚动的效果不能触发。那么,能不能有其他的方式去实现 RN 上的下拉刷新呢?

    3.实现过程

    3.1 判断组件的滚动位置

    在上面的原理分析中,一个重点就是判断要操作的组件的滚动位置,那么改如何去判断呢?在这里我们对 RN 的 View,ScrollView,ListView,FlatList 进行了相关的判断,不过要注意的是,FlatList 是 RN0.43 版本之后才出现的,所以如果你使用的 RN 版本小于 0.43 的话,那么你就要删除掉该下拉刷新框架关于 FlatList 的部分。
    我们来看下如何进行相关的判断。

     onShouldSetPanResponder = (e, gesture) => {
            let y = 0
            if (this.scroll instanceof ListView) { //ListView下的判断
                y = this.scroll.scrollProperties.offset;
            } else if (this.scroll instanceof FlatList) {//FlatList下的判断
                y = this.scroll.getScrollMetrics().offset  //这个方法需要自己去源码里面添加
            }
            //根据y的值来判断是否到达顶部
            this.state.atTop = (y <= 0)
            if (this.state.atTop && index.isDownGesture(gesture.dx, gesture.dy) && this.props.refreshable) {
                this.lastY = this.state.pullPan.y._value;
                return true;
            }
            return false;
        }
    

    首先对于普通的 View,由于它没有滚动属性,所以它默认处于顶部。而对于 ListView 来说,通过查找它的源码,发现它有个 scrollProperties 属性,里面包含了一些滚动的属性值,而 scrollProperties.offset 就是表示横向或者纵向的滚动值。而对于 FlatList 而言,它并没相关的属性。但是发现 VirtualizedList 中存在如下属性,而 FlatList 是对 VirtualizedList 的一个封装

     _scrollMetrics = {
            visibleLength: 0, contentLength: 0, offset: 0, dt: 10, velocity: 0, timestamp: 0,
        };
    

    那么很容易想到自己添加方法去获取。那么在
    FlatList(node_modules/react-native/Libraries/Lists/FlatList.js) 添加如下方法

    getScrollMetrics = () => {
        return this._listRef.getScrollMetrics()
    }
    

    同时在 VirtualizedList(node_modules/react-native/Libraries/Lists/VirtualizedList.js) 添加如下方法

    getScrollMetrics = () => {
        return this._scrollMetrics
     }
    

    另外,对于 ScrollView 而言,并没有找到相关滚动位置的属性,所以在这里用 ListView 配合 ScrollView 来使用,将 ScrollView 作为
    ListView 的一个子控件

    //ScrollView 暂时没有找到比较好的方法去判断时候滚动到顶部,
    //所以这里用ListView配合ScrollView进行使用
    export default  class PullScrollView extends Pullable {
        getScrollable=()=> {
            return (
                <ListView
                    ref={(c) => {this.scroll = c;}}
                    renderRow={this.renderRow}
                    dataSource={new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}).cloneWithRows([])}
                    enableEmptySections={true}
                    renderHeader={this._renderHeader}/>
            );
        }
    
        renderRow = (rowData, sectionID, rowID, highlightRow) => {
            return <View/>
        }
    
        _renderHeader = () => {
            return (
                <ScrollView
                    scrollEnabled={false}>
                    {this.props.children}
                </ScrollView>
            )
        }
    }
    
    

    那么当要操作的组件滚动到顶部的时候,此时下拉就是下拉刷新操作,而上拉就实现原本的操作逻辑

    3.2 组件位置的布局控制

    下拉刷新的滚动方式一般有两种,一种是内容跟随下拉头部一起下拉滚动,一种是内容固定不动,只有下拉头部在滚动。在这里用isContentScroll属性来进行选择判断

    render() {
            return (
                <View style={styles.wrap} {...this.panResponder.panHandlers} onLayout={this.onLayout}>
                    {this.props.isContentScroll ?
                        <View pointerEvents='box-none'>
                            <Animated.View style={[this.state.pullPan.getLayout()]}>
                                {this.renderTopIndicator()}
                                <View ref={(c) => {this.scrollContainer = c;}}
                                      style={{width: this.state.width, height: this.state.height}}>
                                    {this.getScrollable()}
                                </View>
                            </Animated.View>
                        </View> :
                        <View>
                            <View ref={(c) => {this.scrollContainer = c;}}
                                  style={{width: this.state.width, height: this.state.height}}>
                                {this.getScrollable()}
                            </View>
                            <View pointerEvents='box-none'
                                  style={{position: 'absolute', left: 0, right: 0, top: 0}}>
                                <Animated.View style={[this.state.pullPan.getLayout()]}>
                                    {this.renderTopIndicator()}
                                </Animated.View>
                            </View>
                        </View>}
                </View>
            );
        }
    

    从里面可以看到一个方法 this.getScrollable() , 这个就是我们要进行下拉刷新的内容,这个方法类似我们在 java 中的抽象方法,是一定要实现的,并且操作的内容的要指定 ref 为 this.scroll,举个例子

    export default class PullView extends Pullable {
    
        getScrollable = () => {
            return (
                <View ref={(c) => {this.scroll = c;}}
                    {...this.props}>
                    {this.props.children}
                </View>
            );
        }
    }
    

    3.3 添加默认刷新头部

    这里我们添加个默认的下拉刷新头部,用于当不添加下拉刷新头部时候的默认的显示

    defaultTopIndicatorRender = () => {
            return (
                <View style={{flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: index.defaultTopIndicatorHeight}}>
                    <ActivityIndicator size="small" color="gray" style={{marginRight: 5}}/>
                    <Text ref={(c) => {
                        this.txtPulling = c;
                    }} style={styles.hide}>{index.pulling}</Text>
                    <Text ref={(c) => {
                        this.txtPullok = c;
                    }} style={styles.hide}>{index.pullok}</Text>
                    <Text ref={(c) => {
                        this.txtPullrelease = c;
                    }} style={styles.hide}>{index.pullrelease}</Text>
                </View>
            );
        }
    

    效果就是上面的 gif 中除了 View 的 tab 的展示效果,同时需要根据下拉的状态来进行头部效果的切换

     if (this.pullSatte == "pulling") {
                    this.txtPulling && this.txtPulling.setNativeProps({style: styles.show});
                    this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
                    this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
                } else if (this.pullSatte == "pullok") {
                    this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
                    this.txtPullok && this.txtPullok.setNativeProps({style: styles.show});
                    this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
                } else if (this.pullSatte == "pullrelease") {
                    this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
                    this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
                    this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.show});
                }
    const styles = StyleSheet.create({
        wrap: {
            flex: 1,
            flexGrow: 1,
            zIndex: -999,
        },
        hide: {
            position: 'absolute',
            left: 10000,
            backgroundColor: 'transparent'
        },
        show: {
            position: 'relative',
            left: 0,
            backgroundColor: 'transparent'
        }
    });
    

    这里借助 setNativeProps 方法来代替 setStat e的使用,减少 render 的次数

    3.4 下拉刷新手势控制

    在下拉刷新之中,手势的控制是必不可少的一环,至于如何为组件添加手势,大家可以看下 RN 官网上的介绍

    this.panResponder = PanResponder.create({
                onStartShouldSetPanResponder: this.onShouldSetPanResponder,
                onStartShouldSetPanResponderCapture: this.onShouldSetPanResponder,
                onMoveShouldSetPanResponder: this.onShouldSetPanResponder,
                onMoveShouldSetPanResponderCapture: this.onShouldSetPanResponder,
                onPanResponderTerminationRequest: (evt, gestureState) => false, //这个很重要,这边不放权
                onPanResponderMove: this.onPanResponderMove,
                onPanResponderRelease: this.onPanResponderRelease,
                onPanResponderTerminate: this.onPanResponderRelease,
            });
    

    这里比较重要的一点就是 onPanResponderTerminationRequest (有其他组件请求使用手势),这个时候不能将手势控制交出去

    onShouldSetPanResponder = (e, gesture) => {
            let y = 0
            if (this.scroll instanceof ListView) { //ListView下的判断
                y = this.scroll.scrollProperties.offset;
            } else if (this.scroll instanceof FlatList) {//FlatList下的判断
                y = this.scroll.getScrollMetrics().offset  //这个方法需要自己去源码里面添加
            }
            //根据y的值来判断是否到达顶部
            this.state.atTop = (y <= 0)
            if (this.state.atTop && index.isDownGesture(gesture.dx, gesture.dy) && this.props.refreshable) {
                this.lastY = this.state.pullPan.y._value;
                return true;
            }
            return false;
        }
    

    onShouldSetPanResponder方法主要是对当前是否进行下拉操作进行判断。下拉的前提是内容滚动到顶部,下拉手势并且该内容需要下拉刷新操作( refreshable 属性)

    onPanResponderMove = (e, gesture) => {
            if (index.isDownGesture(gesture.dx, gesture.dy) && this.props.refreshable) { //下拉
                this.state.pullPan.setValue({x: this.defaultXY.x, y: this.lastY + gesture.dy / 2});
                this.onPullStateChange(gesture.dy)
            }
        }
     //下拉的时候根据高度进行对应的操作
        onPullStateChange = (moveHeight) => {
            //因为返回的moveHeight单位是px,所以要将this.topIndicatorHeight转化为px进行计算
            let topHeight = index.dip2px(this.topIndicatorHeight)
            if (moveHeight > 0 && moveHeight < topHeight) { //此时是下拉没有到位的状态
                this.pullSatte = "pulling"
            } else if (moveHeight >= topHeight) { //下拉刷新到位
                this.pullSatte = "pullok"
            } else { //下拉刷新释放,此时返回的值为-1
                this.pullSatte = "pullrelease"
            }
    
            if (this.props.topIndicatorRender == null) { //没有就自己来
                if (this.pullSatte == "pulling") {
                    this.txtPulling && this.txtPulling.setNativeProps({style: styles.show});
                    this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
                    this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
                } else if (this.pullSatte == "pullok") {
                    this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
                    this.txtPullok && this.txtPullok.setNativeProps({style: styles.show});
                    this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.hide});
                } else if (this.pullSatte == "pullrelease") {
                    this.txtPulling && this.txtPulling.setNativeProps({style: styles.hide});
                    this.txtPullok && this.txtPullok.setNativeProps({style: styles.hide});
                    this.txtPullrelease && this.txtPullrelease.setNativeProps({style: styles.show});
                }
            }
            //告诉外界是否要锁住
            this.props.onPushing && this.props.onPushing(this.pullSatte != "pullrelease")
            //进行状态和下拉距离的回调
            this.props.onPullStateChangeHeight && this.props.onPullStateChangeHeight(
                this.pullSatte == "pulling", this.pullSatte == "pullok",
                this.pullSatte == "pullrelease", moveHeight)
        }
    

    onPanResponderMove 方法中主要是对下拉时候头部组件 UI 进行判断,这里有三个状态的判断以及下拉距离的回调

     onPanResponderRelease = (e, gesture) => {
            if (this.pullSatte == 'pulling') { //没有下拉到位
                this.resetDefaultXYHandler(); //重置状态
            } else if (this.pullSatte == 'pullok') { //已经下拉到位了
                //传入-1,表示此时进行的是释放刷新的操作
                this.onPullStateChange(-1)
                //进行下拉刷新的回调
                this.props.onPullRelease && this.props.onPullRelease();
                //重置刷新的头部到初始位置
                Animated.timing(this.state.pullPan, {
                    toValue: {x: 0, y: 0},
                    easing: Easing.linear,
                    duration: this.duration
                }).start();
            }
        }
     //重置刷新的操作
        resetDefaultXYHandler = () => {
            Animated.timing(this.state.pullPan, {
                toValue: this.defaultXY,
                easing: Easing.linear,
                duration: this.duration
            }).start(() => {
                //ui要进行刷新
                this.onPullStateChange(-1)
            });
        }
    

    onPanResponderRelease 方法中主要是下拉刷新完成或者下拉刷新中断时候对头部 UI 的一个重置,并且有相关的回调操作

    4.属性和方法介绍

    4.1 属性

    Porp Type Optional Default Description
    refreshable bool yes true 是否需要下拉刷新功能
    isContentScroll bool yes false 在下拉的时候内容时候要一起跟着滚动
    onPullRelease func yes 刷新的回调
    topIndicatorRender func yes 下拉刷新头部的样式,当它为空的时候就使用默认的
    topIndicatorHeight number yes 下拉刷新头部的高度,当topIndicatorRender不为空的时候要设置正确的topIndicatorHeight
    onPullStateChangeHeight func yes 下拉时候的回调,主要是刷新的状态的下拉的距离
    onPushing func yes 下拉时候的回调,告诉外界此时是否在下拉刷新

    4.2 方法

    startRefresh() : 手动调用下拉刷新功能
    finishRefresh() : 结束下拉刷新

    5.最后

    该组件已经发布到 npm 仓库,使用的时候只需要 npm install react-native-rk-pull-to-refresh --save 就可以了,同时需要 react-native link react-native-rk-pull-to-refresh,它的使用Demo已经上传Github了:https://github.com/hzl123456/react-native-rk-pull-to-refresh
    另外:在使用过程中不要设置内容组件 Bounce 相关的属性为 false ,例如:ScrollView 的 bounces 属性( ios 特有)

    6.更新与2018年1月9日

    在使用的过程中,发现在 Android 中使用的过程中经常会出现下拉无法触发下拉刷新的问题,所以 Android 的下拉刷新采用原生组件封装的形式。对 android-Ultra-Pull-To-Refresh 进行封装。调用主要如下

    'use strict';
    import React from 'react';
    import RefreshLayout from '../view/RefreshLayout'
    import RefreshHeader from '../view/RefreshHeader'
    import PullRoot from './PullRoot'
    import * as index from './info';
    
    export default class Pullable extends PullRoot {
    
        constructor(props) {
            super(props);
            this.pullState = 'pulling'; //pulling,pullok,pullrelease
            this.topIndicatorHeight = this.props.topIndicatorHeight ? this.props.topIndicatorHeight : index.defaultTopIndicatorHeight;
        }
    
        render() {
            return (
                <RefreshLayout
                    {...this.props}
                    style={{flex: 1}}
                    ref={(c) => this.refresh = c}>
    
                    <RefreshHeader
                        style={{flex: 1, height: this.topIndicatorHeight}}
                        viewHeight={index.dip2px(this.topIndicatorHeight)}
                        onPushingState={(e) => this.onPushingState(e)}>
                        {this.renderTopIndicator()}
                    </RefreshHeader>
    
                    {this.getScrollable()}
                </RefreshLayout>
            )
        }
    
    
        onPushingState = (event) => {
            let moveHeight = event.nativeEvent.moveHeight
            let state = event.nativeEvent.state
            //因为返回的moveHeight单位是px,所以要将this.topIndicatorHeight转化为px进行计算
            let topHeight = index.dip2px(this.topIndicatorHeight)
            if (moveHeight > 0 && moveHeight < topHeight) { //此时是下拉没有到位的状态
                this.pullState = "pulling"
            } else if (moveHeight >= topHeight) { //下拉刷新到位
                this.pullState = "pullok"
            } else { //下拉刷新释放,此时返回的值为-1
                this.pullState = "pullrelease"
            }
            //此时处于刷新中的状态
            if (state == 3) {
                this.pullState = "pullrelease"
            }
            //默认的设置
            this.defaultTopSetting()
            //告诉外界是否要锁住
            this.props.onPushing && this.props.onPushing(this.pullState != "pullrelease")
            //进行状态和下拉距离的回调
            this.props.onPullStateChangeHeight && this.props.onPullStateChangeHeight(this.pullState, moveHeight)
        }
    
        finishRefresh = () => {
            this.refresh && this.refresh.finishRefresh()
        }
    
        startRefresh = () => {
            this.refresh && this.refresh.startRefresh()
        }
    }
    

    同时修改了主动调用下拉刷新的的方法为 startRefresh() , 结束刷新的方法为 finishRefresh() , 其他的使用方式和方法没有修改

    7.更新与2018年5月14日

    由于 React Native 版本的更新,移除了 React.PropTypes ,更新了 PropTypes 的引入方式,改动如下(基于 RN 0.55.4 版本):
    1.使用 import PropTypes from 'prop-types' 引入 PropTypes
    2.修改 FlatList 滑动距离的判断,这样你就不需要再修改源码了

    let y = 0
    if (this.scroll instanceof ListView) { //ListView下的判断
         y = this.scroll.scrollProperties.offset;
    } else if (this.scroll instanceof FlatList) {//FlatList下的判断
        y = this.scroll._listRef._getScrollMetrics().offset
    }
    

    相关文章

      网友评论

      • 偏小风:PullFlatList一下拉就闪退,请问源代码怎么改 没写清楚
        偏小风:@林志河 但是我只要一滑动就闪退了
        这真不是玩笑:最新版本的不需要修改源码了,文档里面有写
      • 蓝精灵112:view下拉刷新有啥限制 我的报错,error:only 2children
        这真不是玩笑:你直接使用pullview的话没啥限制啊
      • 4d9b5da3ee5c:D:\iot\iotdata-app\iotdata\android\app\src\main\java\com\iotdata\MainApplication.java:6: 错误: 程序包com.hzl.pulltorefresh不存在
        import com.hzl.pulltorefresh.RefreshReactPackage;
        ^
        D:\iot\iotdata-app\iotdata\android\app\src\main\java\com\iotdata\MainApplication.java:38: 错误: 找不到符号
        new RefreshReactPackage(),
        ^
        符号: 类 RefreshReactPackage
        2 个错误
        :app:compileDebugJavaWithJavac FAILED
        请问一下这是为什么,我检查了一下包明明也存在。并没有名字之类的错误
      • 蓝精灵112:lideMacBook-Pro:react-native-rk-pull-to-refresh-master lanjingling$ yarn install
        yarn install v1.3.2
        [1/4] 🔍 Resolving packages...
        [2/4] 🚚 Fetching packages...
        error An unexpected error occurred: "https://registry.yarnpkg.com/react/-/react-16.3.1.tgz: ETIMEDOUT".
        info If you think this is a bug, please open a bug report with the information provided in "/Users/li/Downloads/react-native-rk-pull-to-refresh-master/yarn-error.log".
        info Visit https://yarnpkg.com/en/docs/cli/install for documentation about this command.
        lideMacBook-Pro:react-native-rk-pull-to-refresh-master lanjingling$
        蓝精灵112:@蓝精灵112 先用npm install 后来又用了yarn install,demo已经可以运行了
        蓝精灵112:用 npm 也会报错
        蓝精灵112:什么情况
      • 浮岳牵云:hi 楼主 上拉加载 实现了吗? 与下拉一样的效果。找了很多上拉的都是添加footer 这种做法略显尴尬。
      • 小芹菜嘿嘿:大神你好,在FlatView 上加了刷新后,如果那个页面的导航条不是系统的,是自己写的View的时候,那个刷新头就显示在View 上面,如果换系统真导航,就挡住了,这么咋搞啊!!
        这真不是玩笑:@小芹菜嘿嘿 这种一般是ios平台overflow属性的原因,,,在设置那个View的属性为style={{overflow:'hidden'}}
        小芹菜嘿嘿:是PullFlatList 这个组件哦!!
      • 蓝精灵112:界面 转圈和list个占一半
        蓝精灵112:@请叫我百米冲刺 界面结构是scrollView有包裹了3个FlatList,需要刷新的是list,用react Native的刷新组件,导致安卓(手势冲突,很难刷新),我用了你的组件后,list只占半个屏幕
        这真不是玩笑:我的demo里就有scrollview的用法,并没有你说的问题啊
      • 哪有什么只是罢了:楼主改一下代码里的Proptype.xxx吧,新版本的RN都废弃了,每次更新都要改掉这个代码,谢谢了
        这真不是玩笑:@哪有什么只是罢了 npm install xxx进行导入的话,并不会改到其他的库啊,至少我用了这么久没遇到过
        哪有什么只是罢了:@请叫我百米冲刺 我每次导入其他库的时候,其他第三方库里改过的代码都会被重置,所以我都要重新改一遍,这是怎么回事啊,楼主,求解答,谢谢了!
        这真不是玩笑:基于 RN 0.55.4 版本,修改 PropTypes 的引入方式,修改 FlatList 滑动距离判断(这样就无需修改源码了),
      • Cheriez:好厉害~ 给楼主点赞,楼主可以做个iOS端封装了MJRefresh组件的下拉刷新么
      • 鹏雨燕:你好,这个方法在IOS很ok,但是在安卓下拉稍快就会有拉不下来的情况,很影响体验,是否有更新?@请叫我百米冲刺
        这真不是玩笑:@鹏雨燕 把原生的下拉刷新组件进行封装调用就行了,封装一个下拉刷新头部,类似这样
        <RefreshView
        ref={(c) => this.refreshView = c}
        style={this.props.rootStyle}
        refreshable={this.props.refreshable}
        onRefrshing={() => this.onRefresh()}>
        <FlatList
        enableEmptySections={true}
        keyExtractor={(item, index) => index + ""}
        //加载更多需要
        onEndReachedThreshold={5}
        onEndReached={(event) => this.onEndReached(event)}
        ListFooterComponent={() => this.renderFooter()}
        {...this.props}/>
        </RefreshView>
        鹏雨燕:@请叫我百米冲刺 感谢回复,原生封装下拉组件?原理是什么呢,是只是一个下拉的组件还是在list上做修改
        这真不是玩笑:目前来看,,,还是原生封装下拉刷新组件体验上最靠谱了
      • 迷迭象:sectionlist能支持吗?我试了下有问题
      • ce74fc5b39d1:使用flatlist, 官方自带的属性可以使用对吧, 就把标签换成你那个使用就OK了吗《PullFlatList /》
        这真不是玩笑:是的,但是需要修改FlatList和VirtualizedList的源码
      • 羽纱:好像很厉害的样子:clap:

      本文标题:React Native支持任意组件实现下拉刷新功能,并且可以自

      本文链接:https://www.haomeiwen.com/subject/woooqxtx.html