Skip to content

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

属性类型默认值说明
ComponentElementType'div'HTML 标签类型
slideThresholdnumber5识别为滑动的最小距离(px)
usePointerHoverboolean-使用 pointer 而非 mouse 事件
useCapturebooleanfalse事件捕获模式
noSlideClickbooleanfalse滑动后阻止点击事件
stopPropagationbooleanfalse阻止事件冒泡
onStartTouchEventHandler-手势开始
onStartXTouchEventHandler-横向手势开始
onStartYTouchEventHandler-纵向手势开始
onMoveTouchEventHandler-手势移动
onMoveXTouchEventHandler-横向移动
onMoveYTouchEventHandler-纵向移动
onEndTouchEventHandler-手势结束
onEndXTouchEventHandler-横向手势结束
onEndYTouchEventHandler-纵向手势结束
onEnterHoverHandler-鼠标进入
onLeaveHoverHandler-鼠标离开

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>
  );
}

手势识别逻辑

  1. 阈值判断: 滑动距离超过 slideThreshold(默认 5px) 才识别为手势
  2. 方向识别: 自动判断横向还是纵向手势
  3. 独占性: 一旦识别为横向或纵向,不会同时触发
  4. 多点触控: 检测到多点触控时自动结束手势

与 Tappable 的区别

特性TouchTappable
用途复杂手势(滑动、拖拽)简单点击反馈
事件丰富的手势事件标准点击事件
视觉反馈无内置反馈水波纹/透明度
性能略高开销轻量级
使用场景轮播图、滑动删除按钮、列表项

性能优化

  1. 事件委托: 手势监听器添加到 window.document,避免每个元素都绑定
  2. 防止默认: 自动阻止拖拽图片/链接的默认行为
  3. 被动监听: 使用 passive: false 确保可以调用 preventDefault()
  4. 及时清理: 手势结束后自动移除全局监听器

最佳实践

  1. 避免嵌套: 不要嵌套多个 Touch 组件
  2. 明确方向: 优先使用 onMoveX/onMoveY 而非 onMove
  3. 添加视觉反馈: Touch 不提供内置视觉反馈,需自行实现
  4. 处理取消: 实现 onEnd 处理手势中断情况
  5. 禁用文字选择: 添加 user-select: none 避免拖拽时选中文字
css
.touch-container {
  user-select: none;
  -webkit-user-select: none;
  touch-action: none; /* 禁用浏览器默认手势 */
}

浏览器兼容性

  • ✅ 现代浏览器 (支持 Touch Events)
  • ✅ 移动端 Safari/Chrome
  • ✅ 桌面端 (鼠标事件降级)
  • ⚠️ IE11 (部分功能受限)

注意事项

  1. 触摸穿透: 在某些移动浏览器上,快速点击可能触发底层元素
  2. 滚动冲突: 纵向手势可能与页面滚动冲突,考虑设置 touch-action
  3. 多指手势: 组件检测到多指触控会自动终止手势

基于 MIT 许可发布