feat: 卡片滑动时的动画效果
This commit is contained in:
@@ -12,7 +12,13 @@
|
||||
v-for="slot in renderSlots"
|
||||
:key="slot.key"
|
||||
class="swiper-card"
|
||||
:class="[`is-${slot.role}`, { 'is-current': slot.role === 'current' }]"
|
||||
:class="[
|
||||
`is-${slot.role}`,
|
||||
{
|
||||
'is-current': slot.role === 'current',
|
||||
'is-fill-fade': slot.key === fillFadeKey,
|
||||
},
|
||||
]"
|
||||
:style="slot.style"
|
||||
@tap="handleCardTap(slot)"
|
||||
>
|
||||
@@ -62,6 +68,7 @@ const emit = defineEmits(["update:modelValue", "change", "didSelectItem"]);
|
||||
|
||||
const DURATION = 280;
|
||||
const RECYCLE_FRAME_DELAY = 32;
|
||||
const FILL_FADE_DURATION = 240;
|
||||
const CLICK_THRESHOLD = 8;
|
||||
const SWIPE_THRESHOLD = 60;
|
||||
|
||||
@@ -79,11 +86,13 @@ const isAnimating = ref(false);
|
||||
const isRecycling = ref(false);
|
||||
const isTapCandidate = ref(false);
|
||||
const swipeStep = ref(0);
|
||||
const fillFadeKey = ref("");
|
||||
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let settleTimer = null;
|
||||
let recycleTimer = null;
|
||||
let fillFadeTimer = null;
|
||||
const instance = getCurrentInstance();
|
||||
const hasExternalModel = Object.prototype.hasOwnProperty.call(
|
||||
instance?.vnode.props || {},
|
||||
@@ -131,6 +140,13 @@ function clearRecycleTimer() {
|
||||
}
|
||||
}
|
||||
|
||||
function clearFillFadeTimer() {
|
||||
if (fillFadeTimer) {
|
||||
clearTimeout(fillFadeTimer);
|
||||
fillFadeTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
const getCircularDistance = (from, to, total) => {
|
||||
const forward = normalizeIndex(to - from, total);
|
||||
const backward = normalizeIndex(from - to, total);
|
||||
@@ -174,6 +190,8 @@ watch(
|
||||
isDragging.value = false;
|
||||
isAnimating.value = false;
|
||||
isRecycling.value = false;
|
||||
fillFadeKey.value = "";
|
||||
clearFillFadeTimer();
|
||||
if (hasExternalModel) {
|
||||
syncActiveCursorByActualIndex(props.modelValue);
|
||||
} else {
|
||||
@@ -392,7 +410,9 @@ const finishSwipe = (step) => {
|
||||
clearSettleTimer();
|
||||
settleTimer = setTimeout(() => {
|
||||
const nextCursor = normalizeIndex(activeCursor.value + step, virtualCount.value);
|
||||
const fillIndex = normalizeIndex(nextCursor + step, virtualCount.value);
|
||||
isRecycling.value = true;
|
||||
fillFadeKey.value = getItemKey(fillIndex);
|
||||
activeCursor.value = nextCursor;
|
||||
const actualIndex = getActualIndex(nextCursor);
|
||||
emit("update:modelValue", actualIndex);
|
||||
@@ -403,6 +423,11 @@ const finishSwipe = (step) => {
|
||||
isRecycling.value = false;
|
||||
recycleTimer = null;
|
||||
}, RECYCLE_FRAME_DELAY);
|
||||
clearFillFadeTimer();
|
||||
fillFadeTimer = setTimeout(() => {
|
||||
fillFadeKey.value = "";
|
||||
fillFadeTimer = null;
|
||||
}, FILL_FADE_DURATION);
|
||||
}, DURATION);
|
||||
};
|
||||
|
||||
@@ -462,12 +487,14 @@ const handleTouchCancel = () => {
|
||||
if (!canSwipe.value) return;
|
||||
clearSettleTimer();
|
||||
clearRecycleTimer();
|
||||
clearFillFadeTimer();
|
||||
deltaX.value = 0;
|
||||
isDragging.value = false;
|
||||
isAnimating.value = false;
|
||||
isRecycling.value = false;
|
||||
isTapCandidate.value = false;
|
||||
swipeStep.value = 0;
|
||||
fillFadeKey.value = "";
|
||||
};
|
||||
|
||||
const handleCardTap = (slot) => {
|
||||
@@ -478,6 +505,7 @@ const handleCardTap = (slot) => {
|
||||
onBeforeUnmount(() => {
|
||||
clearSettleTimer();
|
||||
clearRecycleTimer();
|
||||
clearFillFadeTimer();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.swiper-card.is-fill-fade .card-shell {
|
||||
animation: card-fill-fade-in 240ms ease-out both;
|
||||
}
|
||||
|
||||
.card-shell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
@@ -95,3 +99,13 @@
|
||||
.is-single .swiper-stage {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
@keyframes card-fill-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user