custom-list-view.uvue 5.1 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
<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
37 38 39
      },
      defaultItemSize() {
        this.rearrange(this.lastScrollTop)
40 41 42 43 44 45 46 47 48 49 50 51 52
      }
    },
    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,
53
        lastScrollTop: 0,
54
        rearrangeQueue: [] as number[]
55 56 57 58
      };
    },
    provide() {
      return {
59 60 61 62 63 64 65
        setCachedSize: (item : any, size : number) => {
          if (!this.hasDefaultSize) {
            this.defaultItemSize = size
            this.hasDefaultSize = true
          }
          this.cachedSize.set(item, size)
        }
66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
      }
    },
    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
86 87 88 89
        if(scrollTop === this.lastScrollTop || scrollTop < 0) {
          return
        }
        this.lastScrollTop = scrollTop
90
        if (scrollTop < this.offsetThreshold[1] || scrollTop > this.offsetThreshold[2]) {
91
          this.queue(scrollTop)
92 93
        }
      },
94 95 96 97 98 99 100 101 102 103 104 105 106
      queue(scrollTop : number) {
        /*
         * rearrange内为大量同步逻辑,在上次rearrange未执行完毕的情况下将后续多个rearrange合并成一次执行,即仅执行最后一次
         * 由于滚动机制差异,此优化仅在web端才有意义。
         * 如何测试:push后console.log(this.rearrangeQueue.length) 输出结果大于1时触发优化
         */
        this.rearrangeQueue.push(scrollTop)
        setTimeout(() => {
          this.flush()
        }, 1)
      },
      flush() {
        const queueLength = this.rearrangeQueue.length
107
        if (queueLength == 0) {
108 109 110 111 112 113
          return
        }
        const lastScrollTop = this.rearrangeQueue[queueLength - 1]
        this.rearrange(lastScrollTop)
        this.rearrangeQueue = [] as number[]
      },
114
      rearrange(scrollTop : number) {
115
        this.offsetThreshold[0] = Math.max(scrollTop - this.scrollElementHeight * 5, 0)
116 117
        this.offsetThreshold[1] = Math.max(scrollTop - this.scrollElementHeight * 3, 0)
        this.offsetThreshold[2] = Math.min(scrollTop + this.scrollElementHeight * 4, this.placeholderHeight)
118 119 120
        this.offsetThreshold[3] = Math.min(scrollTop + this.scrollElementHeight * 6, this.placeholderHeight)
        const offsetStart = this.offsetThreshold[0]
        const offsetEnd =this.offsetThreshold[3]
121
        const items = [] as any[]
122 123 124
        const defaultItemSize = this.defaultItemSize
        const cachedSize = this.cachedSize
        const list = this.list
125 126
        let tempTotalHeight = 0
        let containerTop = 0
127 128 129 130 131
        let start = false, end = false
        for (let i = 0; i < list.length; i++) {
          const item = list[i]
          let itemSize = defaultItemSize
          const cachedItemSize = cachedSize.get(item)
132 133 134 135
          if (cachedItemSize != null) {
            itemSize = cachedItemSize
          }
          tempTotalHeight += itemSize
136 137 138 139
          if (end) {
            continue
          }
          if (tempTotalHeight < offsetStart) {
140
            containerTop = tempTotalHeight
141 142 143 144 145 146 147 148 149
          } else if (tempTotalHeight >= offsetStart && tempTotalHeight <= offsetEnd) {
            if (start == false) {
              start = true
            }
            items.push(item)
          } else {
            if (!end) {
              end = true
            }
150 151 152 153 154 155 156 157 158 159 160 161
          }
        }
        this.placeholderHeight = tempTotalHeight
        this.items = items
        this.containerTop = containerTop
      }
    }
  }
</script>

<style>

162
</style>