useSmoothSwipe.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. /*
  2. 简单使用时:
  3. <template>
  4. <div class="relic-list">
  5. <div
  6. ref="listEl"
  7. class="the-list"
  8. >
  9. <button @click="onClickBtn"></button>
  10. <div class="content">
  11. slflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsdfsdflsdflkslkfsjdfsdfslflsfdjsflksfdlkskdfsfdlsfdlsdflsfksdfsd123456789over!
  12. </div>
  13. </div>
  14. </div>
  15. </template>
  16. <script setup>
  17. import { ref, computed, watch, watchEffect, onMounted } from "vue"
  18. import useSmoothSwipe from '@/useFunctions/useSmoothSwipe.js'
  19. import { useWindowSize } from '@vueuse/core'
  20. const listEl = ref(null)
  21. const { width: windowWidth, height: windowHeight } = useWindowSize()
  22. const { haveSwipedThisTime, translateLength } = useSmoothSwipe({
  23. scrollTargetRef: listEl,
  24. viewportWidth: windowWidth,
  25. })
  26. function onClickBtn(e) {
  27. if (!haveSwipedThisTime.value) {
  28. console.log('button click!')
  29. }
  30. }
  31. </script>
  32. */
  33. import { ref, unref, watch, watchEffect, onMounted, onBeforeUnmount } from 'vue'
  34. export default function useSmoothSwipe({
  35. scrollTargetRef,
  36. maxTranslateLength,
  37. viewportWidth = document.documentElement.clientWidth,
  38. speedFactor = 1,
  39. damping = 0.003,
  40. enableAutoMoving = false
  41. } = {}) {
  42. const isOperating = ref(false)
  43. const haveSwipedThisTime = ref(false)
  44. let haveSwipedThisTimeTimeoutId = null
  45. let lastMoveEventTimeStamp = 0
  46. let lastCursorPos = 0
  47. let moveSpeed = ref(0)
  48. let translateLength = ref(0)
  49. let maxTranslateLengthInner = Infinity
  50. let isAutoMoving = ref(false)
  51. let beginAutoMoveIntervalId = null
  52. let lastAnimationTimeStamp = 0
  53. let animationFrameId = null
  54. //
  55. watch(moveSpeed, (v) => {
  56. if (!haveSwipedThisTime.value && v) {
  57. haveSwipedThisTime.value = true
  58. }
  59. })
  60. watch(isOperating, (v) => {
  61. if (v) {
  62. clearTimeout(haveSwipedThisTimeTimeoutId)
  63. haveSwipedThisTime.value = false
  64. } else {
  65. clearTimeout(haveSwipedThisTimeTimeoutId)
  66. haveSwipedThisTimeTimeoutId = setTimeout(() => {
  67. haveSwipedThisTime.value = false
  68. }, 333)
  69. }
  70. })
  71. // compute maxTranslateLengthInner
  72. watchEffect(() => {
  73. if (maxTranslateLength) {
  74. maxTranslateLengthInner = unref(maxTranslateLength) - unref(viewportWidth)
  75. if (maxTranslateLengthInner < 0) {
  76. maxTranslateLengthInner = 0
  77. }
  78. }
  79. })
  80. function computeMaxTranslateLengthInnerByScrollTarget() {
  81. maxTranslateLengthInner = scrollTargetRef.value.scrollWidth - unref(viewportWidth)
  82. }
  83. function onWheel(e) {
  84. moveSpeed.value += e.deltaY / 200
  85. }
  86. function speedUp(v) {
  87. console.log('sdf')
  88. moveSpeed.value += v
  89. }
  90. /**
  91. * swipe begin
  92. */
  93. function onMouseDown(e) {
  94. isOperating.value = true
  95. moveSpeed.value = 0
  96. lastMoveEventTimeStamp = 0
  97. lastAnimationTimeStamp = Date.now()
  98. lastCursorPos = e.clientX
  99. if (beginAutoMoveIntervalId) {
  100. clearInterval(beginAutoMoveIntervalId)
  101. beginAutoMoveIntervalId = null
  102. }
  103. if (isAutoMoving.value) {
  104. isAutoMoving.value = false
  105. }
  106. }
  107. function onTouchStart(e) {
  108. isOperating.value = true
  109. moveSpeed.value = 0
  110. lastMoveEventTimeStamp = 0
  111. lastAnimationTimeStamp = Date.now()
  112. lastCursorPos = e.changedTouches[0].clientX
  113. if (beginAutoMoveIntervalId) {
  114. clearInterval(beginAutoMoveIntervalId)
  115. beginAutoMoveIntervalId = null
  116. }
  117. if (isAutoMoving.value) {
  118. isAutoMoving.value = false
  119. }
  120. }
  121. /**
  122. * swiping
  123. */
  124. function onMouseMove(e) {
  125. if (isOperating.value) {
  126. // 疯狂操作的极端情况下两个时间戳之间的时差会不合理,甚至为0
  127. if (lastMoveEventTimeStamp && (e.timeStamp - lastMoveEventTimeStamp > 1)) {
  128. // 更新speed
  129. const currentMoveSpeed = - (e.clientX - lastCursorPos) / (e.timeStamp - lastMoveEventTimeStamp) * unref(speedFactor)
  130. moveSpeed.value = moveSpeed.value * 0.9 + currentMoveSpeed * 0.1
  131. lastCursorPos = e.clientX
  132. }
  133. lastMoveEventTimeStamp = e.timeStamp
  134. }
  135. }
  136. function onTouchMove(e) {
  137. if (isOperating.value && e.changedTouches.length === 1) {
  138. // 疯狂操作的极端情况下两个时间戳之间的时差会不合理,甚至为0
  139. if (lastMoveEventTimeStamp && (e.timeStamp - lastMoveEventTimeStamp > 1)) {
  140. // 更新speed
  141. const currentMoveSpeed = - (e.changedTouches[0].clientX - lastCursorPos) / (e.timeStamp - lastMoveEventTimeStamp) * unref(speedFactor)
  142. moveSpeed.value = moveSpeed.value * 0.9 + currentMoveSpeed * 0.1
  143. lastCursorPos = e.changedTouches[0].clientX
  144. }
  145. lastMoveEventTimeStamp = e.timeStamp
  146. }
  147. }
  148. /**
  149. * swipe end
  150. */
  151. function onMouseUp(e) {
  152. isOperating.value = false
  153. e.stopPropagation()
  154. beginAutoMoveIntervalId = setInterval(() => {
  155. if (moveSpeed.value === 0 && unref(enableAutoMoving)) {
  156. isAutoMoving.value = true
  157. }
  158. }, 2000)
  159. }
  160. function onTouchEnd(e) {
  161. isOperating.value = false
  162. beginAutoMoveIntervalId = setInterval(() => {
  163. if (moveSpeed.value === 0 && unref(enableAutoMoving)) {
  164. isAutoMoving.value = true
  165. }
  166. }, 2000)
  167. }
  168. function onMouseLeave() {
  169. isOperating.value = false
  170. beginAutoMoveIntervalId = setInterval(() => {
  171. if (moveSpeed.value === 0 && unref(enableAutoMoving)) {
  172. isAutoMoving.value = true
  173. }
  174. }, 2000)
  175. }
  176. function onTouchCancel() {
  177. isOperating.value = false
  178. beginAutoMoveIntervalId = setInterval(() => {
  179. if (moveSpeed.value === 0 && unref(enableAutoMoving)) {
  180. isAutoMoving.value = true
  181. }
  182. }, 2000)
  183. }
  184. beginAutoMoveIntervalId = setInterval(() => {
  185. if (moveSpeed.value === 0 && unref(enableAutoMoving)) {
  186. isAutoMoving.value = true
  187. }
  188. }, 2000)
  189. watch(isAutoMoving, (vNew) => {
  190. if (vNew) {
  191. moveSpeed.value = 0.03
  192. } else {
  193. moveSpeed.value = 0
  194. }
  195. })
  196. onBeforeUnmount(() => {
  197. clearInterval(beginAutoMoveIntervalId)
  198. })
  199. // 在每一帧更新镜头速度、位置
  200. function animationFrameTask() {
  201. const timeStamp = Date.now()
  202. const timeElapsed = timeStamp - lastAnimationTimeStamp
  203. if (!isAutoMoving.value) {
  204. // 速度减慢
  205. if (moveSpeed.value > 0) {
  206. moveSpeed.value -= unref(damping) * timeElapsed
  207. if (moveSpeed.value < 0) {
  208. moveSpeed.value = 0
  209. }
  210. } else if (moveSpeed.value < 0) {
  211. moveSpeed.value += unref(damping) * timeElapsed
  212. if (moveSpeed.value > 0) {
  213. moveSpeed.value = 0
  214. }
  215. }
  216. }
  217. // 根据速度更新位置
  218. translateLength.value += moveSpeed.value * timeElapsed
  219. if (translateLength.value < 0) {
  220. translateLength.value = 0
  221. moveSpeed.value = 0
  222. } else if (translateLength.value > maxTranslateLengthInner) {
  223. if (isAutoMoving.value) {
  224. translateLength.value = 0
  225. } else {
  226. translateLength.value = maxTranslateLengthInner
  227. moveSpeed.value = 0
  228. }
  229. }
  230. lastAnimationTimeStamp = timeStamp
  231. animationFrameId = requestAnimationFrame(animationFrameTask)
  232. }
  233. onMounted(() => {
  234. animationFrameId = requestAnimationFrame(animationFrameTask)
  235. })
  236. onBeforeUnmount(() => {
  237. cancelAnimationFrame(animationFrameId)
  238. })
  239. if (scrollTargetRef) {
  240. watch(scrollTargetRef, (v) => {
  241. if (v) {
  242. v.addEventListener('wheel', onWheel)
  243. v.addEventListener('mousedown', onMouseDown)
  244. v.addEventListener('mouseleave', onMouseLeave)
  245. v.addEventListener('mousemove', onMouseMove)
  246. v.addEventListener('mouseup', onMouseUp)
  247. // v.addEventListener('touchstart', onTouchStart)
  248. // v.addEventListener('touchmove', onTouchMove)
  249. // v.addEventListener('touchend', onTouchEnd)
  250. // v.addEventListener('touchcancel', onTouchCancel)
  251. }
  252. }, {
  253. immediate: true,
  254. })
  255. watch(translateLength, (v) => {
  256. scrollTargetRef.value && (scrollTargetRef.value.scrollLeft = v)
  257. })
  258. }
  259. return {
  260. onWheel,
  261. onMouseDown,
  262. onMouseLeave,
  263. onMouseMove,
  264. onMouseUp,
  265. onTouchStart,
  266. onTouchMove,
  267. onTouchEnd,
  268. onTouchCancel,
  269. translateLength,
  270. haveSwipedThisTime,
  271. computeMaxTranslateLengthInnerByScrollTarget,
  272. speedUp
  273. }
  274. }