|
@@ -0,0 +1,213 @@
|
|
|
|
+import { ref, unref, watch, watchEffect, onMounted, onBeforeUnmount } from 'vue'
|
|
|
|
+import TWEEN from '@tweenjs/tween.js'
|
|
|
|
+
|
|
|
|
+export default function useSmoothSwipe({
|
|
|
|
+ scrollTargetRef,
|
|
|
|
+ maxTranslateLength,
|
|
|
|
+ viewportWidth = document.documentElement.clientWidth,
|
|
|
|
+ anchorPosList,
|
|
|
|
+ initialAnchorIdx = 0,
|
|
|
|
+} = {}) {
|
|
|
|
+
|
|
|
|
+ const isOperating = ref(false)
|
|
|
|
+ const haveSwipedThisTime = ref(false)
|
|
|
|
+ let hasOperatedThisTimeTimeoutId = null
|
|
|
|
+
|
|
|
|
+ let translateLength = ref(anchorPosList[initialAnchorIdx])
|
|
|
|
+ let maxTranslateLengthInner = Infinity
|
|
|
|
+
|
|
|
|
+ let animationFrameId = null
|
|
|
|
+
|
|
|
|
+ let tween = null
|
|
|
|
+ let isTweening = false
|
|
|
|
+
|
|
|
|
+ let fingerPosXWhenTouchStart = 0
|
|
|
|
+
|
|
|
|
+ let anchorPosListInner = [...anchorPosList].sort((a, b) => {
|
|
|
|
+ return a > b
|
|
|
|
+ })
|
|
|
|
+ let anchorPosListInnerReverse = [...anchorPosListInner].reverse()
|
|
|
|
+ const currentAnchorIdx = ref(initialAnchorIdx)
|
|
|
|
+ const goingToAnchorIdx = ref(null)
|
|
|
|
+
|
|
|
|
+ watch(isOperating, (v) => {
|
|
|
|
+ if (v) {
|
|
|
|
+ clearTimeout(hasOperatedThisTimeTimeoutId)
|
|
|
|
+ haveSwipedThisTime.value = false
|
|
|
|
+ } else {
|
|
|
|
+ clearTimeout(hasOperatedThisTimeTimeoutId)
|
|
|
|
+ hasOperatedThisTimeTimeoutId = setTimeout(() => {
|
|
|
|
+ haveSwipedThisTime.value = false
|
|
|
|
+ }, 333)
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ // compute maxTranslateLengthInner todo: 不能用if包裹吧?
|
|
|
|
+ if (maxTranslateLength) {
|
|
|
|
+ watchEffect(() => {
|
|
|
|
+ maxTranslateLengthInner = unref(maxTranslateLength) - unref(viewportWidth)
|
|
|
|
+ if (maxTranslateLengthInner < 0) {
|
|
|
|
+ maxTranslateLengthInner = 0
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ } else if (scrollTargetRef) {
|
|
|
|
+ watchEffect(() => {
|
|
|
|
+ if (scrollTargetRef.value) {
|
|
|
|
+ maxTranslateLengthInner = scrollTargetRef.value.scrollWidth - unref(viewportWidth)
|
|
|
|
+ if (maxTranslateLengthInner < 0) {
|
|
|
|
+ maxTranslateLengthInner = 0
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ maxTranslateLengthInner = Infinity
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function onTouchStart(e) {
|
|
|
|
+ isOperating.value = true
|
|
|
|
+ fingerPosXWhenTouchStart = e.changedTouches[0].pageX
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const onTouchMove = (event) => {
|
|
|
|
+ if (isTweening) {
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ let currentX = event.changedTouches[0].pageX
|
|
|
|
+ let dX = currentX - fingerPosXWhenTouchStart
|
|
|
|
+ if (dX < -1) { // 左滑,要往右移动
|
|
|
|
+ const destinationIdx = anchorPosListInner.findIndex((item) => {
|
|
|
|
+ return item - translateLength.value > 1
|
|
|
|
+ })
|
|
|
|
+ if ((destinationIdx !== -1) && !isTweening) {
|
|
|
|
+ isTweening = true
|
|
|
|
+ tween = new TWEEN.Tween(translateLength)
|
|
|
|
+ tween.to({
|
|
|
|
+ value: anchorPosListInner[destinationIdx],
|
|
|
|
+ }, 1200)
|
|
|
|
+ tween.easing(TWEEN.Easing.Cubic.InOut)
|
|
|
|
+ tween.start()
|
|
|
|
+ goingToAnchorIdx.value = destinationIdx
|
|
|
|
+ tween.onComplete(() => {
|
|
|
|
+ isTweening = false
|
|
|
|
+ currentAnchorIdx.value = destinationIdx
|
|
|
|
+ goingToAnchorIdx.value = null
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ } else if (dX > 1) {
|
|
|
|
+ const destinationIdx = anchorPosListInnerReverse.findIndex((item) => {
|
|
|
|
+ return item - translateLength.value < -1
|
|
|
|
+ })
|
|
|
|
+ if ((destinationIdx !== -1) && !isTweening) {
|
|
|
|
+ isTweening = true
|
|
|
|
+ tween = new TWEEN.Tween(translateLength)
|
|
|
|
+ tween.to({
|
|
|
|
+ value: anchorPosListInnerReverse[destinationIdx],
|
|
|
|
+ }, 1200)
|
|
|
|
+ tween.easing(TWEEN.Easing.Cubic.InOut)
|
|
|
|
+ tween.start()
|
|
|
|
+ goingToAnchorIdx.value = anchorPosList.length - destinationIdx - 1
|
|
|
|
+ tween.onComplete(() => {
|
|
|
|
+ isTweening = false
|
|
|
|
+ currentAnchorIdx.value = anchorPosList.length - destinationIdx - 1
|
|
|
|
+ goingToAnchorIdx.value = null
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const onSwipeLeft = () => {
|
|
|
|
+ // console.log('在左滑')
|
|
|
|
+ if (isTweening) {
|
|
|
|
+ return
|
|
|
|
+ }
|
|
|
|
+ const destinationIdx = anchorPosListInner.findIndex((item) => {
|
|
|
|
+ return item - translateLength.value > 1
|
|
|
|
+ })
|
|
|
|
+ if ((destinationIdx !== -1) && !isTweening) {
|
|
|
|
+ isTweening = true
|
|
|
|
+ tween = new TWEEN.Tween(translateLength)
|
|
|
|
+ tween.to({
|
|
|
|
+ value: anchorPosListInner[destinationIdx],
|
|
|
|
+ }, 1200)
|
|
|
|
+ tween.easing(TWEEN.Easing.Cubic.InOut)
|
|
|
|
+ tween.start()
|
|
|
|
+ goingToAnchorIdx.value = destinationIdx
|
|
|
|
+ tween.onComplete(() => {
|
|
|
|
+ isTweening = false
|
|
|
|
+ currentAnchorIdx.value = destinationIdx
|
|
|
|
+ goingToAnchorIdx.value = null
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ const onswipeRight = () => {
|
|
|
|
+ // console.log('在左滑')
|
|
|
|
+ const destinationIdx = anchorPosListInnerReverse.findIndex((item) => {
|
|
|
|
+ return item - translateLength.value < -1
|
|
|
|
+ })
|
|
|
|
+ if ((destinationIdx !== -1) && !isTweening) {
|
|
|
|
+ isTweening = true
|
|
|
|
+ tween = new TWEEN.Tween(translateLength)
|
|
|
|
+ tween.to({
|
|
|
|
+ value: anchorPosListInnerReverse[destinationIdx],
|
|
|
|
+ }, 1200)
|
|
|
|
+ tween.easing(TWEEN.Easing.Cubic.InOut)
|
|
|
|
+ tween.start()
|
|
|
|
+ goingToAnchorIdx.value = anchorPosList.length - destinationIdx - 1
|
|
|
|
+ tween.onComplete(() => {
|
|
|
|
+ isTweening = false
|
|
|
|
+ currentAnchorIdx.value = anchorPosList.length - destinationIdx - 1
|
|
|
|
+ goingToAnchorIdx.value = null
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function onTouchEnd() {
|
|
|
|
+ isOperating.value = false
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ function onTouchCancel() {
|
|
|
|
+ isOperating.value = false
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 在每一帧更新镜头速度、位置
|
|
|
|
+ function animationFrameTask() {
|
|
|
|
+ TWEEN.update()
|
|
|
|
+ animationFrameId = requestAnimationFrame(animationFrameTask)
|
|
|
|
+ }
|
|
|
|
+ onMounted(() => {
|
|
|
|
+ animationFrameId = requestAnimationFrame(animationFrameTask)
|
|
|
|
+ })
|
|
|
|
+ onBeforeUnmount(() => {
|
|
|
|
+ cancelAnimationFrame(animationFrameId)
|
|
|
|
+ tween && tween.stop()
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ if (scrollTargetRef) { // todo: 不能用if包裹吧?
|
|
|
|
+ watch(scrollTargetRef, (v) => {
|
|
|
|
+ if (v) {
|
|
|
|
+ v.addEventListener('touchstart', onTouchStart, { passive: true })
|
|
|
|
+ v.addEventListener('touchmove', onTouchMove, { passive: true })
|
|
|
|
+ v.addEventListener('touchend', onTouchEnd)
|
|
|
|
+ v.addEventListener('touchcancel', onTouchCancel)
|
|
|
|
+ }
|
|
|
|
+ }, {
|
|
|
|
+ immediate: true,
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return {
|
|
|
|
+ onTouchStart,
|
|
|
|
+ onTouchMove,
|
|
|
|
+ onTouchEnd,
|
|
|
|
+ onSwipeLeft,
|
|
|
|
+ onswipeRight,
|
|
|
|
+ onTouchCancel,
|
|
|
|
+ translateLength,
|
|
|
|
+ haveSwipedThisTime,
|
|
|
|
+ currentAnchorIdx,
|
|
|
|
+ goingToAnchorIdx,
|
|
|
|
+ }
|
|
|
|
+}
|