折翼的飞鸟

导航

Taro实现VirtualList虚拟列表

        在使用Taro开发微信小程序时,需要加载长列表数据,在官网找了相关的VirtualList虚拟列表的组件,要么版本过低(项目中使用3.0.1版本),要么使用不方便(可能是自己没看懂的问题),官方也说有虚拟列表就是长列表加载,使用后发现性能不能达到满足,于是就参考网上的虚拟列表的思路开始自己做。

      简单说下思路,设计思路是通过虚拟列表,只展示屏幕可视区域范围的数据,但在taro中效果还是有点卡顿,经大佬指点,渲染可视区域时,
渲染组件不再进行删除和新建,而是创建指定数量的组件展示,向下滚动时,将最上面的组件通过css修改样式显示的位置,
形成一种链条滚动的形式。

     简化的思路图:

      

下面贴出主要的代码:

  1 /**
  2  * author: wang.p  2021-09-18
  3  *
  4  * description:  自定义虚拟列表
  5  *
  6  * */
  7 
  8 import React, {Component} from "react"
  9 import {ScrollView, View} from '@tarojs/components'
 10 import PropsType from 'prop-types';
 11 import classnames from 'classnames';
 12 import './virtual-list.scss';
 13 
 14 class VirtualList extends Component {
 15 
 16   static propTypes = {
 17     className: PropsType.string, // 样式名
 18     rowCount: PropsType.number,  // 渲染的行数
 19     source: PropsType.array,  // 数据源数组
 20     rowHeight: PropsType.number,  // 行高
 21     scrollToIndex: PropsType.number, // 跳转到指定的位置
 22     getRowHeight: PropsType.func,  // 动态行高
 23     onScroll: PropsType.func, // 滚动处罚事件
 24     onRowRender: PropsType.func, // 行渲染
 25     onSrollTopRecommend: PropsType.func, // 触发顶部样式事件   这个是本人项目中使用的,可以不用
 26   }
 27 
 28   static defaultProps = {
 29     rowCount: 20,
 30     source: [],
 31     rowHeight: 40
 32   }
 33 
 34   state = {
 35     rowCount: 20, // 显示行数
 36     scrollHeight: 0, // 所有内容渲染的高度
 37     scrollData: [],  // 渲染可视区域数据的数组
 38     scrollStyles: [], // 样式数组
 39     isCategoryToScroll: false, // 是否是分类切换定位滚动
 40     scrollToIndex: 0, // 跳转到指定位置
 41     compareHeight: 0  // 触发渲染的高度差
 42   }
 43 
 44   componentWillMount = () => {
 45     const {rowCount, source, rowHeight, getRowHeight} = this.props;
 46     let scrollStyles = [];
 47     let scrollData = [];
 48     let scrollHeight = 0;
 49     let compareHeight = 0;
 50     source.forEach((item, idx) => {
 51       let styles = {position: 'absolute', left: 0, top: scrollHeight};
 52       scrollStyles.push(styles);
 53       let tempHeight = typeof getRowHeight === 'function' ? getRowHeight(idx, item) : rowHeight;
 54       scrollHeight += tempHeight;
 55     });
 56 
 57     let showCount = source.length < rowCount ? source.length : rowCount;
 58     for (let i = 0; i < showCount; i++) {
 59       scrollData.push({sort: i, row: i})
 60     }
 61 
 62     compareHeight = Math.floor(scrollStyles[showCount - 1].top / showCount) * 3;
 63 
 64     this.setState({scrollHeight, scrollData, scrollStyles, rowCount, compareHeight});
 65   }
 66 
 67 
 68   componentDidMount = () => {
 69 
 70   }
 71 
 72   componentWillReceiveProps(nextProps: Readonly<P>, nextContext: any) {
 73 
 74     if (nextProps.scrollToIndex != this.state.scrollToIndex && this.state.scrollStyles.length > 0) {
 75       let scrollToIndex = nextProps.scrollToIndex > this.state.scrollStyles.length ? this.state.scrollStyles.length - 1 : nextProps.scrollToIndex;
 76       let scrollTop = this.state.scrollStyles[scrollToIndex];
 77       if (scrollTop) {
 78         this.setState({scrollToIndex: nextProps.scrollToIndex, scrollTop: scrollTop.top , isCategoryToScroll: true});
 79       }
 80     }
 81   }
 82 
 83   render() {
 84     const {className, style } = this.props;
 85     const {scrollHeight, scrollData, scrollStyles, scrollTop} = this.state;
 86 
 87     return <ScrollView className={classnames('self-virtual-list', className)}
 88                        style={{...style}}
 89                        scrollTop={scrollTop}
 90                        scrollY={true}
 91                        scrollWithAnimation
 92                        onScroll={this.onScroll.bind(this)}>
 93       <View className={'self-virtual-list-body'} style={{height: scrollHeight}}>
 94         {scrollData.length > 0 && scrollData.map((item, idx) => {
 95           return this.props.onRowRender(item, scrollStyles[item.row]);
 96         })}
 97       </View>
 98     </ScrollView>
 99 
100 
101   }
102 
103   currentScrollTop = 0;
104   prevScrollTop = 0;  // 记录上次滚动的Scrolltop
105 
106   findMinOrMax = (data, isMax= false) => {
107     if (isMax) {
108       return data.reduce((prev, next) => {
109         if (prev.row < next.row) {
110           return next;
111         } else {
112           return prev;
113         }
114       })
115     } else {
116       return data.reduce((prev, next) => {
117         if (prev.row < next.row) {
118           return prev;
119         } else {
120           return next;
121         }
122       })
123     }
124   }
125 
126   /**
127    * 滚动事件,计算渲染菜品的数据
128    * 滚动到顶部时,如果顶部有推荐菜品就展示出来,如果上拉滚动,就隐藏推荐菜品
129    * */
130   onScroll = (event) => {
131     let scrollY = event.detail.deltaY;
132     const eventScrollTop = event ? event.detail.scrollTop : this.state.scrollTop;
133     const {scrollStyles, rowCount, compareHeight, scrollData, isCategoryToScroll} = this.state;
134 
135     if (Math.abs(this.currentScrollTop - eventScrollTop) > compareHeight || eventScrollTop <= scrollStyles[3].top || eventScrollTop >= scrollStyles[scrollStyles.length - 5].top) {
136       // 查询出当前scrollTop在那个范围
137       this.currentScrollTop = eventScrollTop;
138       let scrollIndex = 0;
139       for(let i=1;i<scrollStyles.length;i++) {
140         if (scrollStyles[i - 1].top <= eventScrollTop && scrollStyles[i].top > eventScrollTop) {
141           scrollIndex = i;
142           break;
143         }
144       }
145       // 计算出渲染范围的最小下标和最大下标
146       let minIndex = scrollIndex - parseInt(Math.floor(rowCount / 2.0));
147       if (minIndex < 0) {
148         minIndex = 0;
149       }
150       let maxIndex = minIndex + rowCount;
151       if (maxIndex > scrollStyles.length - 1) {
152         maxIndex = scrollStyles.length - 1;
153       }
154       // 找出当前显示的数据范围最小值和最大值
155       let minData = this.findMinOrMax(scrollData);
156       let maxData = this.findMinOrMax(scrollData, true);
157       let newScrollData = [...scrollData];
158       if (minIndex > minData.row) {
159         // 向下滑动渲染, 找出最小值,替换成最大值,循环进行替换
160         let cycle = minIndex - minData.row;
161         for (let i = 0; i < cycle; i++) {
162           minData = this.findMinOrMax(scrollData);
163           maxData = this.findMinOrMax(scrollData, true);
164 
165           scrollData[minData.sort]['row'] = maxData.row + 1;
166         }
167 
168         this.setState({scrollData: newScrollData, isCategoryToScroll: false});
169       } else {
170         // 向上滑动渲染
171         let cycle = minData.row - minIndex;
172         for (let i = 0; i < cycle; i++) {
173           minData = this.findMinOrMax(scrollData);
174           maxData = this.findMinOrMax(scrollData, true);
175 
176           scrollData[maxData.sort]['row'] = minData.row - 1;
177         }
178         this.setState({scrollData: newScrollData, isCategoryToScroll: false});
179       }
180 
181     }
182 
183     let scycelScroll = compareHeight / 3;
184     // 滚动一定距离,就触发外部事件
185     if (!isCategoryToScroll && Math.abs(this.prevScrollTop - eventScrollTop) > scycelScroll && this.props.onScroll) {
186       this.prevScrollTop = eventScrollTop;
187       let scrollIndex = 0;
188       for(let i=1;i<scrollStyles.length;i++) {
189         if (scrollStyles[i - 1].top <= eventScrollTop && scrollStyles[i].top > eventScrollTop) {
190           scrollIndex = i;
191           break;
192         }
193       }
194       this.props.onScroll(scrollIndex);
195     }
196 
197     // 处理顶部隐藏的组件
198     if (this.props.onSrollTopRecommend) {
199       if (scrollY > 0) {
200         // 下拉
201         if (event.detail.scrollTop <= scycelScroll) {
202           // 展开推荐菜品
203           this.props.onSrollTopRecommend && this.props.onSrollTopRecommend(true);
204         }
205       } else {
206         // 上拉
207         if (event.detail.scrollTop > scycelScroll) {
208           // 触发收起推荐菜品
209           this.props.onSrollTopRecommend && this.props.onSrollTopRecommend(false);
210         }
211       }
212     }
213 
214   }
215 
216 
217 }
218 
219 
220 export default VirtualList

在项目中的效果图:

   

 

 使用方法说明:引入组件,添加对应的方法

<VirtualList className={'category-virtual'}
            width={width}
            height={height}
            source={source}
            rowCount={20}
            getRowHeight={this.getRowHeight}
            scrollToIndex={scrollIndex}
            onRowRender={this.onRowRender}
            onScroll={this.onScroll}
/>
/**
* 获取行高
* @idx 数据源下标
* @value 数据
*/
getRowHeight = (idx, value) => {
   // 这里可以通过数据源的下标idx,来返回高度

   return 100; // 高度可以通过函数来计算
}
/**
* @data 数据{sort: 渲染的行数的顺序, row: 渲染数据源的下标}
* @style: 样式
* */
onRowRender = (data, style) => {
const {sort, row} = data;
const {source} = this.state;

return <View key={sort} style={style}>
   ...
</View>

}
/**
* 通过滚动
* @currentIndex 分类数组下标
* */
onScroll = (currentIndex) => {
// 通过滚动的触发事件执行某些方法

}

 

下面提供代码下载路径:

  链接:https://pan.baidu.com/s/1kW0w1D03N72uCE3S9Vy2zg
  提取码:460i

 

posted on 2021-09-26 09:57  折翼的飞鸟  阅读(1896)  评论(0编辑  收藏  举报