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

Lit предоставляет удобный и декларативный подход к работе с DOM, включая обработку событий касания (touch) и жестов. Использование touch-событий особенно важно для мобильных интерфейсов, где пользователи взаимодействуют с элементами через сенсорный экран. В основе работы с touch лежат стандартные браузерные события: touchstart, touchmove, touchend и touchcancel. Lit позволяет интегрировать их напрямую через директиву @event.


Обработка базовых touch-событий

Пример: обработка touchstart и touchend

import { LitElement, html, css } from 'lit';

class TouchDemo extends LitElement {
  static styles = css`
    div {
      width: 200px;
      height: 200px;
      background-color: lightblue;
      display: flex;
      align-items: center;
      justify-content: center;
      user-select: none;
      touch-action: none;
    }
  `;

  onTouchStart(e) {
    console.log('Touch start:', e.touches.length);
    this.shadowRoot.querySelector('div').style.backgroundColor = 'lightgreen';
  }

  onTouchEnd(e) {
    console.log('Touch end');
    this.shadowRoot.querySelector('div').style.backgroundColor = 'lightblue';
  }

  render() {
    return html`
      <div
        @touchstart=${this.onTouchStart}
        @touchend=${this.onTouchEnd}
      >
        Touch me
      </div>
    `;
  }
}

customElements.define('touch-demo', TouchDemo);

Ключевой момент: директива @touchstart=${this.onTouchStart} связывает метод компонента с событием DOM, обеспечивая корректное связывание контекста this.


Особенности работы с touchmove

Событие touchmove срабатывает при перемещении пальца по экрану. Важно использовать e.preventDefault() для предотвращения скролла страницы, если элемент должен перехватывать жест полностью.

onTouchMove(e) {
  e.preventDefault(); // предотвращает прокрутку
  const touch = e.touches[0];
  console.log(`Moved to (${touch.clientX}, ${touch.clientY})`);
}

Совет: Для производительных приложений стоит оптимизировать обработку touchmove, используя requestAnimationFrame для обновления DOM, чтобы избежать лагов при частых событиях.


Распознавание жестов

Жесты, такие как свайпы и зумы, можно реализовать через комбинацию touch-событий. Основная идея — хранить состояние касания между событиями:

class SwipeDemo extends LitElement {
  startX = 0;
  startY = 0;

  onTouchStart(e) {
    const touch = e.touches[0];
    this.startX = touch.clientX;
    this.startY = touch.clientY;
  }

  onTouchEnd(e) {
    const touch = e.changedTouches[0];
    const deltaX = touch.clientX - this.startX;
    const deltaY = touch.clientY - this.startY;

    if (Math.abs(deltaX) > Math.abs(deltaY)) {
      console.log(deltaX > 0 ? 'Swipe right' : 'Swipe left');
    } else {
      console.log(deltaY > 0 ? 'Swipe down' : 'Swipe up');
    }
  }

  render() {
    return html`
      <div
        style="width:200px;height:200px;background:lightcoral;"
        @touchstart=${this.onTouchStart}
        @touchend=${this.onTouchEnd}
      >
        Swipe me
      </div>
    `;
  }
}

customElements.define('swipe-demo', SwipeDemo);

Важное замечание: changedTouches содержит только те пальцы, которые завершили взаимодействие, что делает его предпочтительным для анализа финальной позиции жеста.


Настройка touch-action и предотвращение конфликтов

CSS-свойство touch-action играет ключевую роль в контроле жестов браузера:

  • none — блокирует стандартные жесты (скролл, масштабирование).
  • pan-x, pan-y — разрешает вертикальное или горизонтальное скроллирование.
  • manipulation — разрешает стандартные манипуляции, блокируя только двойной тап.

Пример использования:

div {
  touch-action: none; /* полностью контролируем жесты через JS */
}

Это важно для корректной работы кастомных свайпов и мультитач-жестов.


Мультитач и сложные жесты

Touch-события поддерживают несколько касаний одновременно. Объект TouchList (e.touches) содержит все активные точки касания.

Пример обработки зума двумя пальцами:

onTouchMove(e) {
  if (e.touches.length === 2) {
    const [t1, t2] = e.touches;
    const distance = Math.hypot(t2.clientX - t1.clientX, t2.clientY - t1.clientY);
    console.log('Pinch distance:', distance);
  }
}

Используя начальное расстояние между пальцами при touchstart, можно реализовать детектирование увеличения или уменьшения масштаба элемента.


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

  1. Использовать passive: true для событий, где не вызывается preventDefault, чтобы ускорить скроллинг.
  2. Минимизировать работу с DOM внутри touchmove.
  3. Для сложных жестов хранить состояние в объекте компонента, а не в DOM-атрибутах.
  4. Использовать CSS will-change: transform для элементов, которые изменяются в результате жестов, чтобы GPU-ускорение уменьшало лаги.

Интеграция с Lit-директивами

Lit позволяет комбинировать touch-события с условным рендерингом и реактивными свойствами, что делает возможным динамическое включение или отключение обработки жестов в зависимости от состояния компонента.

Пример динамической блокировки свайпов:

html`
  <div
    style="touch-action: ${this.gesturesEnabled ? 'none' : 'pan-y'};"
    @touchstart=${this.gesturesEnabled ? this.onTouchStart : null}
    @touchend=${this.gesturesEnabled ? this.onTouchEnd : null}
  >
    Dynamic gestures
  </div>
`

Такой подход позволяет создавать гибкие интерфейсы, адаптирующиеся под разные режимы работы.