douyin/src/utils/slide.ts
2024-04-23 17:35:40 +08:00

280 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import bus from '@/utils/bus'
import Utils from '@/utils/index'
import GM from '@/utils/index'
import { SlideType } from '@/utils/const_var'
import { nextTick } from 'vue'
import { _css } from '@/utils/dom'
function checkEvent(e) {
const isMobile = /Mobi|Android|iPhone/i.test(navigator.userAgent)
if (!isMobile || (isMobile && e instanceof PointerEvent)) {
e.touches = [
{
clientX: e.clientX,
clientY: e.clientY,
pageX: e.pageX,
pageY: e.pageY
}
]
}
return true
}
//初始化信息获取slide dom的长宽、子元素数量用于move事件判断能否滑动
export function slideInit(el, state) {
state.wrapper.width = GM.$getCss(el, 'width')
state.wrapper.height = GM.$getCss(el, 'height')
nextTick(() => {
state.wrapper.childrenLength = el.children.length
})
//获取偏移量
const t = getSlideOffset(state, el)
let dx1 = 0,
dx2 = 0
if (state.type === SlideType.HORIZONTAL) dx1 = t
else dx2 = t
_css(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
}
/**
* 检测在对应方向上能否允许滑动比如SlideHorizontal组件就只处理左右滑动事件SlideVertical只处理上下滑动事件
* * @param state
* @returns {boolean}
*/
export function canSlide(state) {
//每次按下都需要检测up事件会重置为true
if (state.needCheck) {
//判断move x和y的距离是否大于判断值因为距离太小无法判断滑动方向
if (Math.abs(state.move.x) > state.judgeValue || Math.abs(state.move.y) > state.judgeValue) {
//放大再相除根据长宽比判断方向angle大于1就是左右滑动小于是上下滑动
const angle = (Math.abs(state.move.x) * 10) / (Math.abs(state.move.y) * 10)
//根据当前slide的类型判断能否滑动并记录下来后续不再判断直接返回记录值
state.next = state.type === SlideType.HORIZONTAL ? angle > 1 : angle <= 1
// console.log('angle', angle, state.next)
state.needCheck = false
} else {
return false
}
}
return state.next
}
/**
* 能否继续滑动
* @param state
* @param isNext 朝向,向右或向下
* @returns {boolean}
*/
function canNext(state, isNext) {
return !(
(state.localIndex === 0 && !isNext) ||
(state.localIndex === state.wrapper.childrenLength - 1 && isNext)
)
}
/**
* 开始滑动
* @param e
* @param el
* @param state
*/
export function slideTouchStart(e, el, state) {
// console.log('e', e, state.name)
if (!checkEvent(e)) return
_css(el, 'transition-duration', `0ms`)
//记录起点坐标用于move事件计算移动距离
state.start.x = e.touches[0].pageX
state.start.y = e.touches[0].pageY
//记录按下时间用于up事件判断滑动时间
state.start.time = Date.now()
state.isDown = true
}
/**
* move事件
* @param e
* @param el
* @param state
* @param canNextCb 是否能继续滑的回调
* @param notNextCb 不能继续滑的回调
* @param slideOtherDirectionCb 滑动其他方向时的回调,目前用于图集进于放大模式后,上下滑动推出放大模式
*/
export function slideTouchMove(
e,
el,
state,
canNextCb = null,
notNextCb = null,
slideOtherDirectionCb = null
) {
if (!checkEvent(e)) return
if (!state.isDown) return
// console.log('move', state.name)
//计算移动距离
state.move.x = e.touches[0].pageX - state.start.x
state.move.y = e.touches[0].pageY - state.start.y
// console.log('move', state.name)
//检测能否滑动
const canSlideRes = canSlide(state)
//是否在往到头或尾滑动
const isNext = state.type === SlideType.HORIZONTAL ? state.move.x < 0 : state.move.y < 0
//特别处理竖直的slide组件在第一页往下滑动时向外发送事件
//用于首页顶部导航栏的刷新动画
if (state.type === SlideType.VERTICAL_INFINITE) {
if (canSlideRes && state.localIndex === 0 && !isNext) {
bus.emit(state.name + '-moveY', state.move.y)
}
}
if (canSlideRes) {
//如果传了就用,没传就用默认的
//无限滑动组件要特别判断所以需要传canNextCb
if (!canNextCb) canNextCb = canNext
if (canNextCb(state, isNext)) {
window.isMoved = true
//能滑动,那就把事件捕获,不能给父组件处理
Utils.$stopPropagation(e)
if (state.type === SlideType.HORIZONTAL) {
bus.emit(state.name + '-moveX', state.move.x)
}
//获取偏移量
const t = getSlideOffset(state, el) + (isNext ? state.judgeValue : -state.judgeValue)
let dx1 = 0,
dx2 = 0
//偏移量加当前手指移动的距离就是slide要偏移的值
if (state.type === SlideType.HORIZONTAL) {
dx1 = t + state.move.x
} else {
dx2 = t + state.move.y
}
_css(el, 'transition-duration', `0ms`)
_css(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
} else {
//SlideAlbum.vue组件在用用于捕获事件阻止事件传递给父slide
notNextCb?.()
}
} else {
slideOtherDirectionCb?.(e)
}
}
/**
* 滑动结束事件
* @param e
* @param state
* @param canNextCb
* @param nextCb
* @param notNextCb
* @returns {*}
*/
export function slideTouchEnd(e, state, canNextCb = null, nextCb = null, notNextCb = null) {
if (!checkEvent(e)) return
if (!state.isDown) return
const isHorizontal = state.type === SlideType.HORIZONTAL
const isNext = isHorizontal ? state.move.x < 0 : state.move.y < 0
if (state.next) {
//同move事件
if (!canNextCb) canNextCb = canNext
if (canNextCb(state, isNext)) {
//能滑动,那就把事件捕获,不能给父组件处理
// Utils.$stopPropagation(e)
//结合时间、距离来判断是否成功滑动
const endTime = Date.now()
let gapTime = endTime - state.start.time
const distance = isHorizontal ? state.move.x : state.move.y
const judgeValue = isHorizontal ? state.wrapper.width : state.wrapper.height
//1、距离太短直接不通过
if (Math.abs(distance) < 20) gapTime = 1000
//2、距离太长直接通过
if (Math.abs(distance) > judgeValue / 3) gapTime = 100
//3、若不在上述两种情况那么只需要判断时间即可
if (gapTime < 150) {
if (isNext) {
state.localIndex++
} else {
state.localIndex--
}
return nextCb?.(isNext)
}
} else {
return notNextCb?.()
}
} else {
notNextCb?.()
}
}
/**
* 结束后重置变量
* @param el
* @param state
* @param emit
*/
export function slideReset(e, el, state, emit = null) {
if (!checkEvent(e)) return
_css(el, 'transition-duration', `300ms`)
const t = getSlideOffset(state, el)
let dx1 = 0
let dx2 = 0
if (state.type === SlideType.HORIZONTAL) {
bus.emit(state.name + '-end', state.localIndex)
dx1 = t
} else {
bus.emit(state.name + '-end')
dx2 = t
}
_css(el, 'transform', `translate3d(${dx1}px, ${dx2}px, 0)`)
state.start.x = state.start.y = state.start.time = state.move.x = state.move.y = 0
state.next = false
state.needCheck = true
state.isDown = false
// e.target.style.pointerEvents = null
setTimeout(() => {
window.isMoved = false
}, 200)
emit?.('update:index', state.localIndex)
}
//根据当前index获取slide偏移距离
//如果每个页面的宽度是相同均为100%只需要当前index * wrapper的宽度即可 -state.localIndex * state.wrapper.width
export function getSlideOffset(state: any, el: HTMLDivElement) {
//横竖判断逻辑基本同理
if (state.type === SlideType.HORIZONTAL) {
let widths = []
//获取所有子元素的宽度
Array.from(el.children).map((v) => {
widths.push(v.getBoundingClientRect().width)
})
//取0到当前index的子元素的宽度
widths = widths.slice(0, state.localIndex)
if (widths.length) {
//累计就是当前index之前所有页面的宽度
return -widths.reduce((a, b) => a + b)
}
return 0
// return -state.localIndex * state.wrapper.width
} else {
//VERTICAL_INFINITE 列表只需要计算index * 高就行
if (state.type === SlideType.VERTICAL_INFINITE) {
return -state.localIndex * state.wrapper.height
} else {
//同上
let heights = []
Array.from(el.children).map((v) => {
heights.push(v.getBoundingClientRect().height)
})
heights = heights.slice(0, state.localIndex)
if (heights.length) return -heights.reduce((a, b) => a + b)
return 0
}
}
}