React Native TabView + FlatList爬坑记

关注公众号前端方程式,更多前端小干货等着你喔!公众号会不定期分享前端技术,每天进步一点点,与大家相伴成长

最近接了一个简单列表页面的需求,小列表硬是爬大坑,给大家介绍一下本次爬坑的艰辛历程。

需求很简单,如上图所示,一个信息介绍,加一个左右滑动的列表。主要的工作量都在于这个TAB与左右滑动的交互上,问题不大,直接上插件,一番查找下,很快就找到了一个react-native-tab-view的组件,效果完全符合要求。

安装react-native-tab-view

好的,开始安装,一顿npm install与npm link。

yarn add react-native-tab-view
yarn add react-native-reanimated react-native-gesture-handler
react-native link react-native-reanimated
react-native link react-native-gesture-handler

再抄上一个demo。

import * as React from 'react';
import { View, StyleSheet, Dimensions } from 'react-native';
import { TabView, SceneMap } from 'react-native-tab-view';
​
​
const FirstRoute = () => (
 <View style={[styles.scene, { backgroundColor: '#ff4081' }]} />
);
​
​
const SecondRoute = () => (
 <View style={[styles.scene, { backgroundColor: '#673ab7' }]} />
);
​
​
const initialLayout = { width: Dimensions.get('window').width };
​
​
export default function TabViewExample() {
 const [index, setIndex] = React.useState(0);
 const [routes] = React.useState([
 { key: 'first', title: 'First' },
 { key: 'second', title: 'Second' },
 ]);
​
​
 const renderScene = SceneMap({
 first: FirstRoute,
 second: SecondRoute,
 });
​
​
 return (
 <TabView
 navigationState={{ index, routes }}
 renderScene={renderScene}
 onIndexChange={setIndex}
 initialLayout={initialLayout}
 />
 );
}
​
​
const styles = StyleSheet.create({
 scene: {
 flex: 1,
 },
});

哦豁,并没有成功运行,红色错误警告。仔细查看文档,文档中有一个大大的 IMPORTANT 提示,react-native-gesture-handler 库在安卓上有一个额外的配置。原来是需要在 MainActivity.java 额外加一个函数。

package com.swmansion.gesturehandler.react.example;
​
​
import com.facebook.react.ReactActivity;
+ import com.facebook.react.ReactActivityDelegate;
+ import com.facebook.react.ReactRootView;
+ import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;
public class MainActivity extends ReactActivity {
​
​
 @Override
 protected String getMainComponentName() {
 return "Example";
 }
+  @Override
+  protected ReactActivityDelegate createReactActivityDelegate() {
+    return new ReactActivityDelegate(this, getMainComponentName()) {
+      @Override
+      protected ReactRootView createRootView() {
+       return new RNGestureHandlerEnabledRootView(MainActivity.this);
+      }
+    };
+  }
}

火速加上,再稍微调整一下UI,打完收工,回家睡觉!

回家是不可能回家的,这仅仅是完成了TabView的功能,每个列表还需要上拉加载分页的功能呢。

分页功能

一般来说,这种的列表不可能就只有几条数据,分页功能是必不可少的,而RN中想要做一个高性能的列表我们可以选择 FlatList 来实现。

FlatList 基于 VirtualizedList 可以实现一个高性能的列表,其在渲染区域外的元素状态将不再保留,自始至终仅保留少量元素渲染在页面中,从而保证超长列表时不会因为元素过多导致页面卡顿甚至是奔溃的情况发生。

并且 FlatList 同时支持下拉刷新与上拉加载更多功能,是实现超长列表的不二选择,使用也很简单。

<FlatList
 refreshing={true}
 style={styles.wrap}
 data={data}
 renderItem={renderItem}
 keyExtractor={item => '' + item.id}
 ListEmptyComponent={empty}
 onEndReached={onEndReached}
 onEndReachedThreshold={0.5}
/>

那么问题来了,实际使用中发现,加载一屏数据后,滚动条自动滚动到底部,导致整个页面跳动明显,而且滚动到顶部也明显不符合要求。

原因很容易猜到,多半是因为 FlatList 高度不确定导致的,简单验证一下,去除 TabView ,仅使用 FlatList 列表,结果显示表现正常,而我这边使用 FlatList 是使用 flex: 1; 指定了高度的,那么这是为什么呢?

那再猜一下,多半是因为 TabView 组件在渲染底部列表的时候在中间增加了其他容器且没有设置高度导致的,仔细查看react-native-tab-view源代码,发现 src/SceneMap.tsx 中,被用于渲染页面的函数 renderScene 调用。

class SceneComponent<
 T extends { component: React.ComponentType<any> }
> extends React.PureComponent<T> {
 render() {
 const { component, ...rest } = this.props;
 return React.createElement(component, rest);
 }
}
​
​
export default function SceneMap<T extends any>(scenes: {
 [key: string]: React.ComponentType<T>;
}) {
 return ({ route, jumpTo, position }: T) => (
 <SceneComponent
 key={route.key}
 component={scenes[route.key]}
 route={route}
 jumpTo={jumpTo}
 position={position}
 />
 );
}

此处 renderScene 会在实际页面上层包裹上包括 <SceneComponent> 以及 React.createElement 在内的两层容器,从而导致页面高度丢失,最终导致 FlatList 滚动异常。

仔细查看 TabView 中的 renderScene 可以使用另外一种写法。

renderScene = ({ route }: any) => {
 const { activeIndex, tabs } = this.state;
 switch (route.key) {
 case 'all':
 return (
 <List
 data={tabs[0].data}
 onReachBottom={this.onReachBottom}
 isVisible={activeIndex === 0}
 />
 );
 case 'earning':
 return (
 <List
 data={tabs[1].data}
 onReachBottom={this.onReachBottom}
 isVisible={activeIndex === 1}
 />
 );
 case 'spending':
 return (
 <List
 data={tabs[2].data}
 onReachBottom={this.onReachBottom}
 isVisible={activeIndex === 2}
 />
 );
 }
};

使用该方法,可以避免上述两层容器的生成,直接填充指定的组件,自然也就不会出现高度丢失的问题了。

仔细对比一下两种方式生成的元素,可以很明显发现,初始写法多了两层容器,分别就是<SceneComponent>以及 React.createElement 生成的 <all>。再验证一下效果,滚动确实正常了,不会出现滚动条自动滚动到底部的问题,完美!

so!你以为就结束了吗?并没有!使用该方式,第一页的列表确实表现正常,但是第二页以及第三页的列表居然无法滚动,经测试发现,页面初始加载的时候第二、三页初始就没有触发 onEndReached 导致后续加载无法触发,原因不想再纠结了,暴力解决一下,既然没有触发,那就手动给他触发一下可以了。

// 列表组件中
​
​
const [initialized, setInitialized] = useState(false);
const { onReachBottom, isVisible = false } = props;
// isVisible = 当前列表的index === TabView的activeIndex
​
useEffect(() => {
 if (isVisible && !initialized) {
 onReachBottom();
 setInitialized(true);
 }
}, [isVisible, initialized]);

到此,所有的功能都完成了,完美,又是提前下班的一天!

本文首发于本人公众号,前端方程式,分享与关注前端技术,欢迎关注~~

前端方程式