Taro优化VirtualList虚拟列表组件
在之前的虚拟列表VirtualScroll组件基础上做了优化,主要是修复一些问题,目前还没有实现骨架屏 原来的虚拟列表 : Taro实现VirtualList虚拟列表
不多说,直接贴代码
下面是百度网盘源码下载路径:
链接:https://pan.baidu.com/s/1HByy-U2AYQrssPFqdA9wDw
提取码:d543
/**
* author: wang.p 2022-04-15
*
* description: 自定义虚拟滚动列表
*
* */
import React, {Component} from "react"
import {ScrollView, View} from '@tarojs/components'
import PropsType from 'prop-types';
import classnames from 'classnames';
import './virtual-list.scss';
class VirtualScroll extends Component {
static propTypes = {
refresh: PropsType.bool, // 数据改变是否刷新
className: PropsType.string, // 样式名
rowCount: PropsType.number, // 渲染的行数
total: PropsType.number, // 总行数,存在就是动态请求数据来渲染
source: PropsType.array, // 数据源数组
rowHeight: PropsType.number, // 行高
scrollToIndex: PropsType.number, // 跳转到指定的位置
getRowHeight: PropsType.func, // 动态行高
onScroll: PropsType.func, // 滚动处罚事件
onRowRender: PropsType.func, // 行渲染
onSrollTopRecommend: PropsType.func, // 触发顶部样式事件
}
static defaultProps = {
total: 0,
rowCount: 20,
source: [],
rowHeight: 40,
placeholder: false,
refresh: true
}
constructor(props) {
super(props);
this.state = {
rowCount: 20, // 显示行数
scrollHeight: 0, // 所有内容渲染的高度
scrollData: [], // 渲染可视区域数据的数组
scrollStyles: [], // 样式数组
isCategoryToScroll: false, // 是否是分类切换定位滚动
scrollToIndex: 0, // 跳转到指定位置
compareHeight: 0, // 触发渲染的高度差
scrollTop: 0,
source: [] //
}
}
componentDidMount = () => {
let newState = this.setSourceStyle(this.props);
this.setState({...newState, source: this.props.source});
}
/**
* 设置布局
* @data 数据
* @fresh 是否是重新渲染数据
* */
setSourceStyle = (data, fresh) => {
const {rowCount, source, rowHeight, getRowHeight, total} = data;
let scrollStyles = [];
let scrollData = [];
let scrollHeight = 0;
let compareHeight = 0;
let lastHeight = 0;
let newState = {};
if (total > 0) {
for(let i=0;i<total;i++) {
let styles = {position: 'absolute', left: 0, top: scrollHeight};
scrollStyles.push(styles);
let tempHeight = typeof getRowHeight === 'function' ? getRowHeight(source[i], i) : rowHeight;
scrollHeight += tempHeight;
lastHeight = tempHeight;
}
newState = {scrollHeight: scrollHeight + lastHeight , scrollStyles};
if (!fresh || this.isRenderScrollData(scrollStyles, this.state.scrollData, rowCount)) {
// 首次渲染
let showCount = total < rowCount ? total : rowCount;
for (let i = 0; i < showCount; i++) {
scrollData.push({sort: i, row: i})
}
compareHeight = Math.floor(scrollStyles[showCount - 1].top / showCount) * 3;
newState['scrollData'] = scrollData;
newState['rowCount'] = rowCount;
newState['compareHeight'] = compareHeight;
}
} else if (source && source.length > 0) {
source.forEach((item, idx) => {
let styles = {position: 'absolute', left: 0, top: scrollHeight};
scrollStyles.push(styles);
let tempHeight = typeof getRowHeight === 'function' ? getRowHeight(item, idx) : rowHeight;
scrollHeight += tempHeight;
lastHeight = tempHeight;
})
newState = {scrollHeight: scrollHeight + lastHeight , scrollStyles};
if (!fresh || this.isRenderScrollData(scrollStyles, this.state.scrollData, rowCount)) {
// 首次渲染
let showCount = source.length < rowCount ? source.length : rowCount;
for (let i = 0; i < showCount; i++) {
scrollData.push({sort: i, row: i})
}
compareHeight = Math.floor(scrollStyles[showCount - 1].top / showCount) * 3;
newState['scrollData'] = scrollData;
newState['rowCount'] = rowCount;
newState['compareHeight'] = compareHeight;
}
} else {
newState = {scrollData, rowCount, compareHeight, scrollHeight};
}
return newState
}
/**是否需要更新渲染的数组 */
isRenderScrollData = (scrollStyles, scrollData, rowCount) => {
if (scrollStyles.length < rowCount && scrollStyles.length != scrollData.length) {
return true;
}
if (scrollStyles.length >= rowCount && scrollData.length != rowCount) {
return true;
}
let maxData = this.findMinOrMax(scrollData, true);
if (maxData.row > scrollStyles.length) {
return true;
}
return false;
}
componentWillReceiveProps(nextProps, nextContext) {
if (JSON.stringify(nextProps.source) !== JSON.stringify(this.state.source)) {
const {scrollTop} = this.state;
let newState = this.setSourceStyle(nextProps, true);
if (nextProps.refresh) {
this.setState({...newState, source: nextProps.source, scrollTop: (scrollTop > 0 ? 0 : 0.1), isCategoryToScroll: true})
} else {
this.setState({...newState, source: nextProps.source});
}
}
}
render() {
const {className, style} = this.props;
const {scrollHeight, scrollData, scrollStyles, scrollTop, source} = this.state;
return <ScrollView className={classnames('virtual-scroll', className)}
style={{...style}}
scrollTop={scrollTop}
scrollY={true}
scrollWithAnimation
onScroll={this.onScroll.bind(this)}>
<View className={'virtual-scroll-body'} style={{height: scrollHeight}}>
{scrollData.length > 0 && scrollData.map((item, idx) => {return <View className="virtual-scroll-item" key={item.row} style={scrollStyles[item.row]}>
{this.props.onRowRender(item, scrollStyles[item.row], source)}
</View>
})}
</View>
{this.props.children}
</ScrollView>
}
currentScrollTop = 0;
prevScrollTop = 0; // 记录上次滚动的Scrolltop
findMinOrMax = (data, isMax= false) => {
if (isMax) {
return data.reduce((prev, next) => {
if (prev.row < next.row) {
return next;
} else {
return prev;
}
})
} else {
return data.reduce((prev, next) => {
if (prev.row < next.row) {
return prev;
} else {
return next;
}
})
}
}
/**
* 滚动事件,计算渲染菜品的数据
* 滚动到顶部时,如果顶部有推荐菜品就展示出来,如果上拉滚动,就隐藏推荐菜品
* */
onScroll = (event) => {
let scrollY = event.detail.deltaY;
const eventScrollTop = event ? event.detail.scrollTop : this.state.scrollTop;
const {scrollStyles, rowCount, compareHeight, scrollData, isCategoryToScroll} = this.state;
if (scrollData.length === 0) {
return;
}
let styleLen = scrollStyles.length;
if (Math.abs(this.currentScrollTop - eventScrollTop) > compareHeight || (styleLen > 4 && eventScrollTop <= scrollStyles[3].top) || ( styleLen > 5 && eventScrollTop >= scrollStyles[scrollStyles.length - 5].top)) {
// 查询出当前scrollTop在那个范围
this.currentScrollTop = eventScrollTop;
let scrollIndex = -1;
let afterIndex = scrollStyles.findIndex(item => item.top > eventScrollTop);
if (afterIndex != -1) {
if (afterIndex > 0) {
if (scrollStyles[afterIndex - 1] && scrollStyles[afterIndex - 1].top <= eventScrollTop) {
scrollIndex = afterIndex - 1;
}
} else {
scrollIndex = 0;
}
}
if (scrollIndex == -1) {
return;
}
// 计算出渲染范围的最小下标和最大下标
let minIndex = scrollIndex - parseInt(Math.floor(rowCount / 2.0));
if (minIndex < 0) {
minIndex = 0;
}
// 找出当前显示的数据范围最小值和最大值
let minData = this.findMinOrMax(scrollData);
let maxData = this.findMinOrMax(scrollData, true);
let newScrollData = [...scrollData];
if (minIndex > minData.row) {
// 向下滑动渲染, 找出最小值,替换成最大值,循环进行替换
let cycle = minIndex - minData.row;
for (let i = 0; i < cycle; i++) {
minData = this.findMinOrMax(scrollData);
maxData = this.findMinOrMax(scrollData, true);
// 超过最大值就不再循环
if (maxData.row + 1 > scrollStyles.length) {
break;
}
scrollData[minData.sort]['row'] = maxData.row + 1;
}
this.setState({scrollData: newScrollData, isCategoryToScroll: false});
} else if(scrollY > 0){
// 向上滑动渲染
let cycle = minData.row - minIndex;
for (let i = 0; i < cycle; i++) {
minData = this.findMinOrMax(scrollData);
maxData = this.findMinOrMax(scrollData, true);
scrollData[maxData.sort]['row'] = minData.row - 1;
}
this.setState({scrollData: newScrollData, isCategoryToScroll: false});
}
}
let scycelScroll = compareHeight / 3;
// 滚动一定距离,就触发外部事件
if (!isCategoryToScroll && Math.abs(this.prevScrollTop - eventScrollTop) > scycelScroll && this.props.onScroll) {
this.prevScrollTop = eventScrollTop;
let scrollIndex = 0;
for(let i=1;i<scrollStyles.length;i++) {
if (scrollStyles[i - 1].top <= eventScrollTop && scrollStyles[i].top > eventScrollTop) {
scrollIndex = i;
break;
}
}
// 返回当前渲染的行,以及滚动条滚动的方向 scrollY > 0 表示下拉, scrollY > 0 表示上拉
this.props.onScroll(scrollIndex, scrollY);
}
// 处理顶部隐藏的组件
if (this.props.onSrollTopRecommend) {
if (scrollY > 0) {
// 下拉
if (event.detail.scrollTop <= scycelScroll) {
// 展开推荐菜品
this.props.onSrollTopRecommend && this.props.onSrollTopRecommend(true);
}
} else {
// 上拉
if (event.detail.scrollTop > scycelScroll) {
// 触发收起推荐菜品
this.props.onSrollTopRecommend && this.props.onSrollTopRecommend(false);
}
}
}
}
}
export default VirtualScroll
以下是样式文件内容:
.virtual-scroll {
width: 100%;
height: 100%;
box-sizing: border-box;
.virtual-scroll-body {
position: relative;
box-sizing: inherit;
width: 100%;
height: 100%;
}
.virtual-scroll-item {
width: 100%;
}
}
引用的示例代码:
<VirtualScroll className='scroll-content' source={[]} rowHeight={50} onRowRender={this.onRowRender} />
// 元素高度不一样,可以使用 getRowHeight 函数来动态设置列元素高度
/* 渲染的列内容 @data 列表的下标 {row: 行数, sort: 0} @style 样式 @ sourceData 所有的数据对象 */ onRowRender = (data, style, sourceData) => { const {row} = data; let itemData = sourceData[row]; if (itemData) { return <View>渲染的列内容{itemData}</View> } else { return null; } }
浙公网安备 33010602011771号