乐趣区

关于react-native:React-Native-实现自定义下拉刷新组件

本文作者:李磊

背景

Web 利用如果要更新列表数据,个别会抉择点击左上角刷新按钮,或应用快捷键 Ctrl+F5,进行页面资源和数据的全量更新。如果页面提供了刷新按钮或是翻页按钮,也能够点击只做数据更新。

但挪动客户端屏幕寸土寸金,无论是加上一个刷新按钮,还是配合越来越少的手机按键来做刷新操作,都不是非常便捷的计划。

于是,在这方寸之间,各种各样的滑动计划和手势计划来触发事件,成了挪动客户端的广泛趋势。在刷新数据方面,挪动端最罕用的计划就是下拉刷新的机制。

什么是下拉刷新?

下拉刷新的机制最早是由 Loren Brichter 在 Tweetie 2 中实现。Tweetie 是 Twitter 的第三方客户端,起初被 Twitter 收买,Loren Brichter 也成为 Twitter 员工(现已来到)。

Loren Brichter 在 2010 年 4 月 8 日为下拉刷新申请了专利,并取得受权 United States Patent: 8448084。但他很违心看到这个机制被其余 app 采纳,也已经说过申请是防御性的。

咱们看下专利爱护范畴最大的主权项是:

  • 在一种安排中,显示蕴含内容项的滚动列表;
  • 能够承受与滚动命令相关联的输出;
  • 依据滚动命令,显示一个滚动刷新的触发器;
  • 基于滚动命令,确定滚动刷新的触发器被激活后,刷新滚动列表中的内容。

简略来说,下拉加载的机制蕴含三个状态:

  • “下拉更新”:展现用户下拉可扩大的操作。
  • “松开更新”:提醒用户下拉操作的临界点。
  • “数据更新动画”:手势开释,揭示用户数据正在更新。

在那之后,很多以 news feed 为主的挪动客户端都相继采纳了这个设计。

React Native 反对下拉刷新么?

React Native 提供了 RefreshControl 组件,能够用在 ScrollView 或 FlatList 外部,为其增加下拉刷新的性能。

RefreshControl 外部实现是别离封装了 iOS 环境下的 UIRefreshControl 和安卓环境下的 AndroidSwipeRefreshLayout,两个都是挪动端的原生组件。

因为适配的原生计划不同,RefreshControl 不反对自定义,只反对一些简略的参数批改,如:刷新指示器色彩、刷新指示器下方字体。并且已有参数还受不同平台的限度。

最常见的需要会要求下拉加载指示器有本人特色的 loading 动画,个别的需求方还会加上操作的文字说明和上次加载的工夫。只反对批改色彩的 RefreshControl 必定是无奈满足的。

那想要自定义下拉刷新要怎么做呢?

解决方案 1

ScrollView 是官网提供的一个封装了平台 ScrollView(滚动视图)的组件,罕用于显示滚动区域。同时还集成了触摸的“手势响应者”零碎。

手势响应零碎用来判断用户的一次触摸操作的实在用意是什么。通常用户的一次触摸须要通过几个阶段能力判断。比方开始是点击,之后变成了滑动。随着持续时间的不同,这些操作会转化。

另外,手势响应零碎也能够提供给其余组件,能够使组件在不关怀父组件或子组件的前提下自行处理触摸交互。PanResponder 类提供了一个对触摸响应零碎的可预测的包装。它能够将多点触摸操作协调成一个手势。它使得一个单点触摸能够承受更多的触摸操作,也能够用于辨认简略的多点触摸手势。

它在原生事件外提供了一个新的 gestureState 对象:

onPanResponderMove: (nativeEvent, gestureState) => {}

nativeEvent 原生事件对象蕴含以下字段:

  • changedTouches – 在上一次事件之后,所有发生变化的触摸事件的数组汇合(即上一次事件后,所有挪动过的触摸点)
  • identifier – 触摸点的 ID
  • locationX – 触摸点绝对于父元素的横坐标
  • locationY – 触摸点绝对于父元素的纵坐标
  • pageX – 触摸点绝对于根元素的横坐标
  • pageY – 触摸点绝对于根元素的纵坐标
  • target – 触摸点所在的元素 ID
  • timestamp – 触摸事件的工夫戳,可用于挪动速度的计算
  • touches – 以后屏幕上的所有触摸点的汇合

gestureState 对象为了描述手势操作,有如下的字段:

  • stateID – 触摸状态的 ID。在屏幕上有至多一个触摸点的状况下,这个 ID 会始终无效。
  • moveX – 最近一次挪动时的屏幕横坐标
  • moveY – 最近一次挪动时的屏幕纵坐标
  • x0 – 当响应器产生时的屏幕坐标
  • y0 – 当响应器产生时的屏幕坐标
  • dx – 从触摸操作开始时的累计横向途程
  • dy – 从触摸操作开始时的累计纵向途程
  • vx – 以后的横向挪动速度
  • vy – 以后的纵向挪动速度
  • numberActiveTouches – 以后在屏幕上的无效触摸点的数量

能够看下 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} />
  );
},

联合下面状态剖析,看到 onPanResponderMoveonPanResponderRelease 这两个参数,根本是能够满足下拉刷新机制的操作流程的。

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>

expo pulltorefresh1

通过试用,发现这个计划有以下几个致命性问题:

  1. 因为下拉过程是通过触摸响应零碎经前端反馈给原生视图的,大量的数据通讯和页面重绘会导致页面的卡顿,在页面数据量较大时会更加显著;
  2. 上滑和下拉的切换时通过 ScrollView 的 Enable 的属性管制的,这样会造成手势操作的中断;
  3. 手势滑动过程短少阻尼函数,体现得不如原生下拉刷新天然;

另外还有 ScrollView 的滑动和模仿的下拉过程滑动配合不够默契的问题。

解决方案 2

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 给出)

expo pulltorefresh2

(demo 倡议在挪动设施查看,Web 端适配可尝试将 onScrollBeginDrag onScrollEndDrag 更换为 onTouchStart onTouchEnd

总结

本文次要介绍了在 React Native 开发过程中,下拉刷新组件的技术调研和实现过程。Expo demo 蕴含了两个计划的次要实现逻辑,读者可依据本身业务需要做定制,有问题欢送沟通。

参考链接

  • 下拉刷新是哪个设计师想进去的?
  • United States Patent: 8448084
  • 「下拉刷新」被申请专利爱护之后,为什么还有如此多的利用应用它?
  • React Native 中文网 /RefreshControl
  • GitHub facebook/React Native/RefreshControl
  • React Native 自定义下拉刷新组件
  • React Native 中文网 /panresponder
  • react-native-ultimate-listview

本文公布自 网易云音乐前端团队,可自在转载,转载请在题目表明转载并在显著地位保留出处。咱们始终在招人,如果你恰好筹备换工作,又恰好喜爱云音乐,那就 退出咱们!

退出移动版