React Native 自定义下拉刷新组件

6,904 阅读6分钟

React Native 自定义下拉刷新组件 PullToRefresh

针对猴急一些的同学,可以先在这个 Expo网站在线运行下demo看看效果

完整的代码,在 Github仓库

下拉刷新,是一个很常见的交互方式。React-Native(以下简称RN)内置的 FlatList 是支持下拉刷新组件的,通过设置 refreshControl 属性即可。通常我们不仅仅需要定制下拉组件,还需要在下拉过程中,下拉组件执行一些动画,比如在我们场景下,公司logo会随着下拉的幅度,不同的笔画还是显现出颜色。这就需要我们的下拉组件,知道当前下拉的幅度,以此来计算我们动画执行的进度。显然,RN官方的 refreshControl并不能满足我们的需求。

看到有两个已经存在的开源包 react-native-pull-refreshreact-native-ptr-control ,基本都有2年左右历史了,而且我也确实没看懂,为什么要用到 两个 ScrollView 嵌套来实现。

直观上来看,我应该只需要有一个 ScrollView 就可以了,我监听下拉距离,重新render自定义的下拉组件。嗯,按照这个思路,尝试撸一个试试。

一步步实现自定义下拉

首先,我们提供的是一个容器(命名为 PullToRefresh 吧 ),内部是用户通过 children 传进来的 FlatList,这样也方便用户修改,在需要自定义下拉刷新的场景下,用我们这个容器把已经存在的 FlatList 包起来就可以了,改动也挺小。当然,因为是自定义下拉刷新header,肯定还需要用户把自定义的下拉刷新header组件传进来,就命名为 props.HeaderComponent 吧,到这一步,我们容器内render出来的DOM结构,大概是这样的:

<View>
  <Animated.View><HeaderComponent /></Animated.View>
	<FlatList />
</View>

最外层的 View 的展示区域,和用户自己的 FlatList 完全一样。那么问题来了,我们的下拉刷新 HeaderComponent 在默认情况下,应该是不可见的,是在用户下拉过程中,逐渐的从上到下进入容器的可视区域。那就默认把 HeaderComponent 绝对定位到容器可视区域的外边吧,可是往上移动多大呢,这就需要用户告诉我们容器一个下拉组件的高度了,props.headerHeight ,到这一步,容器渲染出来的样式大概如下:

<View>
  <Animated.View style={{position: 'absolute', top: - this.props.headerHeight}}>
    <HeaderComponent />
  </Animated.View>
	<FlatList />
</View>

完成了初始的DOM结构样式,接下来容器下拉时机的问题。

首先,什么时候用户下拉,是触发我们容器的下拉操作,而不是内部的 FlatList 的默认下拉呢?这个好像比较直接,当内部的 FlatList 已经下拉到顶部,不能再继续下拉时,用户的下拉动作,就应该触发容器的下拉。那么,我们就需要知道内部的 FlatList 的当前下拉位置,这可以通过 FlatList 的 onScroll 属性来获取当前 FlatList 的滚动距离。

什么时机触发容器的下拉确定了,那在容器下拉过程中,我们需要更新哪些组件呢?1) 自定义header组件肯定要更新,将最新的下拉距离传给header组件。2) 如果只是将header组件往下移动,我们的 FlatList 不动,那么自定义header会遮挡住 FlatList 的内容,这不是我们想要的;因此,在容器下拉过程中,内部的 FlatList 位置也需要响应的往下移动。

如果我们用容器的 state.containerTop 这个 Animated.Value 来保存当前容器下拉的距离,那么目前我们容器render的DOM结果大概如下:

const headerStyle = {
  position: 'absolute',
  left: 0,
  width: '100%',
  top: -this.props.headerHeight,
  transform: [{ translateY: this.state.containerTop }],
};
<View>
  <Animated.View style={[{ flex: 1, transform: [{ translateY: this.state.containerTop }] }]}>
  	<FlatList />
  </Animated.View>
  <Animated.View style={headerStyle}>
  	<HeaderComponent />
  </Animated.View>
</View>

这样,基本就完成了容器下拉过程中,自定义header和内部的FlatList同步下拉了。

下拉动作实现了,那下拉到什么位置,可以触发刷新呢?这就需要用户再传递一个触发刷新的下拉距离,就叫 props.refreshTriggerHeight 吧,当用户松开时,如果当前下拉距离 >= props.refreshTriggerHeight ,就会调用用户传入的刷新函数 props.onRefresh 。通常,用户如果下拉的距离比较大,松开手指时触发了刷新动作,这时候会整个组件会先回跳到一个刷新中的位置,这个位置,用户可以通过 props.refreshingHoldHeight 来指定。props.refreshTriggerHeightprops.refreshingHoldHeight 都是可选的,如果用户不传,默认为 props.headerHeight

One More Thing

上面其实还省略了一些工作,最重要的,就是在容器下拉过程中,怎么把下拉距离(下拉进度)传给用户的自定义 HeaderComponent ?上面容器上的 state.containerTop 其实就是当前容器下拉距离,只不过这是一个 Animated.Value ,我们 不能 读取到它当前的值。因此,我在容器上添加了一个 实例属性 this.containerTranslateY 来保存当前容器下拉的距离,我们会监听 state.containerTop 值的变化,在回调函数里,修改 this.containerTranslateY

等等!!containerTranslateY为什么没有放到容器的 state 上呢?不应该是 this.state.containerTranslateY 么??嗯,刚开始我确实是放在 state 上的,然后在用户下拉容器过程中,通过在容器上 setState,触发容器重新render,然后把 containerTranslateY 传递过header。但是,这样通过容器上 setState 触发header更新的方式,在我测试中,发现页面会比较卡顿。因此,在用户下拉容器过程中,并没有去修改容器的 state ,而是通过 方法调用 的命令方式,将用户当前下拉距离传给了header组件。这里可能还可以怎么优化一下吧。I'm not sure.

因此,用户的自定义header组件,必须 暴露一个实例方法 setProgress 来接收容器下拉过程中的一些参数,目前这个方法的签名是这样的:

// pullDistance 表示容器下拉的距离;percent 代表下拉的进度,[0, 1]
setProgress({pullDistance, percent}){}

完整的 header 组件demo,请参考 expo上的运行demo

The End

最后,听说,微交互动画,使用 lottie 和 RN 更配哦。

本来想尝试用 AE 做一个公司logo的 lottie 动画的,奈何没hold住……

完整的代码在github上:github.com/sophister/r…

相关链接

广告时间

人人贷大端技术博客中心

最后广而告之。 欢迎访问 人人贷大前端技术博客中心

里面有关 nodejs react react native 小程序 前端工程化等相关的技术文章陆续更新中,欢迎访问和吐槽~