壹 概念须知
要完成这个组件,首先要理解几个事件及其参数
touchstart:
当手指触摸屏幕时候触发,即使已经有一个手指放在屏幕上也会触发。
touchmove:
当手指在屏幕上滑动的时候连续地触发。在这个事件发生期间,调用preventDefault()事件可以阻止滚动。
touchend:
当手指从屏幕上离开的时候触发。
我们可以在这三个事件的回调参数e中通过e.touches[0](单指)获取需要的数据
- pageX 触摸点在页面中的横坐标
- pageY 触摸点在页面中的纵坐标
- clientX 触摸点在浏览器窗口中的横坐标
- clientY 触摸点在浏览器窗口中的纵坐标
- screenX 触摸点在屏幕中的横坐标
- screenY 触摸点在屏幕中的纵坐标
- force 触摸点压力大小
- identifier 触摸点唯一标识(ID)
- radiusX 触摸点椭圆的水平半径
- radiusY 触摸点椭圆的垂直半径
- rotationAngle 旋转角度
贰 要点梳理
如何准确判断用户滑动方向
按道理说,只需要比较判断touchmove和touchstart的坐标即可,但存在例外情况,比如用户向左滑动了一大段距离,又突然向右滑动一小段距离,此时touchmove的坐标依然在touchstart坐标的左边,无法准确判断滑动方向。
A :这时就需要一个变量记录上一次滑动的位移量preTouchS
preTouchS = touchmove坐标 - touchstart 坐标
这个变量的初始值设为0, 滑动时,只要比较preTouchS和当前的touchS即可判断实时的滑动方向
2.如何防止过度的滑动
// 估算位移 = 起始left + touch偏移
let s = touchStartLeft + touchS
// 校正位移 防止滑动幅度过大
if (s < 0) {
// 左滑
s = Math.abs(s) > maxLeftS ? -maxLeftS : s
} else {
// 右滑
s = s > 5 ? 5 : s
}
其中, touchStartLeft为每次touchstart记录下的当前偏移left值, maxLeftS是允许左滑的最大距离,在滑动删除功能中,可以设置为右边按钮组的宽度,5是右滑的缓冲值
叁 完整代码
import React, { useCallback, useEffect, useRef } from 'react'
type baseFnType = (...args: any) => void | unknown
type RightOptionsType = {
text: string
onPress: baseFnType
style: {
backgroundColor: string
color: string
}
}
export interface SwipeActionProps {
index: number
right?: RightOptionsType[]
onOpen?: baseFnType
onClose?: baseFnType
disabled?: boolean
children: React.ReactNode
autoClose: boolean
}
const SwipeAction: React.FC<SwipeActionProps> = (props) => {
const {
right: btnOptions,
index,
onOpen,
onClose,
disabled = false,
children,
autoClose = false,
} = props
const getLeft = (dom: HTMLElement): number =>
parseInt(dom.style.left || '0px', 10)
const contentDomRef = useRef<HTMLDivElement>(null)
const btnDomRef = useRef<HTMLDivElement>(null)
const touchStartX = useRef<number>(0) // 保存点击时的初始点击坐标
const touchStartLeft = useRef<number>(0) // 保存点击时的初始位置
const directionRef = useRef<string>('') // 保存滑动方向,touchmove和touchend里都要用到
const preTouchS = useRef<number>(0)
/**
* @description 滑动过程
* @param e
*/
const touchmove = useCallback(
(e) => {
if (disabled) return
const contentDom = contentDomRef.current
const startX = touchStartX.current // 触碰开始
const currentX = e.touches[0].pageX // 实时位置
const btnWidth = btnDomRef.current.offsetWidth // 按钮宽度
const maxLeftS = btnWidth + 15 // 左滑最大距离
const touchS = currentX - startX
if (touchS - preTouchS.current < 0) {
directionRef.current = 'left'
} else {
directionRef.current = 'right'
}
if (
(directionRef.current === 'right' && getLeft(contentDom) > 5) ||
(directionRef.current === 'left' && Math.abs(touchS) >= maxLeftS)
) {
return
}
// 估算位移 = 起始left + touch偏移
let s = touchStartLeft.current + touchS
// 校正位移 防止滑动幅度过大
if (s < 0) {
s = Math.abs(s) > maxLeftS ? -maxLeftS : s
} else {
s = s > 5 ? 5 : s
}
// window.requestAnimationFrame(() => {
contentDom.style.left = `${s}px`
// })
preTouchS.current = touchS
},
[disabled]
)
/**
* @description 滑动开始 记录开始的位置
* @param e
*/
const touchstart = useCallback((e) => {
touchStartLeft.current = getLeft(contentDomRef.current)
touchStartX.current = e.touches[0].pageX
}, [])
/**
* @description 滑动结束 还原位置
* @param e
*/
const touchend = useCallback(() => {
const contentDom = contentDomRef.current
const direction = directionRef.current // 往哪滑的
const btnWidth = btnDomRef.current.offsetWidth // 按钮宽度 即滑动的最大距离
if (direction === 'left') {
contentDom.style.left = `${-btnWidth}px`
typeof onOpen === 'function' && onOpen(index)
} else if (direction === 'right') {
contentDom.style.left = `${0}px`
typeof onClose === 'function' && onClose(index)
}
}, [index, onClose, onOpen])
/**
* 还原
*/
const handleReset = () => {
const contentDom = contentDomRef.current
contentDom.style.left = '0px'
}
/**
* @description 组件挂载 监听/移除事件
*/
useEffect(() => {
const contentDom = contentDomRef.current
contentDom.style.transition = '0.1s all'
contentDom.addEventListener('touchstart', touchstart)
contentDom.addEventListener('touchmove', touchmove)
contentDom.addEventListener('touchend', touchend)
return () => {
contentDom.removeEventListener('touchstart', touchstart)
contentDom.removeEventListener('touchmove', touchmove)
contentDom.removeEventListener('touchend', touchend)
}
}, [touchend, touchmove, touchstart])
return (
<div className="slider-wrap">
<div ref={contentDomRef} className="slider-content">
{children}
</div>
<div ref={btnDomRef} className="slider-btn">
{btnOptions.map((item) => (
<button
type="button"
key={item.text}
onClick={() => {
autoClose && handleReset()
item.onPress(index)
}}
style={{
background: item.style.backgroundColor,
color: item.style.color,
}}
>
{item.text}
</button>
))}
</div>
</div>
)
}
export default SwipeAction
scss
$swipeActionPrefixCls: 'slider-wrap';
$listItemPrefixCls: 'au-list-item';
.slider-wrap {
position: relative;
display: inline-block;
overflow-x: hidden; // 左边滑动部分裁掉
}
.slider-content {
display: inline-block;
width: 100%;
height: 100%;
position: relative;
z-index: 20;
vertical-align: bottom; // 不设置这个的话,wrap会比content高一点,不知道是为什么x
}
.slider-btn {
position: absolute;
right: 0;
// width: 100px;
height: 100%;
display: inline-flex;
flex-direction: row;
transform: scale(0.98);
button {
flex: 1;
padding: 0;
border: none;
}
}
.#{$swipeActionPrefixCls} {
border-top: 1px solid $gray-400;
&:last-child {
border-bottom: 1px solid $gray-400;
}
.#{$listItemPrefixCls} {
border: none !important;
&:active {
opacity: 0.8;
}
}
}