Touch 触摸手势
高级触摸手势处理组件,支持滑动、拖拽等复杂交互。源自 VKUI 库。
导入
ts
import { Touch } from '@xcloud/ui-mobile';示例
基础用法
tsx
import { Touch } from '@xcloud/ui-mobile';
function Example() {
return (
<Touch
onMove={(e) => console.log('Moving:', e.shiftX, e.shiftY)}
onEnd={() => console.log('Gesture ended')}
>
<div>滑动我</div>
</Touch>
);
}横向滑动检测
tsx
<Touch
onMoveX={(e) => {
console.log('Horizontal swipe:', e.shiftX);
}}
onEndX={(e) => {
if (Math.abs(e.shiftX) > 100) {
console.log('Swipe completed!');
}
}}
>
<div>横向滑动</div>
</Touch>纵向滑动检测
tsx
<Touch
onMoveY={(e) => {
console.log('Vertical swipe:', e.shiftY);
}}
onEndY={(e) => {
if (e.shiftY > 60) {
refreshData();
}
}}
>
<div>纵向滑动</div>
</Touch>拖拽元素
tsx
function DraggableBox() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const startPos = useRef({ x: 0, y: 0 });
return (
<Touch
onStart={() => {
startPos.current = position;
}}
onMove={(e) => {
setPosition({
x: startPos.current.x + e.shiftX,
y: startPos.current.y + e.shiftY,
});
}}
>
<div style={{ transform: `translate(${position.x}px, ${position.y}px)` }}>
拖我
</div>
</Touch>
);
}API
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| Component | ElementType | 'div' | HTML 标签类型 |
| slideThreshold | number | 5 | 识别为滑动的最小距离(px) |
| usePointerHover | boolean | - | 使用 pointer 而非 mouse 事件 |
| useCapture | boolean | false | 事件捕获模式 |
| noSlideClick | boolean | false | 滑动后阻止点击事件 |
| stopPropagation | boolean | false | 阻止事件冒泡 |
| onStart | TouchEventHandler | - | 手势开始 |
| onStartX | TouchEventHandler | - | 横向手势开始 |
| onStartY | TouchEventHandler | - | 纵向手势开始 |
| onMove | TouchEventHandler | - | 手势移动 |
| onMoveX | TouchEventHandler | - | 横向移动 |
| onMoveY | TouchEventHandler | - | 纵向移动 |
| onEnd | TouchEventHandler | - | 手势结束 |
| onEndX | TouchEventHandler | - | 横向手势结束 |
| onEndY | TouchEventHandler | - | 纵向手势结束 |
| onEnter | HoverHandler | - | 鼠标进入 |
| onLeave | HoverHandler | - | 鼠标离开 |
TouchEvent 对象
手势事件回调接收的参数:
ts
interface TouchEvent {
originalEvent: CustomTouchEvent; // 原始事件
startX: number; // 起始 X 坐标
startY: number; // 起始 Y 坐标
startT: Date; // 起始时间
clientX: number; // 当前 X 坐标
clientY: number; // 当前 Y 坐标
shiftX: number; // X 轴位移
shiftY: number; // Y 轴位移
shiftXAbs: number; // X 轴位移绝对值
shiftYAbs: number; // Y 轴位移绝对值
duration: number; // 手势持续时间(ms)
isPressed: boolean; // 是否按下
isX: boolean; // 是否为横向手势
isY: boolean; // 是否为纵向手势
isSlide: boolean; // 是否识别为滑动
isSlideX: boolean; // 是否为横向滑动
isSlideY: boolean; // 是否为纵向滑动
}使用场景
1. 卡片滑动删除
tsx
function SwipeToDelete({ onDelete, children }) {
const [offset, setOffset] = useState(0);
return (
<Touch
onMoveX={(e) => setOffset(e.shiftX < 0 ? e.shiftX : 0)}
onEndX={(e) => {
if (e.shiftX < -100) {
onDelete();
} else {
setOffset(0);
}
}}
>
<div style={{ transform: `translateX(${offset}px)` }}>
{children}
<div className="absolute right-0 top-0 h-full bg-red-500">
删除
</div>
</div>
</Touch>
);
}2. 轮播图
tsx
function Carousel({ images }) {
const [currentIndex, setCurrentIndex] = useState(0);
return (
<Touch
onEndX={(e) => {
if (e.shiftX < -50 && currentIndex < images.length - 1) {
setCurrentIndex(i => i + 1);
} else if (e.shiftX > 50 && currentIndex > 0) {
setCurrentIndex(i => i - 1);
}
}}
>
<div className="relative overflow-hidden">
<div style={{ transform: `translateX(-${currentIndex * 100}%)` }}>
{images.map((img, i) => (
<img key={i} src={img} />
))}
</div>
</div>
</Touch>
);
}3. 下拉刷新
tsx
function PullToRefresh({ onRefresh, children }) {
const [pullDistance, setPullDistance] = useState(0);
return (
<Touch
onMoveY={(e) => {
if (e.shiftY > 0) {
setPullDistance(Math.min(e.shiftY, 100));
}
}}
onEndY={(e) => {
if (e.shiftY > 60) {
onRefresh();
}
setPullDistance(0);
}}
>
<div>
{pullDistance > 0 && (
<div className="text-center">
{pullDistance > 60 ? '释放刷新' : '下拉刷新'}
</div>
)}
{children}
</div>
</Touch>
);
}手势识别逻辑
- 阈值判断: 滑动距离超过
slideThreshold(默认 5px) 才识别为手势 - 方向识别: 自动判断横向还是纵向手势
- 独占性: 一旦识别为横向或纵向,不会同时触发
- 多点触控: 检测到多点触控时自动结束手势
与 Tappable 的区别
| 特性 | Touch | Tappable |
|---|---|---|
| 用途 | 复杂手势(滑动、拖拽) | 简单点击反馈 |
| 事件 | 丰富的手势事件 | 标准点击事件 |
| 视觉反馈 | 无内置反馈 | 水波纹/透明度 |
| 性能 | 略高开销 | 轻量级 |
| 使用场景 | 轮播图、滑动删除 | 按钮、列表项 |
性能优化
- 事件委托: 手势监听器添加到
window.document,避免每个元素都绑定 - 防止默认: 自动阻止拖拽图片/链接的默认行为
- 被动监听: 使用
passive: false确保可以调用preventDefault() - 及时清理: 手势结束后自动移除全局监听器
最佳实践
- 避免嵌套: 不要嵌套多个 Touch 组件
- 明确方向: 优先使用
onMoveX/onMoveY而非onMove - 添加视觉反馈: Touch 不提供内置视觉反馈,需自行实现
- 处理取消: 实现
onEnd处理手势中断情况 - 禁用文字选择: 添加
user-select: none避免拖拽时选中文字
css
.touch-container {
user-select: none;
-webkit-user-select: none;
touch-action: none; /* 禁用浏览器默认手势 */
}浏览器兼容性
- ✅ 现代浏览器 (支持 Touch Events)
- ✅ 移动端 Safari/Chrome
- ✅ 桌面端 (鼠标事件降级)
- ⚠️ IE11 (部分功能受限)
注意事项
- 触摸穿透: 在某些移动浏览器上,快速点击可能触发底层元素
- 滚动冲突: 纵向手势可能与页面滚动冲突,考虑设置
touch-action - 多指手势: 组件检测到多指触控会自动终止手势