Touch события и жесты

Preact использует синтетическую систему событий, совместимую с DOM API и во многом повторяющую модель React. Touch-события обрабатываются как обычные DOM-события, но регистрируются через JSX-атрибуты и проходят через унифицированный механизм делегирования.

Базовый набор touch-событий:

  • onTouchStart — начало касания
  • onTouchMove — перемещение пальца по экрану
  • onTouchEnd — завершение касания
  • onTouchCancel — прерывание касания (например, при системном жесте)

Каждое событие получает объект TouchEvent, содержащий коллекции:

  • touches — все активные касания
  • targetTouches — касания, начавшиеся на текущем элементе
  • changedTouches — касания, изменившиеся в данном событии

Preact не оборачивает TouchEvent в дополнительный слой, поэтому доступ к свойствам и методам идентичен нативному DOM.

function TouchArea() {
  const handleStart = (e) => {
    console.log(e.touches.length);
  };

  return <div onTouchSt art={handleStart}>...</div>;
}

Особенности работы touch-событий

Touch-события имеют ряд принципиальных отличий от mouse-событий:

  • Одновременно может существовать несколько точек касания
  • Координаты измеряются в пикселях экрана (clientX, clientY)
  • События не всегда завершаются touchend (возможен touchcancel)
  • По умолчанию браузер применяет жесты прокрутки и масштабирования

Для точного контроля над поведением часто требуется отключение стандартных жестов браузера.

.touch-area {
  touch-action: none;
}

Свойство touch-action критически важно для корректной обработки жестов в мобильных интерфейсах.


Предотвращение поведения браузера

Метод preventDefault() позволяет блокировать прокрутку и масштабирование, но с ограничениями. В современных браузерах touch-события часто являются passive по умолчанию.

В Preact для корректной работы preventDefault() необходимо явно отключить passive-режим через CSS (touch-action) или использовать onTouchMove без ожидания блокировки.

const handleMove = (e) => {
  e.preventDefault();
};

Без touch-action: none вызов preventDefault() может быть проигнорирован.


Обработка координат и трекинг движения

Для реализации жестов требуется хранить начальные координаты касания и отслеживать изменения.

import { useRef } from 'preact/hooks';

function DragBlock() {
  const startX = useRef(0);
  const startY = useRef(0);

  const onSt art = (e) => {
    const t = e.touches[0];
    startX.current = t.clientX;
    startY.current = t.clientY;
  };

  const onM ove = (e) => {
    const t = e.touches[0];
    const dx = t.clientX - startX.current;
    const dy = t.clientY - startY.current;
    console.log(dx, dy);
  };

  return <div onTouchSt art={onStart} onTouchM ove={onMove} />;
}

Использование useRef позволяет сохранять данные между событиями без перерендера компонента.


Реализация жестов

Tap и Long Press

Tap определяется как короткое касание без значительного смещения.

const MAX_DURATION = 200;
const MAX_DISTANCE = 10;

Фиксируется время начала и координаты. При touchend сравниваются длительность и смещение.

Long press реализуется через таймер:

let timer = setTimeout(() => {
  // long press
}, 500);

Таймер очищается при touchend или touchmove.


Swipe

Swipe определяется направлением и скоростью движения.

Основные параметры:

  • начальная точка
  • конечная точка
  • время
  • расстояние
const dx = endX - startX;
const dy = endY - startY;

if (Math.abs(dx) > Math.abs(dy)) {
  if (dx > 0) swipeRight();
  else swipeLeft();
}

Минимальное расстояние (например, 30–50px) предотвращает ложные срабатывания.


Pinch (масштабирование)

Pinch использует два одновременных касания.

function distance(t1, t2) {
  return Math.hypot(
    t2.clientX - t1.clientX,
    t2.clientY - t1.clientY
  );
}

При touchstart с двумя точками сохраняется исходное расстояние. При touchmove вычисляется новое расстояние и коэффициент масштабирования.

scale = currentDistance / initialDistance;

Pointer Events как альтернатива

Preact полностью поддерживает Pointer Events, которые объединяют mouse, touch и stylus.

<div
  onPointerD own={start}
  onPointerM ove={move}
  onPointe rUp={end}
/>

Преимущества:

  • единый API
  • поддержка давления (pressure)
  • корректная работа с несколькими указателями

Для современных браузеров Pointer Events предпочтительнее touch-событий.

.pointer-area {
  touch-action: none;
}

Производительность и оптимизация

Частые touchmove события могут вызывать перегрузку основного потока.

Практики оптимизации:

  • минимальная логика внутри обработчиков
  • отказ от setState в touchmove
  • использование requestAnimationFrame
  • хранение промежуточных данных в useRef
let rafId = null;

const onM ove = (e) => {
  if (rafId) return;
  rafId = requestAnimationFrame(() => {
    // обновление
    rafId = null;
  });
};

Интеграция со сторонними библиотеками жестов

Preact совместим с библиотеками:

  • Hammer.js
  • use-gesture
  • ZingTouch

При интеграции предпочтительно использовать ref для доступа к DOM-элементу.

const ref = useRef(null);

useEffect(() => {
  const manager = new Hammer(ref.current);
  manager.on('swipe', handler);
}, []);

Типичные ошибки

  • Игнорирование touch-action
  • Использование setState в каждом touchmove
  • Отсутствие обработки touchcancel
  • Смешивание mouse и touch без унификации
  • Ожидание click после touch на мобильных устройствах

Архитектура жестов в компонентах

Хорошей практикой является вынесение логики жестов в кастомные хуки.

function useSwipe(ref, callback) {
  // логика
}

Это упрощает повторное использование, тестирование и поддержку кода.


Совместимость и кросс-платформенность

  • iOS требует строгого контроля touch-action
  • Android более лоялен к preventDefault
  • Desktop-браузеры могут эмулировать touch
  • Гибридные устройства требуют Pointer Events

Универсальный подход — Pointer Events с fallback на Touch Events при необходимости.