События касаний (тач-события)

Чтобы предоставить качественную поддержку пользовательского интерфейса, связанного с касаниями, тач-события предлагают возможность интерпретировать действия пальца (или стилуса) на сенсорных экранах или трекпадах.

Интерфейсы событий касания - это относительно низкоуровневые API, которые можно использовать для поддержки приложений со специфическими мультитач-взаимодействиями, например жестом двумя пальцами. Мультитач взаимодействие запускается, когда палец (или стилус) впервые касается контакной поверхности. Другие пальцы могут следом коснуться поверхности и, если нужно, перемещаться по ней. Взаимодействие считается завершённым, когда пальцы с поверхности убираются. Во время взаимодействия, события касания срабатывают на начальном этапе (первое прикосновение), этапе перемещения по поверхности, и завершающем этапе (когда пальцы убираются с поверхности)..

События касаний подобны событиям мыши за исключением того, что они поддерживают несколько одновременных касаний в разных местах сенсорной поверхности. Интерфейс TouchEvent содержит все активные в данный момент точки прикосновения. Интерфейс Touch, который представляет одну точку касания, включает такую информацию, как позиция точки касания относительно области видимости браузера.

Определения

Поверхность (Surface)
Чувствительная к касаниям поверхность. Это может быть экран или трекпад.
Точка касания (Touch point)
Точка контакта с поверхностью. Это может быть палец (или локоть, ухо, нос, что угодно, но обычно это палец) или стилус. 

Интерфейсы

TouchEvent
Представляет событие, происходящее при изменении состояния касания поверхности.
Touch
Представляет одну точку контакта пользователя с сенсорной поверхностью.
TouchList
Представляет группу касаний; это используется, если пользователь, например, касается поверхности несколькими пальцами одновременно.

Пример

В этом примере отслеживаются несколько касаний одновременно, позволяя пользователю рисовать в <canvas> несколькими пальцами одновременно. Это будет работать лишь в браузере, который поддерживает touch-события.

Примечание: В тексте ниже для описания контакта с поверхностью используется понятие "палец", но это также может быть стилус или другой способ контакта.

Создание canvas

<canvas id="canvas" width="600" height="600" style="border:solid black 1px;">
  Ваш браузер не поддерживает элемент canvas.
</canvas>
<br>
Log: <pre id="log" style="border: 1px solid #ccc;"></pre>

Добавление обработчиков событий

После загрузки страницы будет вызвана функция startup(), показанная ниже

function startup() {
  var el = document.getElementById("canvas");
  el.addEventListener("touchstart", handleStart, false);
  el.addEventListener("touchend", handleEnd, false);
  el.addEventListener("touchcancel", handleCancel, false);
  el.addEventListener("touchmove", handleMove, false);
}

document.addEventListener("DOMContentLoaded", startup);

Данная функция просто добавляет обработчики событий для элемента <canvas>, чтобы мы могли обрабатывать события касания по мере их возникновения

Отслеживание новых касаний

Рассмотрим касания в действии.

var ongoingTouches = [];

Когда возникает событие touchstart, свидетельствующее о новом касании к поверхности, вызывается приведённая ниже функция handleStart().

function handleStart(evt) {
  evt.preventDefault();
  console.log("touchstart.");
  var el = document.getElementById("canvas");
  var ctx = el.getContext("2d");
  var touches = evt.changedTouches;
        
  for (var i = 0; i < touches.length; i++) {
    console.log("touchstart:" + i + "...");
    ongoingTouches.push(copyTouch(touches[i]));
    var color = colorForTouch(touches[i]);
    ctx.beginPath();
    ctx.arc(touches[i].pageX, touches[i].pageY, 4, 0, 2 * Math.PI, false);  // a circle at the start
    ctx.fillStyle = color;
    ctx.fill();
    console.log("touchstart:" + i + ".");
  }
}

Она вызывает event.preventDefault() для того, чтобы предотвратить обработку браузером события касания (это также предотвращает обработку события мыши). Затем мы получаем контекст и извлекаем список измененных точек касаний из свойства TouchEvent.changedTouches данного события.

После этого мы перебираем все объекты Touch в списке, помещая их в массив активных точек касания и рисуя начальную точку в виде маленького кружка; мы используем линию толщиной 4 пикселя, поэтому получим аккуратный круг радиусом 4 пикселя.

Рисование движением

Каждый раз, когда двигается один или несколько пальцев, срабатывает событие touchmove, в результаете чего вызывается наша функция handleMove().В этом примере данная функция ответственна за обновление данных о касании и рисование линии от предыдущей до текущей точки касания.

function handleMove(evt) {
  evt.preventDefault();
  var el = document.getElementById("canvas");
  var ctx = el.getContext("2d");
  var touches = evt.changedTouches;

  for (var i = 0; i < touches.length; i++) {
    var color = colorForTouch(touches[i]);
    var idx = ongoingTouchIndexById(touches[i].identifier);

    if (idx >= 0) {
      console.log("continuing touch "+idx);
      ctx.beginPath();
      console.log("ctx.moveTo(" + ongoingTouches[idx].pageX + ", " + ongoingTouches[idx].pageY + ");");
      ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY);
      console.log("ctx.lineTo(" + touches[i].pageX + ", " + touches[i].pageY + ");");
      ctx.lineTo(touches[i].pageX, touches[i].pageY);
      ctx.lineWidth = 4;
      ctx.strokeStyle = color;
      ctx.stroke();

      ongoingTouches.splice(idx, 1, copyTouch(touches[i]));  // swap in the new touch record
      console.log(".");
    } else {
      console.log("can't figure out which touch to continue");
    }
  }
}

Функция также перебирает измененные касания, ищет в нашем массиве данных о касаниях предыдущие данные о каждом касании для определения начальной точки каждого отрезка линии, который должен быть нарисован с помощью касания. Это реализовано путем отслеживания свойства Touch.identifier каждого касания. Это свойство является уникальным числом для каждого касания, и остается неизменным на протяжении всего времени контакта пальца с экраном.

Это позволяет нам получать координаты предыдущей позиции каждого касания и использовать подходящий метод "canvas-контекста" для рисования отрезка линии, соединяющего начало и конец.

После рисования линии, мы вызываем Array.splice(), чтобы в массиве ongoingTouches заменить предыдущие данные о точке касания на текущие.

Обработка завершения касания

Когда пользователь убирает палец с сенсорной поверхности, срабатывает событие touchend. Мы обрататываем его, вызывая фунцию handleEnd(), которая представлена ниже. Ее задача - рисовать последний отрезок линии для каждого касания, которое завершилось, и удалять точку касания из текущего списка касаний.

function handleEnd(evt) {
  evt.preventDefault();
  log("touchend");
  var el = document.getElementById("canvas");
  var ctx = el.getContext("2d");
  var touches = evt.changedTouches;

  for (var i = 0; i < touches.length; i++) {
    var color = colorForTouch(touches[i]);
    var idx = ongoingTouchIndexById(touches[i].identifier);

    if (idx >= 0) {
      ctx.lineWidth = 4;
      ctx.fillStyle = color;
      ctx.beginPath();
      ctx.moveTo(ongoingTouches[idx].pageX, ongoingTouches[idx].pageY);
      ctx.lineTo(touches[i].pageX, touches[i].pageY);
      ctx.fillRect(touches[i].pageX - 4, touches[i].pageY - 4, 8, 8);  // and a square at the end
      ongoingTouches.splice(idx, 1);  // remove it; we're done
    } else {
      console.log("can't figure out which touch to end");
    }
  }
}

Она очень похожа на предыдущюю функцию; единственное отличие заключается в том, что,что теперь мы рисуем маленький квадрат, чтобы обозначить конец, и в том, что когда мы мы вызываем  Array.splice(), мы просто удаляем старую запись из текущего списка касаний без добавления обновленной информации. В результате мы прекращаем отслеживать эту точку касания.

Обработка отмененных касаний

Если палец пользователя перемещается в интерфейс браузера или прикосновение должно быть отменено, отправляется событие touchcancel, и мы вызываем функцию handleCancel(), приведённую ниже.

function handleCancel(evt) {
  evt.preventDefault();
  console.log("touchcancel.");
  var touches = evt.changedTouches;
  
  for (var i = 0; i < touches.length; i++) {
    var idx = ongoingTouchIndexById(touches[i].identifier);
    ongoingTouches.splice(idx, 1);  // remove it; we're done
  }
}

Поскольку идея состоит в том, чтобы немедленно прервать касание, мы просто удаляем его из текущего списка касаний без рисования завершающего отрезка линии.

Удобные функции

Этот пример использует две удобные функции, которые следует кратко рассмотреть, чтобы сделать остальную часть кода более понятной.

Выбор цвета для каждого касания

Чтобы рисунок каждого касания выглядел по-разному, используется функция colorForTouch(), в которой цвета выбираются на основе уникального идентификатора касаний. Этот идентификатор является скрытым числом, но мы, по крайней мере, можем полагаться на то, что у каждого активного на данный момент касания он уникальный.

function colorForTouch(touch) {
  var r = touch.identifier % 16;
  var g = Math.floor(touch.identifier / 3) % 16;
  var b = Math.floor(touch.identifier / 7) % 16;
  r = r.toString(16); // make it a hex digit
  g = g.toString(16); // make it a hex digit
  b = b.toString(16); // make it a hex digit
  var color = "#" + r + g + b;
  console.log("color for touch with identifier " + touch.identifier + " = " + color);
  return color;
}

Результатом этой функции является строка, которую можно использовать при вызове <canvas>-функций для задания цвета рисования. Например, для значения Touch.identifier, равного 10, результатом будет "#a31".

Копирование объекта касания

Некоторые браузеры (например, мобильный Safari) повторно используют объекты касания в разных событиях, поэтому лучше копировать только важные свойства, а не ссылаться на весь объект.

function copyTouch({ identifier, pageX, pageY }) {
  return { identifier, pageX, pageY };
}

Поиск текущего касания

Приведённая ниже функция ongoingTouchIndexById() сканирует весь массив ongoingTouches, чтобы найти касание, соответствующее данному идентификатору, после чего возвращает в массив индекс этого касания.

function ongoingTouchIndexById(idToFind) {
  for (var i = 0; i < ongoingTouches.length; i++) {
    var id = ongoingTouches[i].identifier;
    
    if (id == idToFind) {
      return i;
    }
  }
  return -1;    // not found
}

Отображение происходящего

function log(msg) {
  var p = document.getElementById('log');
  p.innerHTML = msg + "\n" + p.innerHTML;
}

Если ваш браузер поддерживает это, вы можете посмотреть живой пример.

Пример sFiddle

Дополнительные советы

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

Обработка кликов

Поскольку вызов preventDefault() для события touchstart или первого события из серии событий touchmove предотвращает запуск соответствующих событий мыши, более распространена практика вызова preventDefault() именно для события touchmove, а не touchstart. Таким образом, события мыши всё ещё будут вызываться, а такие элементы, как ссылки, будут продолжать работать. В качестве альтернативы, в некоторых фреймворках для этой же цели события касаний дублируются событиями мыши.

Данный пример очень упрощён и может привести к странному поведению. Он уместен исключительно как учебный пример.

function onTouch(evt) {
  evt.preventDefault();
  if (evt.touches.length > 1 || (evt.type == "touchend" && evt.touches.length > 0))
    return;

  var newEvt = document.createEvent("MouseEvents");
  var type = null;
  var touch = null;

  switch (evt.type) {
    case "touchstart": 
      type = "mousedown";
      touch = evt.changedTouches[0];
      break;
    case "touchmove":
      type = "mousemove";
      touch = evt.changedTouches[0];
      break;
    case "touchend":        
      type = "mouseup";
      touch = evt.changedTouches[0];
      break;
  }

  newEvt.initMouseEvent(type, true, true, evt.originalTarget.ownerDocument.defaultView, 0,
    touch.screenX, touch.screenY, touch.clientX, touch.clientY,
    evt.ctrlKey, evt.altKey, evt.shiftKey, evt.metaKey, 0, null);
  evt.originalTarget.dispatchEvent(newEvt);
}

Вызов preventDefault() только при втором касании

Один из способов запретить использовать на странице pinchZoom (зум с помощью щипка), – вызвать preventDefault() для второго касания, когда одно касание уже активно. Такое поведение плохо прописано в спецификации событий касаний и приводит к разному поведению в разных браузерах. Например, iOS предотвратит зум, но всё ещё будт позволять перетискивание (panning) двумя пальцами; в Android, наоборот, можно будет осуществлять перетаскивание (panning), но не зум; Opera и Firefox на данный момент предотвращают и перетаскивание (panning) и зум. На данный момент для запрета использования зума рекомендуется полагаться не на какое-то конкретное поведение, а на meta-данные для "viewport".

Спецификация

Specification Status Comment
Touch Events – Level 2
Определение 'Touch' в этой спецификации.
Черновик Добавлены свойства radiusX, radiusY, rotationAngle, force
Touch Events
Определение 'Touch' в этой спецификации.
Рекомендация Начальное определение.

Совместимость с браузером

Touch

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

Прочина этого заключается в том, что некоторые веб-сайты используют поддержку данного API в качестве показателя того, что браузер запущен на мобильном устройстве. Если API событий касания доступен, значит эти сайты будут предполагать работу с мобильного устройства и предоставлять соответствующее содержимое, оптимизированное для мобильных устройств. Это может существенно усложнить работу для пользователей десктопов с сенсорными экранами.

Для поддержки и касаний и мыши на всех типах устройств, используйте вместо этого события указателя

Update compatibility data on GitHub
КомпьютерыМобильные
ChromeEdgeFirefoxInternet ExplorerOperaSafariAndroid webviewChrome для AndroidFirefox для AndroidOpera для AndroidSafari on iOSSamsung Internet
TouchChrome Полная поддержка 22Edge Полная поддержка ≤18Firefox Полная поддержка 52
Замечания
Полная поддержка 52
Замечания
Замечания Touch events support has been fixed and reenabled in Windows desktop platforms.
Нет поддержки 18 — 24
Замечания
Замечания Web compatibility issues seen in bug 888304.
IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да
Touch() constructor
Экспериментальная
Chrome Полная поддержка 48Edge Полная поддержка 79Firefox Полная поддержка 46IE Нет поддержки НетOpera Полная поддержка 35Safari Нет поддержки НетWebView Android Полная поддержка 48Chrome Android Полная поддержка 48Firefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка 5.0
clientXChrome Полная поддержка 22Edge Полная поддержка ≤18Firefox Полная поддержка 52
Замечания
Полная поддержка 52
Замечания
Замечания Touch events support has been fixed and reenabled in Windows desktop platforms.
Нет поддержки 18 — 24
Замечания
Замечания Web compatibility issues seen in bug 888304.
IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да
clientYChrome Полная поддержка 22Edge Полная поддержка ≤18Firefox Полная поддержка 52
Замечания
Полная поддержка 52
Замечания
Замечания Touch events support has been fixed and reenabled in Windows desktop platforms.
Нет поддержки 18 — 24
Замечания
Замечания Web compatibility issues seen in bug 888304.
IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да
force
Экспериментальная
Chrome Полная поддержка ДаEdge Полная поддержка ≤79Firefox Полная поддержка ДаIE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка ДаOpera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да
identifierChrome Полная поддержка 22Edge Полная поддержка ≤18Firefox Полная поддержка 52
Замечания
Полная поддержка 52
Замечания
Замечания Touch events support has been fixed and reenabled in Windows desktop platforms.
Нет поддержки 18 — 24
Замечания
Замечания Web compatibility issues seen in bug 888304.
IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да
pageXChrome Полная поддержка 22Edge Полная поддержка ≤18Firefox Полная поддержка 52
Замечания
Полная поддержка 52
Замечания
Замечания Touch events support has been fixed and reenabled in Windows desktop platforms.
Нет поддержки 18 — 24
Замечания
Замечания Web compatibility issues seen in bug 888304.
IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да
pageYChrome Полная поддержка 22Edge Полная поддержка ≤18Firefox Полная поддержка 52
Замечания
Полная поддержка 52
Замечания
Замечания Touch events support has been fixed and reenabled in Windows desktop platforms.
Нет поддержки 18 — 24
Замечания
Замечания Web compatibility issues seen in bug 888304.
IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да
radiusX
Экспериментальная
Chrome Полная поддержка 43Edge Полная поддержка ≤79Firefox ? IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка 43Chrome Android Полная поддержка 43Firefox Android Полная поддержка ДаOpera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка 4.0
radiusY
Экспериментальная
Chrome Полная поддержка 43Edge Полная поддержка ≤79Firefox ? IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка 43Chrome Android Полная поддержка 43Firefox Android Полная поддержка ДаOpera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка 4.0
rotationAngle
Экспериментальная
Chrome Полная поддержка 43Edge Полная поддержка ≤79Firefox ? IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка 43Chrome Android Полная поддержка 43Firefox Android Полная поддержка ДаOpera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка 4.0
screenXChrome Полная поддержка 22Edge Полная поддержка ≤18Firefox Полная поддержка 52
Замечания
Полная поддержка 52
Замечания
Замечания Touch events support has been fixed and reenabled in Windows desktop platforms.
Нет поддержки 18 — 24
Замечания
Замечания Web compatibility issues seen in bug 888304.
IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да
screenYChrome Полная поддержка 22Edge Полная поддержка ≤18Firefox Полная поддержка 52
Замечания
Полная поддержка 52
Замечания
Замечания Touch events support has been fixed and reenabled in Windows desktop platforms.
Нет поддержки 18 — 24
Замечания
Замечания Web compatibility issues seen in bug 888304.
IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да
targetChrome Полная поддержка 22Edge Полная поддержка ≤18Firefox Полная поддержка 52
Замечания
Полная поддержка 52
Замечания
Замечания Touch events support has been fixed and reenabled in Windows desktop platforms.
Нет поддержки 18 — 24
Замечания
Замечания Web compatibility issues seen in bug 888304.
IE Нет поддержки НетOpera Полная поддержка ДаSafari Нет поддержки НетWebView Android Полная поддержка ДаChrome Android Полная поддержка ДаFirefox Android Полная поддержка 6Opera Android Полная поддержка ДаSafari iOS Полная поддержка ДаSamsung Internet Android Полная поддержка Да

Легенда

Полная поддержка  
Полная поддержка
Нет поддержки  
Нет поддержки
Совместимость неизвестна  
Совместимость неизвестна
Экспериментальная. Ожидаемое поведение может измениться в будущем.
Экспериментальная. Ожидаемое поведение может измениться в будущем.
Смотрите замечания реализации.
Смотрите замечания реализации.

Firefox, события касаний и многопроцессность (e10s)

В Firefox события касания отключены, когда отключен e10s (электролиз; многопроцессорный Firefox). e10s включен по умолчанию в Firefox, но может в конечном итоге отключаться в определенных ситуациях, например, когда установлены определенные инструменты специальных возможностей или надстройки Firefox, для работы которых требуется отключение e10s. Это означает, что даже на настольном компьютере / ноутбуке с сенсорным экраном сенсорные события не будут включены.

Вы можете проверить, отключен ли e10s, перейдя в about:support и посмотрев на запись «Многопроцессорная Windows» в разделе «Основы приложения». 1/1 означает, что он включен, 0/1 означает отключен.

Если вы хотите принудительно включить e10s - чтобы явно повторно включить поддержку сенсорных событий - вам нужно перейти к about:config и создать новое логическое предпочтение browser.tabs.remote.force-enable. Установите значение true, перезапустите браузер, и e10s будет включен независимо от любых других настроек.