折翼的飞鸟

导航

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; } }

 

posted on 2025-02-12 16:21  折翼的飞鸟  阅读(228)  评论(0)    收藏  举报