custom-list-view.uvue 3.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
<template>
  <scroll-view class="custom-list-view-scoll-view" v-bind="$attrs" ref="scroll" @scroll="onScroll">
    <view class="custom-list-view-placeholder" :style="{ height: placeholderHeight + 'px' }">
      <view class="custom-list-view-container" :style="{ top: containerTop + 'px' }">
        <slot :items="items"></slot>
      </view>
    </view>
  </scroll-view>
</template>

<script>
  /**
   * 使用限制
   * - 容器大小变动时未刷新缓存的子元素大小
   * - 不支持设置初始滚动位置
   * - list数据内每一项不可以是基础类型
   * - item不支持设置margin,会导致计算位置不准确
   */
  export default {
    name: "custom-list-view",
    props: {
      list: {
        type: Array as PropType<any[]>,
        default: [] as any[]
      }
    },
    watch: {
      list: {
        handler(list : any[]) {
          this.cachedSize.forEach((_ : number, key : any) => {
            if (!list.includes(key)) {
              this.cachedSize.delete(key)
            }
          })
        },
        deep: true
      }
    },
    data() {
      return {
        items: [] as any[],
        containerTop: 0,
        scrollElementHeight: 0,
        placeholderHeight: 0,
        offsetThreshold: [0, 0, 0, 0], // -5, -3, 3, 5 屏对应的offset
        cachedSize: new Map<any, number>(),
        initialized: false,
        hasDefaultSize: false,
        defaultItemSize: 40,
      };
    },
    provide() {
      return {
        cachedSize: this.cachedSize
      }
    },
    created() {
      this.placeholderHeight = this.list.length * this.defaultItemSize
    },
    mounted() {
      nextTick(() => {
        uni.createSelectorQuery().in(this).select('.custom-list-view-scoll-view').boundingClientRect().exec((ret) => {
          this.scrollElementHeight = (ret[0] as NodeInfo).height!
          this.rearrange(0)
          this.initialized = true
        })
      })
    },
    methods: {
      onScroll(e : UniScrollEvent) {
        if (!this.initialized) {
          return
        }
        const scrollTop = e.detail.scrollTop
        if (scrollTop < this.offsetThreshold[1] || scrollTop > this.offsetThreshold[2]) {
          this.rearrange(scrollTop)
        }
      },
      rearrange(scrollTop : number) {
        this.offsetThreshold[0] = Math.max(scrollTop - this.scrollElementHeight * 5, 0)
        this.offsetThreshold[1] = Math.max(scrollTop - this.scrollElementHeight * 3, 0)
        this.offsetThreshold[2] = Math.min(scrollTop + this.scrollElementHeight * 4, this.placeholderHeight)
        this.offsetThreshold[3] = Math.min(scrollTop + this.scrollElementHeight * 6, this.placeholderHeight)
        const items = [] as any[]
        let tempTotalHeight = 0
        let containerTop = 0
        for (let i = 0; i < this.list.length; i++) {
          const item = this.list[i]
          let itemSize = this.defaultItemSize
          const cachedItemSize = this.cachedSize.get(item)
          if (cachedItemSize != null) {
            itemSize = cachedItemSize
            if (!this.hasDefaultSize) {
              this.defaultItemSize = itemSize
              this.hasDefaultSize = true
            }
          }
          tempTotalHeight += itemSize
          if (tempTotalHeight >= this.offsetThreshold[0] && tempTotalHeight <= this.offsetThreshold[3]) {
            items.push(item)
          } else if (tempTotalHeight < this.offsetThreshold[0]) {
            containerTop = tempTotalHeight
          }
        }
        this.placeholderHeight = tempTotalHeight
        this.items = items
        this.containerTop = containerTop
      }
    }
  }
</script>

<style>

</style>