|
|
@@ -3,13 +3,30 @@
|
|
|
:class="classes"
|
|
|
:scroll-y="true"
|
|
|
:style="{ height: `${getContainerHeight}px` }"
|
|
|
+ scroll-top="0"
|
|
|
@scroll="handleScrollEvent"
|
|
|
ref="list"
|
|
|
+ :id="'list' + refRandomId"
|
|
|
>
|
|
|
- <div class="nut-list-phantom" :style="{ height: listHeight + 'px' }"></div>
|
|
|
- <div class="nut-list-container" :style="{ transform: getTransform }">
|
|
|
- <div class="nut-list-item" :style="{ height: height + 'px' }" v-for="(item, index) in visibleData" :key="item">
|
|
|
- <slot :item="item" :index="index"></slot>
|
|
|
+ <div
|
|
|
+ class="nut-list-phantom"
|
|
|
+ :style="{ height: phantomHeight + 'px' }"
|
|
|
+ ref="phantom"
|
|
|
+ :id="'phantom' + refRandomId"
|
|
|
+ ></div>
|
|
|
+ <div
|
|
|
+ class="nut-list-container"
|
|
|
+ :style="{ transform: getTransform() }"
|
|
|
+ ref="actualContent"
|
|
|
+ :id="'actualContent' + refRandomId"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ class="nut-list-item"
|
|
|
+ v-for="(item, index) in visibleData"
|
|
|
+ :key="item"
|
|
|
+ :id="'list-item-' + Number(index + start)"
|
|
|
+ >
|
|
|
+ <slot :item="item" :index="index + start"></slot>
|
|
|
</div>
|
|
|
</div>
|
|
|
</Nut-Scroll-View>
|
|
|
@@ -19,8 +36,11 @@ import { reactive, toRefs, computed, ref, Ref, watch, ComputedRef } from 'vue';
|
|
|
import { createComponent } from '@/packages/utils/create';
|
|
|
import NutScrollView from '../scrollView/index.taro.vue';
|
|
|
import Taro from '@tarojs/taro';
|
|
|
+import { useTaroRect } from '@/packages/utils/useTaroRect';
|
|
|
+import { CachedPosition, CompareResult, binarySearch } from './type';
|
|
|
const { componentName, create } = createComponent('list');
|
|
|
const clientHeight = Taro.getSystemInfoSync().windowHeight || 667;
|
|
|
+
|
|
|
export default create({
|
|
|
components: {
|
|
|
NutScrollView
|
|
|
@@ -36,19 +56,37 @@ export default create({
|
|
|
return [];
|
|
|
}
|
|
|
},
|
|
|
+ bufferSize: {
|
|
|
+ type: Number,
|
|
|
+ default: 5
|
|
|
+ },
|
|
|
containerHeight: {
|
|
|
type: [Number],
|
|
|
default: clientHeight
|
|
|
+ },
|
|
|
+ estimateRowHeight: {
|
|
|
+ type: Number,
|
|
|
+ default: 80
|
|
|
+ },
|
|
|
+ margin: {
|
|
|
+ type: Number,
|
|
|
+ default: 10
|
|
|
}
|
|
|
},
|
|
|
- emits: ['scroll', 'scroll-bottom'],
|
|
|
+ emits: ['scroll-up', 'scroll-down', 'scroll-bottom'],
|
|
|
|
|
|
setup(props, { emit }) {
|
|
|
const list = ref(null) as Ref;
|
|
|
+ const phantom = ref(null) as Ref;
|
|
|
+ const actualContent = ref(null) as Ref;
|
|
|
+ const refRandomId = Math.random().toString(36).slice(-8);
|
|
|
const state = reactive({
|
|
|
- startOffset: 0,
|
|
|
start: 0,
|
|
|
- list: props.listData.slice()
|
|
|
+ originStartIndex: 0,
|
|
|
+ scrollTop: 0,
|
|
|
+ list: props.listData.slice(),
|
|
|
+ cachePositions: [] as CachedPosition[],
|
|
|
+ phantomHeight: props.estimateRowHeight * props.listData.length
|
|
|
});
|
|
|
|
|
|
const getContainerHeight = computed(() => {
|
|
|
@@ -56,15 +94,11 @@ export default create({
|
|
|
});
|
|
|
|
|
|
const visibleCount = computed(() => {
|
|
|
- return Math.ceil(getContainerHeight.value / props.height);
|
|
|
+ return Math.ceil(getContainerHeight.value / props.estimateRowHeight);
|
|
|
});
|
|
|
|
|
|
const end = computed(() => {
|
|
|
- return state.start + visibleCount.value;
|
|
|
- });
|
|
|
-
|
|
|
- const getTransform = computed(() => {
|
|
|
- return `translate3d(0, ${state.startOffset}px, 0)`;
|
|
|
+ return Math.min(state.originStartIndex + visibleCount.value + props.bufferSize, state.list.length - 1);
|
|
|
});
|
|
|
|
|
|
const classes = computed(() => {
|
|
|
@@ -74,37 +108,166 @@ export default create({
|
|
|
};
|
|
|
});
|
|
|
|
|
|
- const listHeight = computed(() => {
|
|
|
- return state.list.length * props.height;
|
|
|
- });
|
|
|
-
|
|
|
const visibleData: ComputedRef = computed(() => {
|
|
|
- return state.list.slice(state.start, Math.min(end.value, state.list.length));
|
|
|
+ return state.list.slice(state.start, end.value);
|
|
|
});
|
|
|
|
|
|
+ const getTransform = () => {
|
|
|
+ if (actualContent.value) {
|
|
|
+ return `translate3d(0, ${state.start >= 1 ? state.cachePositions[state.start - 1].bottom : 0}px, 0)`;
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const initCachedPosition = () => {
|
|
|
+ state.cachePositions = [];
|
|
|
+ for (let i = 0; i < state.list.length; ++i) {
|
|
|
+ state.cachePositions[i] = {
|
|
|
+ index: i,
|
|
|
+ height: props.estimateRowHeight,
|
|
|
+ top: i * props.estimateRowHeight,
|
|
|
+ bottom: (i + 1) * (props.estimateRowHeight + props.margin),
|
|
|
+ dValue: 0
|
|
|
+ };
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ const updateCachedPosition = () => {
|
|
|
+ let nodes: any[] = actualContent.value.childNodes;
|
|
|
+ nodes = Array.from(nodes).filter((node: HTMLDivElement) => node.nodeType === 1);
|
|
|
+ const start = nodes[0];
|
|
|
+ nodes.forEach(async (node: HTMLDivElement, index: number) => {
|
|
|
+ if (!node) return;
|
|
|
+ const rect = await useTaroRect(node, Taro);
|
|
|
+ if (rect && rect.height) {
|
|
|
+ const { height } = rect;
|
|
|
+ const oldHeight = state.cachePositions[index + state.start]
|
|
|
+ ? state.cachePositions[index + state.start].height
|
|
|
+ : props.height;
|
|
|
+ const dValue = oldHeight - height;
|
|
|
+
|
|
|
+ if (dValue && state.cachePositions[index + state.start]) {
|
|
|
+ state.cachePositions[index + state.start].bottom -= dValue;
|
|
|
+ state.cachePositions[index + state.start].height = height;
|
|
|
+ state.cachePositions[index + state.start].dValue = dValue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ let startIndex = 0;
|
|
|
+ if (start) {
|
|
|
+ startIndex = state.start;
|
|
|
+ }
|
|
|
+
|
|
|
+ const cachedPositionsLen = state.cachePositions.length;
|
|
|
+ let cumulativeDiffHeight = state.cachePositions[startIndex].dValue;
|
|
|
+ state.cachePositions[startIndex].dValue = 0;
|
|
|
+
|
|
|
+ for (let i = startIndex + 1; i < cachedPositionsLen; ++i) {
|
|
|
+ const item = state.cachePositions[i];
|
|
|
+
|
|
|
+ state.cachePositions[i].top = state.cachePositions[i - 1].bottom;
|
|
|
+ state.cachePositions[i].bottom = state.cachePositions[i].bottom - cumulativeDiffHeight;
|
|
|
+
|
|
|
+ if (item.dValue !== 0) {
|
|
|
+ cumulativeDiffHeight += item.dValue;
|
|
|
+ item.dValue = 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ const height = state.cachePositions[cachedPositionsLen - 1].bottom;
|
|
|
+
|
|
|
+ state.phantomHeight = height;
|
|
|
+ };
|
|
|
+
|
|
|
+ const getStartIndex = (scrollTop = 0) => {
|
|
|
+ let idx = binarySearch<CachedPosition, number>(
|
|
|
+ state.cachePositions,
|
|
|
+ scrollTop,
|
|
|
+ (currentValue: CachedPosition, targetValue: number) => {
|
|
|
+ const currentCompareValue = currentValue.bottom;
|
|
|
+ if (currentCompareValue === targetValue) {
|
|
|
+ return CompareResult.eq;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (currentCompareValue < targetValue) {
|
|
|
+ return CompareResult.lt;
|
|
|
+ }
|
|
|
+
|
|
|
+ return CompareResult.gt;
|
|
|
+ }
|
|
|
+ ) as number;
|
|
|
+
|
|
|
+ const targetItem = state.cachePositions[idx];
|
|
|
+
|
|
|
+ if (targetItem.bottom < scrollTop) {
|
|
|
+ idx += 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ return idx;
|
|
|
+ };
|
|
|
+
|
|
|
+ const resetAllVirtualParam = () => {
|
|
|
+ state.originStartIndex = 0;
|
|
|
+ state.start = 0;
|
|
|
+ state.scrollTop = 0;
|
|
|
+ list.value.scrollTop = 0;
|
|
|
+ initCachedPosition();
|
|
|
+ state.phantomHeight = props.estimateRowHeight * state.list.length;
|
|
|
+ };
|
|
|
+
|
|
|
const handleScrollEvent = async (e: any) => {
|
|
|
const scrollTop = Math.max(e.detail ? e.detail.scrollTop : e.target.scrollTop, 0.1);
|
|
|
- state.start = Math.floor(scrollTop / props.height);
|
|
|
- if (end.value >= state.list.length) {
|
|
|
- emit('scroll-bottom');
|
|
|
+ const { originStartIndex } = state;
|
|
|
+ const currentIndex = getStartIndex(scrollTop);
|
|
|
+ if (currentIndex !== originStartIndex) {
|
|
|
+ state.originStartIndex = currentIndex;
|
|
|
+ state.start = Math.max(state.originStartIndex - props.bufferSize, 0);
|
|
|
+ if (end.value >= state.list.length - 1) {
|
|
|
+ emit('scroll-bottom');
|
|
|
+ }
|
|
|
}
|
|
|
- state.startOffset = scrollTop - (scrollTop % props.height);
|
|
|
+ emit(scrollTop > state.scrollTop ? 'scroll-up' : 'scroll-down', scrollTop);
|
|
|
+ state.scrollTop = scrollTop;
|
|
|
};
|
|
|
|
|
|
watch(
|
|
|
() => props.listData,
|
|
|
+ (val: any[]) => {
|
|
|
+ state.list = val.slice();
|
|
|
+ if (state.list.length === val.length) {
|
|
|
+ setTimeout(() => {
|
|
|
+ initCachedPosition();
|
|
|
+ updateCachedPosition();
|
|
|
+ }, 200);
|
|
|
+ } else {
|
|
|
+ resetAllVirtualParam();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ watch(
|
|
|
+ () => state.start,
|
|
|
() => {
|
|
|
- state.list = props.listData.slice();
|
|
|
+ if (actualContent.value && state.list.length > 0) {
|
|
|
+ Taro.nextTick(() => {
|
|
|
+ setTimeout(() => {
|
|
|
+ updateCachedPosition();
|
|
|
+ }, 200);
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
);
|
|
|
|
|
|
return {
|
|
|
...toRefs(state),
|
|
|
list,
|
|
|
+ phantom,
|
|
|
+ actualContent,
|
|
|
getTransform,
|
|
|
- listHeight,
|
|
|
visibleData,
|
|
|
classes,
|
|
|
+ refRandomId,
|
|
|
getContainerHeight,
|
|
|
handleScrollEvent
|
|
|
};
|