早期做RN的时候是基于0.44版本的,后面做需求迭代版本时候因人手不够也没去主动升级,一直拖到近期才升级到0.55+,且还未完成兼容测试。但目前因为有个小需求需要捉急上线,于是就还在老分支上开发,然后打包上线(用的Xcode 8.3)。

接着坑来了,从 2018 年 7 月开始,所有新的 iOS 应用程序和更新到应用商店的更新都必须使用 iOS 11 SDK 构建。

新分支RN虽然升级了但还未测试完毕,肯定是不敢上线的,所以只能选择用Xcode9去build但还保持RN版本不动。

最后使用了Xcode9运行旧分支,调整了一些小问题后,项目是能成功Run起来的,但发现了一个bug:

SmartRefreshLayout 如何设置下拉刷新背景色_ScrollView 下拉刷新 滚动复位


如上图:

页面布局最顶层元素是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