本文做者:李磊
Web 应用若是要更新列表数据,通常会选择点击左上角刷新按钮,或使用快捷键 Ctrl+F5,进行页面资源和数据的全量更新。若是页面提供了刷新按钮或是翻页按钮,也能够点击只作数据更新。html
但移动客户端屏幕寸土寸金,不管是加上一个刷新按钮,仍是配合愈来愈少的手机按键来作刷新操做,都不是十分便捷的方案。前端
因而,在这方寸之间,各类各样的滑动方案和手势方案来触发事件,成了移动客户端的广泛趋势。在刷新数据方面,移动端最经常使用的方案就是下拉刷新的机制。react
下拉刷新的机制最先是由 Loren Brichter 在 Tweetie 2 中实现。Tweetie 是 Twitter 的第三方客户端,后来被 Twitter 收购,Loren Brichter 也成为 Twitter 员工(现已离开)。android
Loren Brichter 在 2010 年 4 月 8 日为下拉刷新申请了专利,并得到受权United States Patent: 8448084。但他很愿意看到这个机制被其余 app 采用,也曾经说过申请是防护性的。git
咱们看下专利保护范围最大的主权项是:github
简单来讲,下拉加载的机制包含三个状态:react-native
在那以后,不少以 news feed 为主的移动客户端都相继采用了这个设计。数组
React Native 提供了 RefreshControl 组件,能够用在 ScrollView 或 FlatList 内部,为其添加下拉刷新的功能。app
RefreshControl 内部实现是分别封装了 iOS 环境下的 UIRefreshControl
和安卓环境下的 AndroidSwipeRefreshLayout
,两个都是移动端的原生组件。ide
因为适配的原生方案不一样,RefreshControl 不支持自定义,只支持一些简单的参数修改,如:刷新指示器颜色、刷新指示器下方字体。而且已有参数还受不一样平台的限制。
最多见的需求会要求下拉加载指示器有本身特点的 loading 动画,个别的需求方还会加上操做的文字说明和上次加载的时间。只支持修改颜色的 RefreshControl 确定是没法知足的。
那想要自定义下拉刷新要怎么作呢?
ScrollView 是官方提供的一个封装了平台 ScrollView (滚动视图)的组件,经常使用于显示滚动区域。同时还集成了触摸的“手势响应者”系统。
手势响应系统用来判断用户的一次触摸操做的真实意图是什么。一般用户的一次触摸须要通过几个阶段才能判断。好比开始是点击,以后变成了滑动。随着持续时间的不一样,这些操做会转化。
另外,手势响应系统也能够提供给其余组件,可使组件在不关心父组件或子组件的前提下自行处理触摸交互。PanResponder
类提供了一个对触摸响应系统的可预测的包装。它能够将多点触摸操做协调成一个手势。它使得一个单点触摸能够接受更多的触摸操做,也能够用于识别简单的多点触摸手势。
它在原生事件外提供了一个新的 gestureState
对象:
onPanResponderMove: (nativeEvent, gestureState) => {}
nativeEvent 原生事件对象包含如下字段:
gestureState 对象为了描绘手势操做,有以下的字段:
能够看下 PanResponder
的基本用法:
componentWillMount: function() { this._panResponder = PanResponder.create({ // 要求成为响应者: onStartShouldSetPanResponder: (evt, gestureState) => true, onStartShouldSetPanResponderCapture: (evt, gestureState) => true, onMoveShouldSetPanResponder: (evt, gestureState) => true, onMoveShouldSetPanResponderCapture: (evt, gestureState) => true, onPanResponderGrant: (evt, gestureState) => { // 开始手势操做。给用户一些视觉反馈,让他们知道发生了什么事情! // gestureState.{x,y} 如今会被设置为0 }, onPanResponderMove: (evt, gestureState) => { // 最近一次的移动距离为gestureState.move{X,Y} // 从成为响应者开始时的累计手势移动距离为gestureState.d{x,y} }, onPanResponderTerminationRequest: (evt, gestureState) => true, onPanResponderRelease: (evt, gestureState) => { // 用户放开了全部的触摸点,且此时视图已经成为了响应者。 // 通常来讲这意味着一个手势操做已经成功完成。 }, onPanResponderTerminate: (evt, gestureState) => { // 另外一个组件已经成为了新的响应者,因此当前手势将被取消。 }, onShouldBlockNativeResponder: (evt, gestureState) => { // 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者 // 默认返回true。目前暂时只支持android。 return true; }, }); }, render: function() { return ( <View {...this._panResponder.panHandlers} /> ); },
结合上面状态分析,看到 onPanResponderMove
和 onPanResponderRelease
这两个参数,基本是能够知足下拉刷新机制的操做流程的。
onPanResponderMove
处理滑动过程。
onPanResponderMove(event, gestureState) { // 最近一次的移动距离为 gestureState.move{X,Y} // 从成为响应者开始时的累计手势移动距离为 gestureState.d{x,y} if (gestureState.dy >= 0) { if (gestureState.dy < 120) { this.state.containerTop.setValue(gestureState.dy); } } else { this.state.containerTop.setValue(0); if (this.scrollRef) { if (typeof this.scrollRef.scrollToOffset === 'function') { // inner is FlatList this.scrollRef.scrollToOffset({ offset: -gestureState.dy, animated: true, }); } else if(typeof this.scrollRef.scrollTo === 'function') { // inner is ScrollView this.scrollRef.scrollTo({ y: -gestureState.dy, animated: true, }); } } } }
onPanResponderRelease
处理释放时的操做。
onPanResponderRelease(event, gestureState) { // 用户放开了全部的触摸点,且此时视图已经成为了响应者。 // 通常来讲这意味着一个手势操做已经成功完成。 // 判断是否达到了触发刷新的条件 const threshold = this.props.refreshTriggerHeight || this.props.headerHeight; if (this.containerTranslateY >= threshold) { // 触发刷新 this.props.onRefresh(); } else { // 没到刷新的位置,回退到顶部 this._resetContainerPosition(); } // 检查 scrollEnabled 开关 this._checkScroll(); }
剩下的就是如何区分容器的滑动,和下拉刷新的触发。
当 ScrollView 的 scrollEnabled
属性设置为 false 时,能够禁止用户滚动。所以,能够将 ScrollView 做为内容容器。当滚动到容器顶部的时候,关闭 ScrollView 的 scrollEnabled
属性,经过设置 Animated.View 的 translateY
,显示自定义加载器。
<Animated.View style={[{ flex: 1, transform: [{ translateY: this.state.containerTop }] }]}> {child} </Animated.View>
通过试用,发现这个方案有如下几个致命性问题:
另外还有 ScrollView 的滑动和模拟的下拉过程滑动配合不够默契的问题。
ScrollView 在 iOS 设备下有个特性,若是内容范围比滚动视图自己大,在到达内容末尾的时候,能够弹性地拉动一截。能够将加载指示器放在页面的上边缘,弹性滚动时露出。这样既不须要利用到手势影响渲染速度,又能够将滚动和下拉过程很好的融合。
所以,只要处理好滚动操做的各阶段事件就好。
onScroll = (event) => { // console.log('onScroll()'); const { y } = event.nativeEvent.contentOffset this._offsetY = y if (this._dragFlag) { if (!this._isRefreshing) { const height = this.props.refreshViewHeight if (y <= -height) { this.setState({ refreshStatus: RefreshStatus.releaseToRefresh, refreshTitle: this.props.refreshableTitleRelease }) } else { this.setState({ refreshStatus: RefreshStatus.pullToRefresh, refreshTitle: this.props.refreshableTitlePull }) } } } if (this.props.onScroll) { this.props.onScroll(event) } } onScrollBeginDrag = (event) => { // console.log('onScrollBeginDrag()'); this._dragFlag = true this._offsetY = event.nativeEvent.contentOffset.y if (this.props.onScrollBeginDrag) { this.props.onScrollBeginDrag(event) } } onScrollEndDrag = (event) => { // console.log('onScrollEndDrag()', y); this._dragFlag = false const { y } = event.nativeEvent.contentOffset this._offsetY = y const height = this.props.refreshViewHeight if (!this._isRefreshing) { if (this.state.refreshStatus === RefreshStatus.releaseToRefresh) { this._isRefreshing = true this.setState({ refreshStatus: RefreshStatus.refreshing, refreshTitle: this.props.refreshableTitleRefreshing }) this._scrollview.scrollTo({ x: 0, y: -height, animated: true }); this.props.onRefresh() } } else if (y <= 0) { this._scrollview.scrollTo({ x: 0, y: -height, animated: true }) } if (this.props.onScrollEndDrag) { this.props.onScrollEndDrag(event) } }
惟一美中不足的就是,iOS 支持超过内容的滑动,安卓不支持,须要单独适配下安卓。
将加载指示器放在页面内,经过 scrollTo
方法控制页面距顶部距离,来模拟下拉空间。(iOS 和安卓方案已在 expo pulltorefresh2 给出)
(demo 建议在移动设备查看,Web 端适配可尝试将 onScrollBeginDrag onScrollEndDrag
更换为 onTouchStart onTouchEnd
)
本文主要介绍了在 React Native 开发过程当中,下拉刷新组件的技术调研和实现过程。 Expo demo 包含了两个方案的主要实现逻辑,读者可根据自身业务需求作定制,有问题欢迎沟通。
本文发布自 网易云音乐前端团队,可自由转载,转载请在标题标明转载并在显著位置保留出处。咱们一直在招人,若是你刚好准备换工做,又刚好喜欢云音乐,那就 加入咱们!