// [z-paging]scroll相关模块 import u from '.././z-paging-utils' import Enum from '.././z-paging-enum' // #ifdef APP-NVUE const weexDom = weex.requireModule('dom'); // #endif export default { props: { // 使用页面滚动,默认为否,当设置为是时则使用页面的滚动而非此组件内部的scroll-view的滚动,使用页面滚动时z-paging无需设置确定的高度且对于长列表展示性能更高,但配置会略微繁琐 usePageScroll: { type: Boolean, default: u.gc('usePageScroll', false) }, // 是否可以滚动,使用内置scroll-view和nvue时有效,默认为是 scrollable: { type: Boolean, default: u.gc('scrollable', true) }, // 控制是否出现滚动条,默认为是 showScrollbar: { type: Boolean, default: u.gc('showScrollbar', true) }, // 是否允许横向滚动,默认为否 scrollX: { type: Boolean, default: u.gc('scrollX', false) }, // iOS设备上滚动到顶部时是否允许回弹效果,默认为否。关闭回弹效果后可使滚动到顶部与下拉刷新更连贯,但是有吸顶view时滚动到顶部时可能出现抖动。 scrollToTopBounceEnabled: { type: Boolean, default: u.gc('scrollToTopBounceEnabled', false) }, // iOS设备上滚动到底部时是否允许回弹效果,默认为是。 scrollToBottomBounceEnabled: { type: Boolean, default: u.gc('scrollToBottomBounceEnabled', true) }, // 在设置滚动条位置时使用动画过渡,默认为否 scrollWithAnimation: { type: Boolean, default: u.gc('scrollWithAnimation', false) }, // 值应为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素 scrollIntoView: { type: String, default: u.gc('scrollIntoView', '') }, }, data() { return { scrollTop: 0, oldScrollTop: 0, scrollLeft: 0, oldScrollLeft: 0, scrollViewStyle: {}, scrollViewContainerStyle: {}, scrollViewInStyle: {}, pageScrollTop: -1, scrollEnable: true, privateScrollWithAnimation: -1, cacheScrollNodeHeight: -1, superContentHeight: 0, } }, watch: { oldScrollTop(newVal) { !this.usePageScroll && this._scrollTopChange(newVal,false); }, pageScrollTop(newVal) { this.usePageScroll && this._scrollTopChange(newVal,true); }, usePageScroll: { handler(newVal) { this.loaded && this.autoHeight && this._setAutoHeight(!newVal); // #ifdef H5 if (newVal) { this.$nextTick(() => { const mainScrollRef = this.$refs['zp-scroll-view'].$refs.main; if (mainScrollRef) { mainScrollRef.style = {}; } }) } // #endif }, immediate: true }, finalScrollTop(newVal) { this.renderPropScrollTop = newVal < 6 ? 0 : 10; } }, computed: { finalScrollWithAnimation() { if (this.privateScrollWithAnimation !== -1) { return this.privateScrollWithAnimation === 1; } return this.scrollWithAnimation; }, finalScrollViewStyle() { if (this.superContentZIndex != 1) { this.scrollViewStyle['z-index'] = this.superContentZIndex; this.scrollViewStyle['position'] = 'relative'; } return this.scrollViewStyle; }, finalScrollTop() { return this.usePageScroll ? this.pageScrollTop : this.oldScrollTop; }, // 当前是否是旧版webview finalIsOldWebView() { return this.isOldWebView && !this.usePageScroll; }, // 当前scroll-view/list-view是否允许滚动 finalScrollable() { return this.scrollable && !this.usePageScroll && this.scrollEnable && (this.refresherCompleteScrollable ? true : this.refresherStatus !== Enum.Refresher.Complete) && (this.refresherRefreshingScrollable ? true : this.refresherStatus !== Enum.Refresher.Loading); } }, methods: { // 滚动到顶部,animate为是否展示滚动动画,默认为是 scrollToTop(animate, checkReverse = true) { // 如果是聊天记录模式并且列表倒置了,则滚动到顶部实际上是滚动到底部 if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) { this.scrollToBottom(animate, false); return; } this.$nextTick(() => { this._scrollToTop(animate, false); // #ifdef APP-NVUE if (this.nvueFastScroll && animate) { u.delay(() => { this._scrollToTop(false, false); }); } // #endif }) }, // 滚动到底部,animate为是否展示滚动动画,默认为是 scrollToBottom(animate, checkReverse = true) { // 如果是聊天记录模式并且列表倒置了,则滚动到底部实际上是滚动到顶部 if (this.useChatRecordMode && checkReverse && !this.isChatRecordModeAndNotInversion) { this.scrollToTop(animate, false); return; } this.$nextTick(() => { this._scrollToBottom(animate); // #ifdef APP-NVUE if (this.nvueFastScroll && animate) { u.delay(() => { this._scrollToBottom(false); }); } // #endif }) }, // 滚动到指定view(vue中有效)。sel为需要滚动的view的id值,不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否 scrollIntoViewById(sel, offset, animate) { this._scrollIntoView(sel, offset, animate); }, // 滚动到指定view(vue中有效)。nodeTop为需要滚动的view的top值(通过uni.createSelectorQuery()获取);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否 scrollIntoViewByNodeTop(nodeTop, offset, animate) { this.scrollTop = this.oldScrollTop; this.$nextTick(() => { this._scrollIntoViewByNodeTop(nodeTop, offset, animate); }) }, // y轴滚动到指定位置(vue中有效)。y为与顶部的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否 scrollToY(y, offset, animate) { this.scrollTop = this.oldScrollTop; this.$nextTick(() => { this._scrollToY(y, offset, animate); }) }, // x轴滚动到指定位置(非页面滚动且在vue中有效)。x为与左侧的距离,单位为px;offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否 scrollToX(x, offset, animate) { this.scrollLeft = this.oldScrollLeft; this.$nextTick(() => { this._scrollToX(x, offset, animate); }) }, // 滚动到指定view(nvue中和虚拟列表中有效)。index为需要滚动的view的index(第几个,从0开始);offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否 scrollIntoViewByIndex(index, offset, animate) { if (index >= this.realTotalData.length) { u.consoleErr('当前滚动的index超出已渲染列表长度,请先通过refreshToPage加载到对应index页并等待渲染成功后再调用此方法!'); return; } this.$nextTick(() => { // #ifdef APP-NVUE // 在nvue中,根据index获取对应节点信息并滚动到此节点位置 this._scrollIntoView(index, offset, animate); // #endif // #ifndef APP-NVUE if (this.finalUseVirtualList) { const isCellFixed = this.cellHeightMode === Enum.CellHeightMode.Fixed; u.delay(() => { if (this.finalUseVirtualList) { // 虚拟列表 + 每个cell高度完全相同模式下,此时滚动到对应index的cell就是滚动到scrollTop = cellHeight * index的位置 // 虚拟列表 + 高度是动态非固定的模式下,此时滚动到对应index的cell就是滚动到scrollTop = 缓存的cell高度数组中第index个的lastTotalHeight的位置 const scrollTop = isCellFixed ? this.virtualCellHeight * index : this.virtualHeightCacheList[index].lastTotalHeight; this.scrollToY(scrollTop, offset, animate); } }, isCellFixed ? 0 : 100) } // #endif }) }, // 滚动到指定view(nvue中有效)。view为需要滚动的view(通过`this.$refs.xxx`获取),不包含"#";offset为偏移量,单位为px;animate为是否展示滚动动画,默认为否 scrollIntoViewByView(view, offset, animate) { this._scrollIntoView(view, offset, animate); }, // 当使用页面滚动并且自定义下拉刷新时,请在页面的onPageScroll中调用此方法,告知z-paging当前的pageScrollTop,否则会导致在任意位置都可以下拉刷新 updatePageScrollTop(value) { this.pageScrollTop = value; }, // 当使用页面滚动并且设置了slot="top"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="top"的view高度动态改变时,在其高度需要更新时调用此方法 updatePageScrollTopHeight() { this._updatePageScrollTopOrBottomHeight('top'); }, // 当使用页面滚动并且设置了slot="bottom"时,默认初次加载会自动获取其高度,并使内部容器下移,当slot="bottom"的view高度动态改变时,在其高度需要更新时调用此方法 updatePageScrollBottomHeight() { this._updatePageScrollTopOrBottomHeight('bottom'); }, // 更新slot="left"和slot="right"宽度,当slot="left"或slot="right"宽度动态改变时调用 updateLeftAndRightWidth() { if (!this.finalIsOldWebView) return; this.$nextTick(() => this._updateLeftAndRightWidth(this.scrollViewContainerStyle, 'zp-page')); }, // 更新z-paging内置scroll-view的scrollTop updateScrollViewScrollTop(scrollTop, animate = true) { this._updatePrivateScrollWithAnimation(animate); this.scrollTop = this.oldScrollTop; this.$nextTick(() => { this.scrollTop = scrollTop; this.oldScrollTop = this.scrollTop; }); }, // 当滚动到顶部时 _onScrollToUpper() { this._emitScrollEvent('scrolltoupper'); this.$emit('scrollTopChange', 0); this.$nextTick(() => { this.oldScrollTop = 0; }) }, // 当滚动到底部时 _onScrollToLower(e) { (!e.detail || !e.detail.direction || e.detail.direction === 'bottom') && this.toBottomLoadingMoreEnabled && this._onLoadingMore(this.useChatRecordMode ? 'click' : 'toBottom') }, // 滚动到顶部 _scrollToTop(animate = true, isPrivate = true) { // #ifdef APP-NVUE // 在nvue中需要通过weex.scrollToElement滚动到顶部,此时在顶部插入了一个view,使得滚动到这个view位置 const el = this.$refs['zp-n-list-top-tag']; if (this.usePageScroll) { this._getNodeClientRect('zp-page-scroll-top', false).then(node => { const nodeHeight = node ? node[0].height : 0; weexDom.scrollToElement(el, { offset: -nodeHeight, animated: animate }); }); } else { if (!this.isIos && this.nvueListIs === 'scroller') { this._getNodeClientRect('zp-n-refresh-container', false).then(node => { const nodeHeight = node ? node[0].height : 0; weexDom.scrollToElement(el, { offset: -nodeHeight, animated: animate }); }); } else { weexDom.scrollToElement(el, { offset: 0, animated: animate }); } } return; // #endif if (this.usePageScroll) { this.$nextTick(() => { uni.pageScrollTo({ scrollTop: 0, duration: animate ? 100 : 0, }); }); return; } this._updatePrivateScrollWithAnimation(animate); this.scrollTop = this.oldScrollTop; this.$nextTick(() => { this.scrollTop = 0; this.oldScrollTop = this.scrollTop; }); }, // 滚动到底部 async _scrollToBottom(animate = true) { // #ifdef APP-NVUE // 在nvue中需要通过weex.scrollToElement滚动到顶部,此时在底部插入了一个view,使得滚动到这个view位置 const el = this.$refs['zp-n-list-bottom-tag']; if (el) { weexDom.scrollToElement(el, { offset: 0, animated: animate }); } else { u.consoleErr('滚动到底部失败,因为您设置了hideNvueBottomTag为true'); } return; // #endif if (this.usePageScroll) { this.$nextTick(() => { uni.pageScrollTo({ scrollTop: Number.MAX_VALUE, duration: animate ? 100 : 0, }); }); return; } try { this._updatePrivateScrollWithAnimation(animate); const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container'); const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view'); const pagingContainerH = pagingContainerNode ? pagingContainerNode[0].height : 0; const scrollViewH = scrollViewNode ? scrollViewNode[0].height : 0; if (pagingContainerH > scrollViewH) { this.scrollTop = this.oldScrollTop; this.$nextTick(() => { this.scrollTop = pagingContainerH - scrollViewH + this.virtualPlaceholderTopHeight; this.oldScrollTop = this.scrollTop; }); } } catch (e) {} }, // 滚动到指定view _scrollIntoView(sel, offset = 0, animate = false, finishCallback) { try { this.scrollTop = this.oldScrollTop; this.$nextTick(() => { // #ifdef APP-NVUE const refs = this.$parent.$refs; if (!refs) return; const dataType = Object.prototype.toString.call(sel); let el = null; if (dataType === '[object Number]') { const els = refs[`z-paging-${sel}`]; el = els ? els[0] : null; } else if (dataType === '[object Array]') { el = sel[0]; } else { el = sel; } if (el) { weexDom.scrollToElement(el, { offset: -offset, animated: animate }); } else { u.consoleErr('在nvue中滚动到指定位置,cell必须设置 :ref="`z-paging-${index}`"'); } return; // #endif this._getNodeClientRect('#' + sel.replace('#', ''), this.$parent).then((node) => { if (node) { let nodeTop = node[0].top; this._scrollIntoViewByNodeTop(nodeTop, offset, animate); finishCallback && finishCallback(); } }); }); } catch (e) {} }, // 通过nodeTop滚动到指定view _scrollIntoViewByNodeTop(nodeTop, offset = 0, animate = false) { // 如果是聊天记录模式并且列表倒置了,此时nodeTop需要等于scroll-view高度 - nodeTop if (this.isChatRecordModeAndInversion) { this._getNodeClientRect('.zp-scroll-view').then(sNode => { if (sNode) { this._scrollToY(sNode[0].height - nodeTop, offset, animate, true); } }) } else { this._scrollToY(nodeTop, offset, animate, true); } }, // y轴滚动到指定位置 _scrollToY(y, offset = 0, animate = false, addScrollTop = false) { this._updatePrivateScrollWithAnimation(animate); u.delay(() => { if (this.usePageScroll) { if (addScrollTop && this.pageScrollTop !== -1) { y += this.pageScrollTop; } const scrollTop = y - offset; uni.pageScrollTo({ scrollTop, duration: animate ? 100 : 0 }); } else { if (addScrollTop) { y += this.oldScrollTop; } this.scrollTop = y - offset; } }, 10) }, // x轴滚动到指定位置 _scrollToX(x, offset = 0, animate = false) { this._updatePrivateScrollWithAnimation(animate); u.delay(() => { if (!this.usePageScroll) { this.scrollLeft = x - offset; } else { u.consoleErr('使用页面滚动时不支持scrollToX'); } }, 10) }, // scroll-view滚动中 _scroll(e) { this.$emit('scroll', e); const { scrollTop, scrollLeft } = e.detail; // #ifndef APP-NVUE this.finalUseVirtualList && this._updateVirtualScroll(scrollTop, this.oldScrollTop - scrollTop); // #endif this.oldScrollTop = scrollTop; this.oldScrollLeft = scrollLeft; // 滚动区域内容的总高度 - 当前滚动的scrollTop = 当前滚动区域的顶部与内容底部的距离 const scrollDiff = e.detail.scrollHeight - this.oldScrollTop; // 在非ios平台滚动中,再次验证一下是否滚动到了底部。因为在一些安卓设备中,有概率滚动到底部不触发@scrolltolower事件,因此添加双重检测逻辑 !this.isIos && this._checkScrolledToBottom(scrollDiff); }, // emit scrolltolower/scrolltoupper事件 _emitScrollEvent(type) { const reversedType = type === 'scrolltolower' ? 'scrolltoupper' : 'scrolltolower'; const eventType = this.useChatRecordMode && !this.isChatRecordModeAndNotInversion ? reversedType : type; this.$emit(eventType); }, // 更新内置的scroll-view是否启用滚动动画 _updatePrivateScrollWithAnimation(animate) { this.privateScrollWithAnimation = animate ? 1 : 0; u.delay(() => this.$nextTick(() => { // 在滚动结束后将滚动动画状态设置回初始状态 this.privateScrollWithAnimation = -1; }), 100, 'updateScrollWithAnimationDelay') }, // 检测scrollView是否要铺满屏幕 _doCheckScrollViewShouldFullHeight(totalData) { if (this.autoFullHeight && this.usePageScroll && this.isTotalChangeFromAddData) { // #ifndef APP-NVUE this.$nextTick(() => { this._checkScrollViewShouldFullHeight((scrollViewNode, pagingContainerNode) => { this._preCheckShowNoMoreInside(totalData, scrollViewNode, pagingContainerNode) }); }) // #endif // #ifdef APP-NVUE this._preCheckShowNoMoreInside(totalData) // #endif } else { this._preCheckShowNoMoreInside(totalData) } }, // 检测z-paging是否要全屏覆盖(当使用页面滚动并且不满全屏时,默认z-paging需要铺满全屏,避免数据过少时内部的empty-view无法正确展示) async _checkScrollViewShouldFullHeight(callback) { try { const scrollViewNode = await this._getNodeClientRect('.zp-scroll-view'); const pagingContainerNode = await this._getNodeClientRect('.zp-paging-container-content'); if (!scrollViewNode || !pagingContainerNode) return; const scrollViewHeight = pagingContainerNode[0].height; const scrollViewTop = scrollViewNode[0].top; if (this.isAddedData && scrollViewHeight + scrollViewTop <= this.windowHeight) { this._setAutoHeight(true, scrollViewNode); callback(scrollViewNode, pagingContainerNode); } else { this._setAutoHeight(false); callback(null, null); } } catch (e) { callback(null, null); } }, // 更新缓存中z-paging整个内容容器高度 async _updateCachedSuperContentHeight() { const superContentNode = await this._getNodeClientRect('.z-paging-content'); if (superContentNode) { this.superContentHeight = superContentNode[0].height; } }, // scrollTop改变时触发 _scrollTopChange(newVal, isPageScrollTop){ this.$emit('scrollTopChange', newVal); this.$emit('update:scrollTop', newVal); this._checkShouldShowBackToTop(newVal); // 之前在安卓中scroll-view有概率滚动到顶部时scrollTop不为0导致下拉刷新判断异常,因此判断scrollTop在105之内都允许下拉刷新,但此方案会导致某些情况(例如滚动到距离顶部10px处)下拉抖动,因此改为通过获取zp-scroll-view的节点信息中的scrollTop进行验证的方案 // const scrollTop = this.isIos ? (newVal > 5 ? 6 : 0) : (newVal > 105 ? 106 : (newVal > 5 ? 6 : 0)); const scrollTop = newVal > 5 ? 6 : 0; if (isPageScrollTop && this.wxsPageScrollTop !== scrollTop) { this.wxsPageScrollTop = scrollTop; } else if (!isPageScrollTop && this.wxsScrollTop !== scrollTop) { this.wxsScrollTop = scrollTop; if (scrollTop > 6) { this.scrollEnable = true; } } }, // 更新使用页面滚动时slot="top"或"bottom"插入view的高度 _updatePageScrollTopOrBottomHeight(type) { // #ifndef APP-NVUE if (!this.usePageScroll) return; // #endif this._doCheckScrollViewShouldFullHeight(this.realTotalData); const node = `.zp-page-${type}`; const marginText = `margin${type.slice(0,1).toUpperCase() + type.slice(1)}`; let safeAreaInsetBottomAdd = this.safeAreaInsetBottom; this.$nextTick(() => { let delayTime = 0; // #ifdef MP-BAIDU || APP-NVUE delayTime = 50; // #endif u.delay(() => { this._getNodeClientRect(node).then((res) => { if (res) { let pageScrollNodeHeight = res[0].height; if (type === 'bottom') { if (safeAreaInsetBottomAdd) { pageScrollNodeHeight += this.safeAreaBottom; } } else { this.cacheTopHeight = pageScrollNodeHeight; } this.$set(this.scrollViewStyle, marginText, `${pageScrollNodeHeight}px`); } else if (safeAreaInsetBottomAdd) { this.$set(this.scrollViewStyle, marginText, `${this.safeAreaBottom}px`); } }); }, delayTime) }) }, } }