UNPKG

miniprogram-recycle-view

Version:
735 lines (729 loc) 22.6 kB
/* eslint complexity: ["error", {"max": 50}] */ /* eslint-disable indent */ const DEFAULT_SHOW_SCREENS = 4 const RECT_SIZE = 200 const systemInfo = wx.getSystemInfoSync() const DEBUG = false const transformRpx = require('./utils/transformRpx.js').transformRpx Component({ options: { multipleSlots: true // 在组件定义时的选项中启用多slot支持 }, relations: { '../recycle-item/recycle-item': { type: 'child', // 关联的目标节点应为子节点 linked(target) { // 检查第一个的尺寸就好了吧 if (!this._hasCheckSize) { this._hasCheckSize = true const size = this.boundingClientRect(this._pos.beginIndex) if (!size) { return } setTimeout(() => { try { target.createSelectorQuery().select('.wx-recycle-item').boundingClientRect((rect) => { if (rect && (rect.width !== size.width || rect.height !== size.height)) { // eslint-disable-next-line no-console console.warn('[recycle-view] the size in <recycle-item> is not the same with param ' + `itemSize, expect {width: ${rect.width}, height: ${rect.height}} but got ` + `{width: ${size.width}, height: ${size.height}}`) } }).exec() } catch (e) { // do nothing } }, 10) } } } }, /** * 组件的属性列表 */ properties: { debug: { type: Boolean, value: false }, scrollY: { type: Boolean, value: true, }, batch: { type: Boolean, value: false, public: true, observer: '_recycleInnerBatchDataChanged' }, batchKey: { type: String, value: 'batchSetRecycleData', public: true, }, scrollTop: { type: Number, value: 0, public: true, observer: '_scrollTopChanged', observeAssignments: true }, height: { type: Number, value: systemInfo.windowHeight, public: true, observer: '_heightChanged' }, width: { type: Number, value: systemInfo.windowWidth, public: true, observer: '_widthChanged' }, // 距顶部/左边多远时,触发bindscrolltoupper upperThreshold: { type: Number, value: 50, public: true, }, // 距底部/右边多远时,触发bindscrolltolower lowerThreshold: { type: Number, value: 50, public: true, }, scrollToIndex: { type: Number, public: true, value: 0, observer: '_scrollToIndexChanged', observeAssignments: true }, scrollWithAnimation: { type: Boolean, public: true, value: false }, enableBackToTop: { type: Boolean, public: true, value: false }, // 是否节流,默认是 throttle: { type: Boolean, public: true, value: true }, placeholderImage: { type: String, public: true, value: '' }, screen: { // 默认渲染多少屏的数据 type: Number, public: true, value: DEFAULT_SHOW_SCREENS } }, /** * 组件的初始数据 */ data: { innerBeforeHeight: 0, innerAfterHeight: 0, innerScrollTop: 0, innerScrollIntoView: '', placeholderImageStr: '', totalHeight: 0, useInPage: false }, attached() { if (this.data.placeholderImage) { this.setData({ placeholderImageStr: transformRpx(this.data.placeholderImage, true) }) } this.setItemSize({ array: [], map: {}, totalHeight: 0 }) }, ready() { this._initPosition(() => { this._isReady = true // DOM结构ready了 // 有一个更新的timer在了 if (this._updateTimerId) return this._scrollViewDidScroll({ detail: { scrollLeft: this._pos.left, scrollTop: this._pos.top, ignoreScroll: true } }, true) }) }, detached() { this.page = null // 销毁对应的RecycleContext if (this.context) { this.context.destroy() this.context = null } }, /** * 组件的方法列表 */ methods: { _log(...args) { if (!DEBUG && !this.data.debug) return const h = new Date() const str = `${h.getHours()}:${h.getMinutes()}:${h.getSeconds()}.${h.getMilliseconds()}` Array.prototype.splice.call(args, 0, 0, str) // eslint-disable-next-line no-console console.log(...args) }, _scrollToUpper(e) { this.triggerEvent('scrolltoupper', e.detail) }, _scrollToLower(e) { this.triggerEvent('scrolltolower', e.detail) }, _beginToScroll() { if (!this._lastScrollTop) { this._lastScrollTop = this._pos && (this._pos.top || 0) } }, _clearList(cb) { this.currentScrollTop = 0 this._lastScrollTop = 0 const pos = this._pos pos.beginIndex = this._pos.endIndex = -1 pos.afterHeight = pos.minTop = pos.maxTop = 0 this.page._recycleViewportChange({ detail: { data: pos, id: this.id } }, cb) }, // 判断RecycleContext是否Ready _isValid() { return this.page && this.context && this.context.isDataReady }, // eslint-disable-next-line no-complexity _scrollViewDidScroll(e, force) { // 如果RecycleContext还没有初始化, 不做任何事情 if (!this._isValid()) { return } // 监测白屏时间 if (!e.detail.ignoreScroll) { this.triggerEvent('scroll', e.detail) } this.currentScrollTop = e.detail.scrollTop // 高度为0的情况, 不做任何渲染逻辑 if (!this._pos.height || !this.sizeArray.length) { // 没有任何数据的情况下, 直接清理所有的状态 this._clearList(e.detail.cb) return } // 在scrollWithAnimation动画最后会触发一次scroll事件, 这次scroll事件必须要被忽略 if (this._isScrollingWithAnimation) { this._isScrollingWithAnimation = false return } const pos = this._pos const that = this const scrollLeft = e.detail.scrollLeft const scrollTop = e.detail.scrollTop const scrollDistance = Math.abs(scrollTop - this._lastScrollTop) if (!force && (Math.abs(scrollTop - pos.top) < pos.height * 1.5)) { this._log('【not exceed height') return } this._lastScrollTop = scrollTop const SHOW_SCREENS = this.data.screen // 固定4屏幕 this._log('SHOW_SCREENS', SHOW_SCREENS, scrollTop) this._calcViewportIndexes(scrollLeft, scrollTop, (beginIndex, endIndex, minTop, afterHeight, maxTop) => { that._log('scrollDistance', scrollDistance, 'indexes', beginIndex, endIndex) // 渲染的数据不变 if (!force && pos.beginIndex === beginIndex && pos.endIndex === endIndex && pos.minTop === minTop && pos.afterHeight === afterHeight) { that._log('------------is the same beginIndex and endIndex') return } // 如果这次渲染的范围比上一次的范围小,则忽略 that._log('【check】before setData, old pos is', pos.minTop, pos.maxTop, minTop, maxTop) that._throttle = false pos.left = scrollLeft pos.top = scrollTop pos.beginIndex = beginIndex pos.endIndex = endIndex // console.log('render indexes', endIndex - beginIndex + 1, endIndex, beginIndex) pos.minTop = minTop pos.maxTop = maxTop pos.afterHeight = afterHeight pos.ignoreBeginIndex = pos.ignoreEndIndex = -1 that.page._recycleViewportChange({ detail: { data: that._pos, id: that.id } }, () => { if (e.detail.cb) { e.detail.cb() } }) }) }, // 计算在视窗内渲染的数据 _calcViewportIndexes(left, top, cb) { const that = this // const st = +new Date this._getBeforeSlotHeight(() => { const { beginIndex, endIndex, minTop, afterHeight, maxTop } = that.__calcViewportIndexes(left, top) if (cb) { cb(beginIndex, endIndex, minTop, afterHeight, maxTop) } }) }, _getBeforeSlotHeight(cb) { if (typeof this.data.beforeSlotHeight !== 'undefined') { if (cb) { cb(this.data.beforeSlotHeight) } } else { this.reRender(cb) } }, _getAfterSlotHeight(cb) { if (typeof this.data.afterSlotHeight !== 'undefined') { if (cb) { cb(this.data.afterSlotHeight) } // cb && cb(this.data.afterSlotHeight) } else { this.reRender(cb) } }, _getIndexes(minTop, maxTop) { if (minTop === maxTop && maxTop === 0) { return { beginIndex: -1, endIndex: -1 } } const startLine = Math.floor(minTop / RECT_SIZE) const endLine = Math.ceil(maxTop / RECT_SIZE) const rectEachLine = Math.floor(this.data.width / RECT_SIZE) let beginIndex let endIndex const sizeMap = this.sizeMap for (let i = startLine; i <= endLine; i++) { for (let col = 0; col < rectEachLine; col++) { const key = `${i}.${col}` // 找到sizeMap里面的最小值和最大值即可 if (!sizeMap[key]) continue for (let j = 0; j < sizeMap[key].length; j++) { if (typeof beginIndex === 'undefined') { beginIndex = endIndex = sizeMap[key][j] continue } if (beginIndex > sizeMap[key][j]) { beginIndex = sizeMap[key][j] } else if (endIndex < sizeMap[key][j]) { endIndex = sizeMap[key][j] } } } } return { beginIndex, endIndex } }, _isIndexValid(beginIndex, endIndex) { if (typeof beginIndex === 'undefined' || beginIndex === -1 || typeof endIndex === 'undefined' || endIndex === -1 || endIndex >= this.sizeArray.length) { return false } return true }, __calcViewportIndexes(left, top) { if (!this.sizeArray.length) return {} const pos = this._pos if (typeof left === 'undefined') { (left = pos.left) } if (typeof top === 'undefined') { (top = pos.top) } // top = Math.max(top, this.data.beforeSlotHeight) const beforeSlotHeight = this.data.beforeSlotHeight || 0 // 和direction无关了 const SHOW_SCREENS = this.data.screen let minTop = top - pos.height * SHOW_SCREENS - beforeSlotHeight let maxTop = top + pos.height * SHOW_SCREENS - beforeSlotHeight // maxTop或者是minTop超出了范围 if (maxTop > this.totalHeight) { minTop -= (maxTop - this.totalHeight) maxTop = this.totalHeight } if (minTop < beforeSlotHeight) { maxTop += Math.min(beforeSlotHeight - minTop, this.totalHeight) minTop = 0 } // 计算落在minTop和maxTop之间的方格有哪些 const indexObj = this._getIndexes(minTop, maxTop) const beginIndex = indexObj.beginIndex let endIndex = indexObj.endIndex if (endIndex >= this.sizeArray.length) { endIndex = this.sizeArray.length - 1 } // 校验一下beginIndex和endIndex的有效性, if (!this._isIndexValid(beginIndex, endIndex)) { return { beginIndex: -1, endIndex: -1, minTop: 0, afterHeight: 0, maxTop: 0 } } // 计算白屏的默认占位的区域 const maxTopFull = this.sizeArray[endIndex].beforeHeight + this.sizeArray[endIndex].height const minTopFull = this.sizeArray[beginIndex].beforeHeight // console.log('render indexes', beginIndex, endIndex) const afterHeight = this.totalHeight - maxTopFull return { beginIndex, endIndex, minTop: minTopFull, // 取整, beforeHeight的距离 afterHeight, maxTop, } }, setItemSize(size) { this.sizeArray = size.array this.sizeMap = size.map if (size.totalHeight !== this.totalHeight) { // console.log('---totalHeight is', size.totalHeight); this.setData({ totalHeight: size.totalHeight, useInPage: this.useInPage || false }) } this.totalHeight = size.totalHeight }, setList(key, newList) { this._currentSetDataKey = key this._currentSetDataList = newList }, setPage(page) { this.page = page }, forceUpdate(cb, reInit) { if (!this._isReady) { if (this._updateTimerId) { // 合并多次的forceUpdate clearTimeout(this._updateTimerId) } this._updateTimerId = setTimeout(() => { this.forceUpdate(cb, reInit) }, 10) return } this._updateTimerId = null const that = this if (reInit) { this.reRender(() => { that._scrollViewDidScroll({ detail: { scrollLeft: that._pos.left, scrollTop: that.currentScrollTop || that.data.scrollTop || 0, ignoreScroll: true, cb } }, true) }) } else { this._scrollViewDidScroll({ detail: { scrollLeft: that._pos.left, scrollTop: that.currentScrollTop || that.data.scrollTop || 0, ignoreScroll: true, cb } }, true) } }, _initPosition(cb) { const that = this that._pos = { left: that.data.scrollLeft || 0, top: that.data.scrollTop || 0, width: this.data.width, height: Math.max(500, this.data.height), // 一个屏幕的高度 direction: 0 } this.reRender(cb) }, _widthChanged(newVal) { if (!this._isReady) return newVal this._pos.width = newVal this.forceUpdate() return newVal }, _heightChanged(newVal) { if (!this._isReady) return newVal this._pos.height = Math.max(500, newVal) this.forceUpdate() return newVal }, reRender(cb) { let beforeSlotHeight let afterSlotHeight const that = this // const reRenderStart = Date.now() function newCb() { if (that._lastBeforeSlotHeight !== beforeSlotHeight || that._lastAfterSlotHeight !== afterSlotHeight) { that.setData({ hasBeforeSlotHeight: true, hasAfterSlotHeight: true, beforeSlotHeight, afterSlotHeight }) } that._lastBeforeSlotHeight = beforeSlotHeight that._lastAfterSlotHeight = afterSlotHeight // console.log('_getBeforeSlotHeight use time', Date.now() - reRenderStart) if (cb) { cb() } } // 重新渲染事件发生 let beforeReady = false let afterReady = false // fix:#16 确保获取slot节点实际高度 this.setData({ hasBeforeSlotHeight: false, hasAfterSlotHeight: false, }, () => { this.createSelectorQuery().select('.slot-before').boundingClientRect((rect) => { beforeSlotHeight = rect.height beforeReady = true if (afterReady) { if (newCb) { newCb() } } }).exec() this.createSelectorQuery().select('.slot-after').boundingClientRect((rect) => { afterSlotHeight = rect.height afterReady = true if (beforeReady) { if (newCb) { newCb() } } }).exec() }) }, _setInnerBeforeAndAfterHeight(obj) { if (typeof obj.beforeHeight !== 'undefined') { this._tmpBeforeHeight = obj.beforeHeight } if (obj.afterHeight) { this._tmpAfterHeight = obj.afterHeight } }, _recycleInnerBatchDataChanged(cb) { if (typeof this._tmpBeforeHeight !== 'undefined') { const setObj = { innerBeforeHeight: this._tmpBeforeHeight || 0, innerAfterHeight: this._tmpAfterHeight || 0 } if (typeof this._tmpInnerScrollTop !== 'undefined') { setObj.innerScrollTop = this._tmpInnerScrollTop } const pageObj = {} let hasPageData = false if (typeof this._currentSetDataKey !== 'undefined') { pageObj[this._currentSetDataKey] = this._currentSetDataList hasPageData = true } const saveScrollWithAnimation = this.data.scrollWithAnimation const groupSetData = () => { // 如果有分页数据的话 if (hasPageData) { this.page.setData(pageObj) } this.setData(setObj, () => { this.setData({ scrollWithAnimation: saveScrollWithAnimation }) if (typeof cb === 'function') { cb() } }) } groupSetData() delete this._currentSetDataKey delete this._currentSetDataList this._tmpBeforeHeight = undefined this._tmpAfterHeight = undefined this._tmpInnerScrollTop = undefined } }, _renderByScrollTop(scrollTop) { // 先setData把目标位置的数据补齐 this._scrollViewDidScroll({ detail: { scrollLeft: this._pos.scrollLeft, scrollTop, ignoreScroll: true } }, true) if (this.data.scrollWithAnimation) { this._isScrollingWithAnimation = true } this.setData({ innerScrollTop: scrollTop }) }, _scrollTopChanged(newVal, oldVal) { // if (newVal === oldVal && newVal === 0) return if (!this._isInitScrollTop && newVal === 0) { this._isInitScrollTop = true return newVal } this.currentScrollTop = newVal if (!this._isReady) { if (this._scrollTopTimerId) { clearTimeout(this._scrollTopTimerId) } this._scrollTopTimerId = setTimeout(() => { this._scrollTopChanged(newVal, oldVal) }, 10) return newVal } this._isInitScrollTop = true this._scrollTopTimerId = null // this._lastScrollTop = oldVal if (typeof this._lastScrollTop === 'undefined') { this._lastScrollTop = this.data.scrollTop } // 滑动距离小于一个屏幕的高度, 直接setData if (Math.abs(newVal - this._lastScrollTop) < this._pos.height) { this.setData({ innerScrollTop: newVal }) return newVal } if (!this._isScrollTopChanged) { // 首次的值需要延后一点执行才能生效 setTimeout(() => { this._isScrollTopChanged = true this._renderByScrollTop(newVal) }, 10) } else { this._renderByScrollTop(newVal) } return newVal }, _scrollToIndexChanged(newVal, oldVal) { // if (newVal === oldVal && newVal === 0) return // 首次滚动到0的不执行 if (!this._isInitScrollToIndex && newVal === 0) { this._isInitScrollToIndex = true return newVal } if (!this._isReady) { if (this._scrollToIndexTimerId) { clearTimeout(this._scrollToIndexTimerId) } this._scrollToIndexTimerId = setTimeout(() => { this._scrollToIndexChanged(newVal, oldVal) }, 10) return newVal } this._isInitScrollToIndex = true this._scrollToIndexTimerId = null if (typeof this._lastScrollTop === 'undefined') { this._lastScrollTop = this.data.scrollTop } const rect = this.boundingClientRect(newVal) if (!rect) return newVal // console.log('rect top', rect, this.data.beforeSlotHeight) const calScrollTop = rect.top + (this.data.beforeSlotHeight || 0) this.currentScrollTop = calScrollTop if (Math.abs(calScrollTop - this._lastScrollTop) < this._pos.height) { this.setData({ innerScrollTop: calScrollTop }) return newVal } if (!this._isScrollToIndexChanged) { setTimeout(() => { this._isScrollToIndexChanged = true this._renderByScrollTop(calScrollTop) }, 10) } else { this._renderByScrollTop(calScrollTop) } return newVal }, // 提供给开发者使用的接口 boundingClientRect(idx) { if (idx < 0 || idx >= this.sizeArray.length) { return null } return { left: 0, top: this.sizeArray[idx].beforeHeight, width: this.sizeArray[idx].width, height: this.sizeArray[idx].height } }, // 获取当前出现在屏幕内数据项, 返回数据项组成的数组 // 参数inViewportPx表示当数据项至少有多少像素出现在屏幕内才算是出现在屏幕内,默认是1 getIndexesInViewport(inViewportPx) { if (!inViewportPx) { (inViewportPx = 1) } const scrollTop = this.currentScrollTop let minTop = scrollTop + inViewportPx if (minTop < 0) minTop = 0 let maxTop = scrollTop + this.data.height - inViewportPx if (maxTop > this.totalHeight) maxTop = this.totalHeight const indexes = [] for (let i = 0; i < this.sizeArray.length; i++) { if (this.sizeArray[i].beforeHeight + this.sizeArray[i].height >= minTop && this.sizeArray[i].beforeHeight <= maxTop) { indexes.push(i) } if (this.sizeArray[i].beforeHeight > maxTop) break } return indexes }, getTotalHeight() { return this.totalHeight }, setUseInPage(useInPage) { this.useInPage = useInPage }, setPlaceholderImage(svgs, size) { const fill = 'style=\'fill:rgb(204,204,204);\'' const placeholderImages = [`data:image/svg+xml,%3Csvg height='${size.height}' width='${size.width}' xmlns='http://www.w3.org/2000/svg'%3E`] svgs.forEach(svg => { placeholderImages.push(`%3Crect width='${svg.width}' x='${svg.left}' height='${svg.height}' y='${svg.top}' ${fill} /%3E`) }) placeholderImages.push('%3C/svg%3E') this.setData({ placeholderImageStr: placeholderImages.join('') }) } } })