feat:修复滑动卡片 闪烁的问题
This commit is contained in:
@@ -61,6 +61,7 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(["update:modelValue", "change", "didSelectItem"]);
|
const emit = defineEmits(["update:modelValue", "change", "didSelectItem"]);
|
||||||
|
|
||||||
const DURATION = 280;
|
const DURATION = 280;
|
||||||
|
const RECYCLE_FRAME_DELAY = 32;
|
||||||
const CLICK_THRESHOLD = 8;
|
const CLICK_THRESHOLD = 8;
|
||||||
const SWIPE_THRESHOLD = 60;
|
const SWIPE_THRESHOLD = 60;
|
||||||
|
|
||||||
@@ -75,12 +76,14 @@ const activeCursor = ref(0);
|
|||||||
const deltaX = ref(0);
|
const deltaX = ref(0);
|
||||||
const isDragging = ref(false);
|
const isDragging = ref(false);
|
||||||
const isAnimating = ref(false);
|
const isAnimating = ref(false);
|
||||||
|
const isRecycling = ref(false);
|
||||||
const isTapCandidate = ref(false);
|
const isTapCandidate = ref(false);
|
||||||
const swipeStep = ref(0);
|
const swipeStep = ref(0);
|
||||||
|
|
||||||
let startX = 0;
|
let startX = 0;
|
||||||
let startY = 0;
|
let startY = 0;
|
||||||
let settleTimer = null;
|
let settleTimer = null;
|
||||||
|
let recycleTimer = null;
|
||||||
const instance = getCurrentInstance();
|
const instance = getCurrentInstance();
|
||||||
const hasExternalModel = Object.prototype.hasOwnProperty.call(
|
const hasExternalModel = Object.prototype.hasOwnProperty.call(
|
||||||
instance?.vnode.props || {},
|
instance?.vnode.props || {},
|
||||||
@@ -89,10 +92,11 @@ const hasExternalModel = Object.prototype.hasOwnProperty.call(
|
|||||||
|
|
||||||
const actualCount = computed(() => props.list.length);
|
const actualCount = computed(() => props.list.length);
|
||||||
const virtualCount = computed(() => {
|
const virtualCount = computed(() => {
|
||||||
if (actualCount.value === 1) return 3;
|
if (actualCount.value <= 1) return actualCount.value;
|
||||||
|
if (actualCount.value === 2) return 4;
|
||||||
return actualCount.value;
|
return actualCount.value;
|
||||||
});
|
});
|
||||||
const canSwipe = computed(() => virtualCount.value > 1);
|
const canSwipe = computed(() => actualCount.value > 1);
|
||||||
const progress = computed(() => {
|
const progress = computed(() => {
|
||||||
if (!canSwipe.value) return 0;
|
if (!canSwipe.value) return 0;
|
||||||
return clamp(deltaX.value / sideOffset, -1, 1);
|
return clamp(deltaX.value / sideOffset, -1, 1);
|
||||||
@@ -113,10 +117,6 @@ const getItemByVirtualIndex = (virtualIndex) => {
|
|||||||
return props.list[getActualIndex(virtualIndex)] || null;
|
return props.list[getActualIndex(virtualIndex)] || null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const syncActiveCursor = (incomingIndex = 0) => {
|
|
||||||
activeCursor.value = normalizeIndex(incomingIndex, virtualCount.value);
|
|
||||||
};
|
|
||||||
|
|
||||||
function clearSettleTimer() {
|
function clearSettleTimer() {
|
||||||
if (settleTimer) {
|
if (settleTimer) {
|
||||||
clearTimeout(settleTimer);
|
clearTimeout(settleTimer);
|
||||||
@@ -124,17 +124,61 @@ function clearSettleTimer() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearRecycleTimer() {
|
||||||
|
if (recycleTimer) {
|
||||||
|
clearTimeout(recycleTimer);
|
||||||
|
recycleTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCircularDistance = (from, to, total) => {
|
||||||
|
const forward = normalizeIndex(to - from, total);
|
||||||
|
const backward = normalizeIndex(from - to, total);
|
||||||
|
return Math.min(forward, backward);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNearestVirtualIndexByActualIndex = (actualIndex, anchor = activeCursor.value) => {
|
||||||
|
if (!actualCount.value || !virtualCount.value) return 0;
|
||||||
|
|
||||||
|
const targetActualIndex = normalizeIndex(actualIndex, actualCount.value);
|
||||||
|
let matchedIndex = targetActualIndex;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
for (let index = 0; index < virtualCount.value; index += 1) {
|
||||||
|
if (getActualIndex(index) !== targetActualIndex) continue;
|
||||||
|
|
||||||
|
const distance = getCircularDistance(anchor, index, virtualCount.value);
|
||||||
|
if (distance < minDistance) {
|
||||||
|
matchedIndex = index;
|
||||||
|
minDistance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchedIndex;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncActiveCursorByVirtualIndex = (incomingIndex = 0) => {
|
||||||
|
activeCursor.value = normalizeIndex(incomingIndex, virtualCount.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncActiveCursorByActualIndex = (incomingIndex = 0) => {
|
||||||
|
activeCursor.value = getNearestVirtualIndexByActualIndex(incomingIndex);
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.list,
|
() => props.list,
|
||||||
() => {
|
() => {
|
||||||
clearSettleTimer();
|
clearSettleTimer();
|
||||||
|
clearRecycleTimer();
|
||||||
deltaX.value = 0;
|
deltaX.value = 0;
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
isAnimating.value = false;
|
isAnimating.value = false;
|
||||||
const nextCursor = hasExternalModel
|
isRecycling.value = false;
|
||||||
? props.modelValue
|
if (hasExternalModel) {
|
||||||
: activeCursor.value;
|
syncActiveCursorByActualIndex(props.modelValue);
|
||||||
syncActiveCursor(nextCursor);
|
} else {
|
||||||
|
syncActiveCursorByVirtualIndex(activeCursor.value);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ deep: true, immediate: true }
|
{ deep: true, immediate: true }
|
||||||
);
|
);
|
||||||
@@ -144,11 +188,11 @@ watch(
|
|||||||
(value) => {
|
(value) => {
|
||||||
if (!hasExternalModel) return;
|
if (!hasExternalModel) return;
|
||||||
if (isAnimating.value || isDragging.value) return;
|
if (isAnimating.value || isDragging.value) return;
|
||||||
syncActiveCursor(value);
|
syncActiveCursorByActualIndex(value);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const getItemKey = (virtualIndex, role) => {
|
const getItemKey = (virtualIndex) => {
|
||||||
const item = getItemByVirtualIndex(virtualIndex) || {};
|
const item = getItemByVirtualIndex(virtualIndex) || {};
|
||||||
const baseKey =
|
const baseKey =
|
||||||
item.id ??
|
item.id ??
|
||||||
@@ -157,7 +201,7 @@ const getItemKey = (virtualIndex, role) => {
|
|||||||
item.title ??
|
item.title ??
|
||||||
virtualIndex;
|
virtualIndex;
|
||||||
|
|
||||||
return `${baseKey}-${virtualIndex}-${role}`;
|
return `${baseKey}-${virtualIndex}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const states = {
|
const states = {
|
||||||
@@ -256,7 +300,7 @@ const buildCardStyle = (role) => {
|
|||||||
transform: `translate3d(-50%, 0, 0) translateX(${state.x}px) scale(${state.scale})`,
|
transform: `translate3d(-50%, 0, 0) translateX(${state.x}px) scale(${state.scale})`,
|
||||||
opacity: state.opacity,
|
opacity: state.opacity,
|
||||||
zIndex: getCardZIndex(role),
|
zIndex: getCardZIndex(role),
|
||||||
transition: isDragging.value
|
transition: isDragging.value || isRecycling.value
|
||||||
? "none"
|
? "none"
|
||||||
: `transform ${DURATION}ms ease, opacity ${DURATION}ms ease`,
|
: `transform ${DURATION}ms ease, opacity ${DURATION}ms ease`,
|
||||||
};
|
};
|
||||||
@@ -287,32 +331,44 @@ const getMaskOpacity = (role) => {
|
|||||||
|
|
||||||
const getMaskStyle = (role) => ({
|
const getMaskStyle = (role) => ({
|
||||||
opacity: getMaskOpacity(role),
|
opacity: getMaskOpacity(role),
|
||||||
transition: isDragging.value ? "none" : "opacity 120ms ease-out",
|
transition: isDragging.value || isRecycling.value ? "none" : "opacity 120ms ease-out",
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderSlots = computed(() => {
|
const renderSlots = computed(() => {
|
||||||
if (!actualCount.value) return [];
|
if (!actualCount.value) return [];
|
||||||
|
|
||||||
|
if (!canSwipe.value) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
key: getItemKey(activeCursor.value),
|
||||||
|
role: "current",
|
||||||
|
index: getActualIndex(activeCursor.value),
|
||||||
|
item: getItemByVirtualIndex(activeCursor.value),
|
||||||
|
style: buildCardStyle("current"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
const prevIndex = normalizeIndex(activeCursor.value - 1, virtualCount.value);
|
const prevIndex = normalizeIndex(activeCursor.value - 1, virtualCount.value);
|
||||||
const nextIndex = normalizeIndex(activeCursor.value + 1, virtualCount.value);
|
const nextIndex = normalizeIndex(activeCursor.value + 1, virtualCount.value);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
key: getItemKey(prevIndex, "prev"),
|
key: getItemKey(prevIndex),
|
||||||
role: "prev",
|
role: "prev",
|
||||||
index: getActualIndex(prevIndex),
|
index: getActualIndex(prevIndex),
|
||||||
item: getItemByVirtualIndex(prevIndex),
|
item: getItemByVirtualIndex(prevIndex),
|
||||||
style: buildCardStyle("prev"),
|
style: buildCardStyle("prev"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: getItemKey(activeCursor.value, "current"),
|
key: getItemKey(activeCursor.value),
|
||||||
role: "current",
|
role: "current",
|
||||||
index: getActualIndex(activeCursor.value),
|
index: getActualIndex(activeCursor.value),
|
||||||
item: getItemByVirtualIndex(activeCursor.value),
|
item: getItemByVirtualIndex(activeCursor.value),
|
||||||
style: buildCardStyle("current"),
|
style: buildCardStyle("current"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: getItemKey(nextIndex, "next"),
|
key: getItemKey(nextIndex),
|
||||||
role: "next",
|
role: "next",
|
||||||
index: getActualIndex(nextIndex),
|
index: getActualIndex(nextIndex),
|
||||||
item: getItemByVirtualIndex(nextIndex),
|
item: getItemByVirtualIndex(nextIndex),
|
||||||
@@ -336,16 +392,22 @@ const finishSwipe = (step) => {
|
|||||||
clearSettleTimer();
|
clearSettleTimer();
|
||||||
settleTimer = setTimeout(() => {
|
settleTimer = setTimeout(() => {
|
||||||
const nextCursor = normalizeIndex(activeCursor.value + step, virtualCount.value);
|
const nextCursor = normalizeIndex(activeCursor.value + step, virtualCount.value);
|
||||||
|
isRecycling.value = true;
|
||||||
activeCursor.value = nextCursor;
|
activeCursor.value = nextCursor;
|
||||||
const actualIndex = getActualIndex(nextCursor);
|
const actualIndex = getActualIndex(nextCursor);
|
||||||
emit("update:modelValue", actualIndex);
|
emit("update:modelValue", actualIndex);
|
||||||
emit("change", actualIndex);
|
emit("change", actualIndex);
|
||||||
resetGesture();
|
resetGesture();
|
||||||
|
clearRecycleTimer();
|
||||||
|
recycleTimer = setTimeout(() => {
|
||||||
|
isRecycling.value = false;
|
||||||
|
recycleTimer = null;
|
||||||
|
}, RECYCLE_FRAME_DELAY);
|
||||||
}, DURATION);
|
}, DURATION);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTouchStart = (event) => {
|
const handleTouchStart = (event) => {
|
||||||
if (!canSwipe.value || isAnimating.value) return;
|
if (!canSwipe.value || isAnimating.value || isRecycling.value) return;
|
||||||
|
|
||||||
const touch = event.touches?.[0] || event.changedTouches?.[0];
|
const touch = event.touches?.[0] || event.changedTouches?.[0];
|
||||||
if (!touch) return;
|
if (!touch) return;
|
||||||
@@ -399,20 +461,23 @@ const handleTouchEnd = () => {
|
|||||||
const handleTouchCancel = () => {
|
const handleTouchCancel = () => {
|
||||||
if (!canSwipe.value) return;
|
if (!canSwipe.value) return;
|
||||||
clearSettleTimer();
|
clearSettleTimer();
|
||||||
|
clearRecycleTimer();
|
||||||
deltaX.value = 0;
|
deltaX.value = 0;
|
||||||
isDragging.value = false;
|
isDragging.value = false;
|
||||||
isAnimating.value = false;
|
isAnimating.value = false;
|
||||||
|
isRecycling.value = false;
|
||||||
isTapCandidate.value = false;
|
isTapCandidate.value = false;
|
||||||
swipeStep.value = 0;
|
swipeStep.value = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCardTap = (slot) => {
|
const handleCardTap = (slot) => {
|
||||||
if (slot.role !== "current" || isDragging.value || isAnimating.value) return;
|
if (slot.role !== "current" || isDragging.value || isAnimating.value || isRecycling.value) return;
|
||||||
emit("didSelectItem", slot.item, slot.index);
|
emit("didSelectItem", slot.item, slot.index);
|
||||||
};
|
};
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearSettleTimer();
|
clearSettleTimer();
|
||||||
|
clearRecycleTimer();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user