早期做RN的时候是基于0.44版本的,后面做需求迭代版本时候因人手不够也没去主动升级,一直拖到近期才升级到0.55+,且还未完成兼容测试。但目前因为有个小需求需要捉急上线,于是就还在老分支上开发,然后打包上线(用的Xcode 8.3)。
接着坑来了,从 2018 年 7 月开始,所有新的 iOS 应用程序和更新到应用商店的更新都必须使用 iOS 11 SDK 构建。
新分支RN虽然升级了但还未测试完毕,肯定是不敢上线的,所以只能选择用Xcode9去build但还保持RN版本不动。
最后使用了Xcode9运行旧分支,调整了一些小问题后,项目是能成功Run起来的,但发现了一个bug:
如上图:
页面布局最顶层元素是ScrollView且加了下拉刷新RefreshControl组件。现象就是当下拉距离过短时或者下拉刷新完毕后,都是没自动回弹、复位置顶,y坐标没有变成0。
因为这是个过渡版本,所以先治标不治本地解决下问题,以便立即上线版本。
原因很明显就是ScrollView 没有scrollTo({y: 0})
,那么该怎么解决呢?很明显我们应该强制让其滚动置顶。
首先我们要考虑到2个触发条件:
1.下拉到某个位置,手指抬起离开屏幕的时候;
2.惯性滚动停止的时候。
1)针对如何捕捉用户手指抬起离开屏幕这个动作,我们可以借助RN中PanResponder
手势响应系统
一个View只要实现了正确的协商方法,就可以成为触摸事件的响应者。我们通过两个方法去“询问”一个View是否愿意成为响应者:
View.props.onStartShouldSetResponder: (evt) => true, - 在用户开始触摸的时候(手指刚刚接触屏幕的瞬间),是否愿意成为响应者?
View.props.onMoveShouldSetResponder: (evt) => true, - 如果View不是响应者,那么在每一个触摸点开始移动(没有停下也没有离开屏幕)时再询问一次:是否愿意响应触摸交互呢?
如果View返回true,并开始尝试成为响应者,那么会触发下列事件之一:
View.props.onResponderGrant: (evt) => {} - View现在要开始响应触摸事件了。这也是需要做高亮的时候,使用户知道他到底点到了哪里。
View.props.onResponderReject: (evt) => {} - 响应者现在“另有其人”而且暂时不会“放权”,请另作安排。
如果View已经开始响应触摸事件了,那么下列这些处理函数会被一一调用:
View.props.onResponderMove: (evt) => {} - 用户正在屏幕上移动手指时(没有停下也没有离开屏幕)。
View.props.onResponderRelease: (evt) => {} - 触摸操作结束时触发,比如"touchUp"(手指抬起离开屏幕)。
View.props.onResponderTerminationRequest: (evt) => true - 有其他组件请求接替响应者,当前的View是否“放权”?返回true的话则释放响应者权力。
View.props.onResponderTerminate: (evt) => {} - 响应者权力已经交出。这可能是由于其他View通过onResponderTerminationRequest请求的,也可能是由操作系统强制夺权(比如iOS上的控制中心或是通知中心)。
高级使用请参考PanResponder类可以将多点触摸操作协调成一个手势。它使得一个单点触摸可以接受更多的触摸操作,也可以用于识别简单的多点触摸手势。
官方代码示例
2)针对捕捉惯性滚动停止时,我们可以理解为是滚动动画结束时,那么就可以利用oScrollView的属性方法 onMomentumScrollEnd?: function #
scrollview组件介绍
知道思路后,接下来就是写代码了,考虑复用性所以封装成一个组件:
import React, { Component } from 'react'
import { View, StyleSheet, PanResponder, ScrollView } from 'react-native'
import PropTypes from 'prop-types'
/**
* 组件
*
* @export
* @class componentName
* @extends {Component}
*/
export class MyScrollView extends Component<Props> {
/**
* 组件属性类型
*
* @static
*/
static propTypes = {}
/**
* 组件默认属性
*
* @static
*/
static defaultProps = {}
constructor(props: any) {
super(props)
var _scrollView = null
var _startY = 0
this.lastScrollTop = 0
this.isResetIng = false
this.state = {
onMoveShouldSetPanResponder: false,
onMoveShouldSetPanResponderCapture: false
}
this.timeOut = null
}
/**
* 复位头部
*/
resetHeader() {
if (this.isResetIng) {
return
}
_startY = this.lastScrollTop
if (Math.abs(_startY) > 20) {
_scrollView.scrollTo({ y: _startY * 0.8, animated: false })
this.timeOut = setTimeout(() => {
_scrollView &&
_scrollView.scrollTo({ y: _startY * 0.4, animated: false })
this.timeOut = setTimeout(() => {
_scrollView && _scrollView.scrollTo({ y: 0, animated: false })
this.isResetIng = false
}, 50)
}, 50)
} else {
_scrollView.scrollTo({ y: 0, animated: false })
this.isResetIng = false
}
}
_onScroll(event) {
this.props.onScroll && this.props.onScroll(event)
this.lastScrollTop = event.nativeEvent.contentOffset.y
//仅在下拉时候 捕获事件处理、响应手势系统,否则拦截了子View中的Touchable事件
if (this.lastScrollTop < 0) {
this.setState({
onMoveShouldSetPanResponder: true,
onMoveShouldSetPanResponderCapture: true
})
} else {
//其他时候 恢复正常
this.setState({
onMoveShouldSetPanResponder: false,
onMoveShouldSetPanResponderCapture: false
})
}
}
_onMomentumScrollEnd(event) {
this.lastScrollTop = event.nativeEvent.contentOffset.y
//仅处在超越上边界位置 且 非刷新中
if (this.lastScrollTop < 0 && !this.props.isRefreshing) {
this.resetHeader()
}
}
componentWillMount() {
this._panResponder = PanResponder.create({
// 要求成为响应者:
onStartShouldSetPanResponder: (evt, gestureState) => false,
onStartShouldSetPanResponderCapture: (evt, gestureState) => false,
onMoveShouldSetPanResponder: (evt, gestureState) =>
this.state.onMoveShouldSetPanResponder,
onMoveShouldSetPanResponderCapture: (evt, gestureState) =>
this.state.onMoveShouldSetPanResponderCapture,
onPanResponderGrant: (evt, gestureState) => {
// 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情!
// gestureState.{x,y}0 现在会被设置为0
},
onPanResponderMove: (evt, gestureState) => {
// 最近一次的移动距离为gestureState.move{X,Y}
// 从成为响应者开始时的累计手势移动距离为gestureState.d{x,y}
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
// 用户放开了所有的触摸点,且此时视图已经成为了响应者。
// 一般来说这意味着一个手势操作已经成功完成。
//仅处在超越上边界位置 且 非刷新中
if (this.lastScrollTop < 0 && !this.props.isRefreshing) {
this.resetHeader()
}
},
onPanResponderTerminate: (evt, gestureState) => {
// 另一个组件已经成为了新的响应者,所以当前手势将被取消。
},
onShouldBlockNativeResponder: (evt, gestureState) => {
// 返回一个布尔值,决定当前组件是否应该阻止原生组件成为JS响应者
// 默认返回true。目前暂时只支持android。
return true
}
})
}
componentWillReceiveProps(newProps) {}
componentWillUnmount() {
this.timeOut && clearTimeout(this.timeOut)
}
/**
* 组件渲染
*
*/
render() {
const { style, ...others } = this.props
return (
<ScrollView
style={[style]}
{...others}
scrollsToTop={false}
ref={scrollView => {
_scrollView = scrollView
}}
scrollEventThrottle={60}
onScroll={this._onScroll.bind(this)}
onMomentumScrollEnd={this._onMomentumScrollEnd.bind(this)}
{...this._panResponder.panHandlers}
>
{this.props.children}
</ScrollView>
)
}
}
//样式设置,所有样式都需要在此变量中定义
const styles = StyleSheet.create({})
简单使用:
<MyScrollView isRefreshing={this.state.isRefreshing} ... >
...
</MyScrollView>
注:ScrollView ios属性 scrollsToTop bool #
当此值为true时,点击状态栏的时候视图会滚动到顶部。默认值为true。
点击顶部也会发生 没有正确回弹、复位 所以需要设置为false