RecyclerListView和FlashList
RecyclerListView
4.2.0
RecyclerListView
使用示例
/***
Use this component inside your React Native Application.
A scrollable list with different item type
*/
import React, { Component } from "react";
import { View, Text, Dimensions } from "react-native";
import { RecyclerListView, DataProvider, LayoutProvider } from "recyclerlistview";
const ViewTypes = {
FULL: 0,
HALF_LEFT: 1,
HALF_RIGHT: 2
};
let containerCount = 0;
class CellContainer extends React.Component {
constructor(args) {
super(args);
this._containerId = containerCount++;
}
render() {
return <View {...this.props}>{this.props.children}<Text>Cell Id: {this._containerId}</Text></View>;
}
}
/***
* To test out just copy this component and render in you root component
*/
export default class RecycleTestComponent extends React.Component {
constructor(args) {
super(args);
let { width } = Dimensions.get("window");
//Create the data provider and provide method which takes in two rows of data and return if those two are different or not.
//THIS IS VERY IMPORTANT, FORGET PERFORMANCE IF THIS IS MESSED UP
let dataProvider = new DataProvider((r1, r2) => {
return r1 !== r2;
});
//Create the layout provider
//First method: Given an index return the type of item e.g ListItemType1, ListItemType2 in case you have variety of items in your list/grid
//Second: Given a type and object set the exact height and width for that type on given object, if you're using non deterministic rendering provide close estimates
//If you need data based check you can access your data provider here
//You'll need data in most cases, we don't provide it by default to enable things like data virtualization in the future
//NOTE: For complex lists LayoutProvider will also be complex it would then make sense to move it to a different file
this._layoutProvider = new LayoutProvider(
index => {
if (index % 3 === 0) {
return ViewTypes.FULL;
} else if (index % 3 === 1) {
return ViewTypes.HALF_LEFT;
} else {
return ViewTypes.HALF_RIGHT;
}
},
(type, dim) => {
switch (type) {
case ViewTypes.HALF_LEFT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.HALF_RIGHT:
dim.width = width / 2;
dim.height = 160;
break;
case ViewTypes.FULL:
dim.width = width;
dim.height = 140;
break;
default:
dim.width = 0;
dim.height = 0;
}
}
);
this._rowRenderer = this._rowRenderer.bind(this);
//Since component should always render once data has changed, make data provider part of the state
this.state = {
dataProvider: dataProvider.cloneWithRows(this._generateArray(300))
};
}
_generateArray(n) {
let arr = new Array(n);
for (let i = 0; i < n; i++) {
arr[i] = i;
}
return arr;
}
//Given type and data return the view component
_rowRenderer(type, data) {
//You can return any view here, CellContainer has no special significance
switch (type) {
case ViewTypes.HALF_LEFT:
return (
<CellContainer style={styles.containerGridLeft}>
<Text>Data: {data}</Text>
</CellContainer>
);
case ViewTypes.HALF_RIGHT:
return (
<CellContainer style={styles.containerGridRight}>
<Text>Data: {data}</Text>
</CellContainer>
);
case ViewTypes.FULL:
return (
<CellContainer style={styles.container}>
<Text>Data: {data}</Text>
</CellContainer>
);
default:
return null;
}
}
render() {
return <RecyclerListView layoutProvider={this._layoutProvider} dataProvider={this.state.dataProvider} rowRenderer={this._rowRenderer} />;
}
}
const styles = {
container: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#00a1f1"
},
containerGridLeft: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#ffbb00"
},
containerGridRight: {
justifyContent: "space-around",
alignItems: "center",
flex: 1,
backgroundColor: "#7cbb00"
}
};
RecyclerListView继承自父类ComponentCompat(继承自React.Component),其render函数如下
public render(): React.ReactNode {
if (!this._hasRenderedOnce) {
this._hasRenderedOnce = true;
this.componentWillMountCompat();
} else {
this.componentWillUpdateCompat();
}
return this.renderCompat();
}
renderCompat函数,通过ScrollComponent包裹_generateRenderStack的元素
_generateRenderStack函数,获取ViewRenderer集合
_renderRowUsingMeta函数,返回每行元素ViewRenderer
renderAheadOffset,指定视图渲染多少像素,增加该值有助于减少空白。默认是250。renderAheadOffset为ScrollComponent的属性
ScrollComponent的render函数,返回ScrollView,内层是View,View层内先调用renderContentContainer,后调用renderFooter
renderAheadOffset生效的原理是
- 定义windowSize,假设为垂直方向,则windowSize为this._height+this.props.renderAheadOffset
- 然后通过renderContentContainer函数,传入windowSize
ViewRenderer
ViewRenderer继承自BaseViewRenderer
renderCompat里调用的是renderItemContainer函数,为外部传入
Provider
dataProvider,类型DataProvider,定义每个元素的数据
核心方法为cloneWithRows,传入newData
- 调用newInstance创建一个DataProvider
- 把newData赋值给_data属性
- 找到_firstIndexToProcess,通过rowHasChanged判断,这个rowHasChanged是DataProvider初始化的时候传入的
contextProvider,类型ContextProvider
LayoutProvider layoutProvider,类型BaseLayoutProvider,定义每个元素的布局
LayoutManager,管理Layout(x,y,width,height)数组
VirtualRenderer
RecycleItemPool
_recyclePool,RecycleItemPool类型,
RecycleItemPool
- _recyclableObjectMap,key为对象类型,value为对象的集合,通过_getRelevantSet方法可以取出对象类型对应的集合
- _availabilitySet,key为object,value为objectType
- putRecycledObject方法,参数objectType,object
- 根据objectType从_getRelevantSet方法取出objectSet
- 如果_availabilitySet没有该object,首先objectSet[object] = null,然后_availabilitySet操作,key为object,value位objectType
- getRecycledObject
- 根据传入的objectType,找到objectSet
- 从objectSet取出一个元素,作为recycledObject
- 从objectSet中删除该自有属性recycledObject对应的内容,然后删除_availabilitySet该自有属性的对应的内容
- 最后返回recycledObject
- removeFromPool
- 从_recyclableObjectMap删除该object对应的内容
- 根据传入的object,从_availabilitySet删除对应的内容
- 提供removeFromPool方法
_recyclePool的使用
首先看getRecycledObject的使用,具体看syncAndGetKey
syncAndGetKey
- 根据index,从_layoutProvider调用getLayoutTypeForIndex方法获取对应的type,该方法就是Layout初始化传入的index和type的映射
- 根据type,调用getRecycledObject获取对应的key
- 根据key,从renderStack中取出itemMeta,itemMeta的dataIndex赋值新的传入的index
- 最后返回key
而该key是可重用的关键,在_renderRowUsingMeta函数中(也就是渲染每一行的时候),ViewRenderer的key就是该key
onScroll的逻辑 _onScroll的逻辑,这里主要调用VirtualRenderer的updateOffset方法,后面会调用ViewabilityTracker的updateOffset
- 根据offset,重新赋值_engagedWindow,_visibleWindow
- 从_visibleIndexes取出第一个元素,赋值给startIndex
- 调用_fitAndUpdate
_fitAndUpdate的逻辑
_fitAndUpdate
const newVisibleItems: number[] = [];
const newEngagedItems: number[] = [];
this._fitIndexes(newVisibleItems, newEngagedItems, startIndex, true);
this._fitIndexes(newVisibleItems, newEngagedItems, startIndex + 1, false);
this._diffUpdateOriginalIndexesAndRaiseEvents(newVisibleItems, newEngagedItems);
对于isReverse为true的时候,从startIndex到0遍历,调用_checkIntersectionAndReport,若返回true,atLeastOneLocated赋值为true,这里会接下去遍历,直到_checkIntersectionAndReport返回false退出循环。
对于isReverse为false,则从startIndex到count遍历,同样是直到_checkIntersectionAndReport返回false退出
对于_checkIntersectionAndReport方法,传入index(遍历的索引)和insertOnTop(即isReverse)。
- _itemIntersectsVisibleWindow返回true,如果insertOnTop为true,index分别插入newVisibleIndexes、newEngagedIndexes的顶部,insertOnTop为false,则插入底部
- 接着判断_itemIntersectsEngagedWindow,若返回true,接着如果insertOnTop为true,index插入newEngagedIndexes顶部,insertOnTop为false,插入newEngagedIndexes底部
- _itemIntersectsVisibleWindow和_itemIntersectsEngagedWindow调用的都是_itemIntersectsWindow,传入的参数,window分别是_engagedWindow(相对于visibleWindow,前后加上了_renderAheadOffset的大小)、_visibleWindow,以及item的startBound和endBound。_itemIntersectsWindow主要是判断item是否显示在window内。
最终newVisibleItems拿到可见的项目,newEngagedItems拿到可渲染的项目(大于可见)
_diffUpdateOriginalIndexesAndRaiseEvents,这里还需进行diff操作
- 调用_diffArraysAndCallFunc函数,对比newVisibleItems(新数据)、this._visibleIndexes(老数据)
- 通过二分查找,找出两个数组中不同的数据,然后调用onVisibleRowsChanged函数
- 依上的逻辑调用_diffArraysAndCallFunc,对比newEngagedItems、this._engagedIndexes
ProgressiveListView
maxRenderAhead
renderAheadStep 每一帧的增量
renderAheadOffset,初始值
FlashList中对其的使用
maxRenderAhead={3 * finalDrawDistance}
finalRenderAheadOffset={finalDrawDistance}
renderAheadStep={finalDrawDistance}
这里会重写父类的onItemLayout(每个组件的布局回调),这里会在第一个项目布局时触发updateRenderAheadProgressively
FlashList
1.4.0
GridLayoutProvider
GridLayoutProvider来自recyclerlistview
estimatedItemSize
大致的item size,用来FlashList大致推断首屏或者滑动的时候需要渲染多少个item。如果item size 不一,可以用平均值/中位数
在快速滑动的时候,如果estimatedItemSize对比实际大小过大,那么很容易出现空白。
defaultEstimatedItemSize为100
render
render
外层是一个StickyHeaderContainer,里面包裹ProgressiveListView
handleSizeChange,会调用validateListSize,并保存宽高到listFixedDimensionSize,新旧size不同会触发RecyclerListView的rerender
renderContentContainer
这里的内容依次布局PureComponentWrapper,AutoLayoutView(传入的children嵌入到AutoLayoutView内),PureComponentWrapper
renderItemContainer
外层为CellRendererComponent,内层PureComponentWrapper。
PureComponentWrapper的renderer函数,为一个View和Separator组成,View的内层通过rowRendererWithIndex函数返回,这里就是FlashList传入的renderItem
可见性
interface ViewabilityConfig: {
minimumViewTime: number;
viewAreaCoveragePercentThreshold: number;
itemVisiblePercentThreshold: number;
waitForInteraction: boolean;
}
决策view 可见性的配置
minimumViewTime:默认250
itemVisiblePercentThreshold:item可见的百分比
isItemViewable的逻辑
- 传入可见的阈值,如果viewAreaCoveragePercentThreshold存在使用该值,否则itemVisiblePercentThreshold
- 计算百分比
- 如果通过viewAreaCoveragePercentThreshold判断,分母为listSize.height(如果为垂直方向),分子为pixelsVisible(即item y轴可见的高度)。viewAreaCoveragePercentThreshold为外部传入0-100
- 如果通过itemVisiblePercentThreshold判断,分母为itemLayout.height,分子pixelsVisible
- itemLayout的实现为getLayout(index),getLayout为外部传入,实现为this.flashListRef.recyclerlistview_unsafe?.getLayout(index)
AutoLayoutView
CellContainer,CellContainerManager,cell相关,其最重要的属性是index
AutoLayoutView,一个自动布局的UIView,外层的UIScrollView由js侧负责包裹 AutoLayoutViewManager,继承自RCTViewManager,设置相关属性