1.场景:列表支持手势touch触屏,列表支持手指上下(或左右)滑动进行滚动,手指点击listItem元素可拖拽元素。
2.问题:滚动条事件和手势touch事件冲突。
3.解决方案:300毫秒以内,判断用户手势滑动角度判断用户手势滑动是横向滑动操作类型:拖拽事件、滚动条事件两种类型。
3.1 y轴有滚动条例子:
垂直方向:在不可取的方向上进行滑动,执行y轴方向滚动事件;
水平方向:在可取范围手指拖动,执行拖拽事件。
这样就能避手势触摸滑动和滚动条事件冲突问题。
3.2 x轴有滚动条例子:和y轴方向处理方案相反。
4.核心代码:
滑动组件代码:
import './index.less';
import React, { useEffect, useRef } from 'react';
interface Props {
/** 手势水平滑动事件 */
onVertical?: () => void;
/** 手势垂直滑动事件 */
onHorizontal?: () => void;
/** 手势滑动的组件 */
children?: React.ReactNode;
/** 可用水平角度值 */
degreeValue?: number;
}
/** 手势触屏滑动位置信息 */
type PositionT = {
// 滑动开始x位置
startX: number,
// 滑动开始x位置
startY: number,
// 滑动结束x位置
endX: number,
// 滑动结束x位置
endY: number,
// 时间差
timeStamp: number,
// 当前手势状态: 水平方向或垂直方向
direction: string,
};
const defaultPosition = {
startX: 0,
startY: 0,
endX: 0,
endY: 0,
timeStamp: 0,
direction: '',
};
/** 设置时间阈值为300毫秒 */
const TIME = 300;
const SwipeDetector: React.FC<Props> = ({
children,
onHorizontal,
onVertical,
degreeValue = 15,
}) => {
// Dom ref属性
const domRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// 手势触屏滑动位置信息
let position: PositionT = { ...defaultPosition }
// 开始触屏
const onTouchStart = (e: TouchEvent) => {
position.startX = e.touches[0].clientX;
position.startY = e.touches[0].clientY;
position.timeStamp = new Date().getTime();
};
// 滑动中
const onTouchMove = (e: TouchEvent) => {
position.endX = e.touches[0].clientX;
position.endY = e.touches[0].clientY;
const currTime = new Date().getTime();
// 300ms生效计算事件类型
if (Math.floor(currTime - position.timeStamp) < TIME && !position.direction) {
const angx = position.endX - position.startX;
const angy = position.endY - position.startY;
// 拖拽的角度
const angle = getAngle(angx, angy);
// 是否在有效区域内拖拽
if (isAvailableAreas(angle)) {
// 水平滑动事件
position.direction = 'horizontal';
onHorizontal?.();
} else {
// 垂直滑动事件
position.direction = 'vertical';
onVertical?.();
}
} else {
// 垂直移动进行中
if (position.direction === 'vertical') {
onVertical?.();
} else if (position.direction === 'horizontal') {
// 水平移动进行中
onHorizontal?.();
}
}
};
// 滑动结束
const onTourchEnd = () => {
position = { ...defaultPosition }
};
/** 计算滑动角度 */
const getAngle = (angx: number, angy: number) => {
return Math.atan2(angy, angx) * 180 / Math.PI;
};
/**
* 300毫秒以内滑动区域是可拖拽区域
*/
const isAvailableAreas = (angle: number) => {
// 右侧有效区域拖拽
if (angle >= -degreeValue && angle <= degreeValue) {
// 水平滑动
return true;
} else if ((angle >= 180 - degreeValue && angle <= 180) || (angle >= -180 && angle < degreeValue - 180)) {
// 水平滑动
return true;
} else {
// 垂直滑动
return false;
}
}
if (domRef?.current) {
// 滑动事件绑定
if (domRef?.current) {
domRef.current.addEventListener('touchstart', onTouchStart);
domRef.current.addEventListener('touchmove', onTouchMove, false);
domRef.current.addEventListener('touchend', onTourchEnd);
}
// 卸载事件
return () => {
if (domRef?.current) {
domRef.current.removeEventListener('touchstart', onTouchStart);
domRef.current.removeEventListener('touchmove', onTouchMove, false);
domRef.current.removeEventListener('touchend', onTourchEnd);
}
};
}
}, []);
return (
<div
ref={domRef}
className="swiper-layer"
style={{ touchAction: 'pan-y' }}
>
{children}
</div>
);
};
export default SwipeDetector;
列表组件使用代码示例:
// 是否水平拖拽状态
const dragging = useRef(false);
const onVertical = () => {
//垂直滑动
dragging.current = false;
};
// 水平滑动
const handleSwiper = () => {
// 水平滑动
};
<SwipeDetector
onVertical={onVertical}
onHorizontal={onHorizontal}
degreeValue={75}
>
<my-list>
<my-list-item>
onTouchMove={(e) => {
if (水平拖拽) {
// 阻止事件冒泡,滚动条无响应
//执行拖拽逻辑
} else if (垂直滑动) {
//执行拖拽逻辑
}
}}
onTouchEnd={(e) => {
dragging.current = false;
}}
/>
</my-list>
</SwipeDetector>
: