Добавьте прикосновения к вашему сайту

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

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

Реагировать на состояния элементов

Вы когда-нибудь касались или щелкали элемент на веб-странице и задавались вопросом, действительно ли сайт обнаружил его?

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

Элементы DOM могут наследовать любое из следующих состояний: по умолчанию, фокус, наведение и активное. Чтобы изменить наш пользовательский интерфейс для каждого из этих состояний, нам нужно применить стили к следующим псевдоклассам :hover , :focus и :active , как показано ниже:

.btn {
  background-color: #4285f4;
}

.btn:hover {
  background-color: #296cdb;
}

.btn:focus {
  background-color: #0f52c1;

  /* The outline parameter suppresses the border
  color / outline when focused */
  outline: 0;
}

.btn:active {
  background-color: #0039a8;
}

Попробуй

Изображение, показывающее разные цвета состояний кнопок.

В большинстве мобильных браузеров состояния наведения и/или фокуса применяются к элементу после его касания.

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

Подавление стилей браузера по умолчанию

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

Большинство браузеров используют свойство CSS outline для отображения кольца вокруг элемента, когда элемент находится в фокусе. Вы можете подавить его с помощью:

.btn:focus {
    outline: 0;

    /* Add replacement focus styling here (i.e. border) */
}

Safari и Chrome добавляют цвет выделения касания, который можно предотвратить с помощью свойства CSS -webkit-tap-highlight-color :

/* Webkit / Chrome Specific CSS to remove tap
highlight color */
.btn {
  -webkit-tap-highlight-color: transparent;
}

Попробуй

Internet Explorer на Windows Phone ведет себя аналогично, но подавляется метатегом:

<meta name="msapplication-tap-highlight" content="no">

У Firefox есть два побочных эффекта, с которыми нужно справиться.

Псевдокласс -moz-focus-inner , который добавляет контур к сенсорным элементам, можно удалить, установив border: 0 .

Если вы используете элемент <button> в Firefox, к вам применяется градиент, который вы можете удалить, установив background-image: none .

/* Firefox Specific CSS to remove button
differences and focus ring */
.btn {
  background-image: none;
}

.btn::-moz-focus-inner {
  border: 0;
}

Попробуй

Отключение выбора пользователя

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

Вы можете сделать это с помощью свойства CSS user-select , но имейте в виду, что выполнение этого для контента может крайне раздражать пользователей, если они захотят выделить текст в элементе. Поэтому используйте его осторожно и экономно.

/* Example: Disable selecting text on a paragraph element: */
p.disable-text-selection {
  user-select: none;
}

Реализация пользовательских жестов

Если у вас есть идея индивидуального взаимодействия и жестов для вашего сайта, следует учитывать две темы:

  1. Как поддерживать все браузеры.
  2. Как поддерживать высокую частоту кадров.

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

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

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

Пример GIF-изображения касания документа

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

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

Пример GIF-изображения касания элемента

Однако если вы ожидаете, что пользователи будут взаимодействовать с несколькими элементами одновременно (с использованием мультитач), вам следует ограничить касание конкретным элементом.

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

Добавить прослушиватели событий

В Chrome (версии 55 и более поздних), Internet Explorer и Edge PointerEvents является рекомендуемым подходом для реализации пользовательских жестов.

В других браузерах TouchEvents и MouseEvents являются правильным подходом.

Замечательной особенностью PointerEvents является то, что он объединяет несколько типов ввода, включая события мыши, касания и пера, в один набор обратных вызовов. События, которые нужно прослушивать, — это pointerdown , pointermove , pointerup и pointercancel .

Эквивалентами в других браузерах являются touchstart , touchmove , touchend и touchcancel для событий касания, и если вы хотите реализовать тот же жест для ввода с помощью мыши, вам нужно будет реализовать mousedown , mousemove и mouseup .

Если у вас есть вопросы о том, какие события использовать, ознакомьтесь с этой таблицей событий касания, мыши и указателя .

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

Вот пример прослушивания начала взаимодействия.

// Check if pointer events are supported.
if (window.PointerEvent) {
  // Add Pointer Event Listener
  swipeFrontElement.addEventListener('pointerdown', this.handleGestureStart, true);
  swipeFrontElement.addEventListener('pointermove', this.handleGestureMove, true);
  swipeFrontElement.addEventListener('pointerup', this.handleGestureEnd, true);
  swipeFrontElement.addEventListener('pointercancel', this.handleGestureEnd, true);
} else {
  // Add Touch Listener
  swipeFrontElement.addEventListener('touchstart', this.handleGestureStart, true);
  swipeFrontElement.addEventListener('touchmove', this.handleGestureMove, true);
  swipeFrontElement.addEventListener('touchend', this.handleGestureEnd, true);
  swipeFrontElement.addEventListener('touchcancel', this.handleGestureEnd, true);

  // Add Mouse Listener
  swipeFrontElement.addEventListener('mousedown', this.handleGestureStart, true);
}

Попробуй

Обработка взаимодействия отдельных элементов

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

TouchEvents будет отслеживать жест после его запуска независимо от того, где происходит касание, а PointerEvents будет отслеживать события независимо от того, где происходит касание после того, как мы вызываем setPointerCapture для элемента DOM.

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

Для реализации этого были предприняты следующие шаги:

  1. Добавьте все прослушиватели TouchEvent и PointerEvent. Для MouseEvents добавьте только стартовое событие.
  2. Внутри обратного вызова начального жеста привяжите события перемещения мыши и завершения к документу. Таким образом, все события мыши принимаются независимо от того, происходит ли событие в исходном элементе или нет. Для PointerEvents нам нужно вызвать setPointerCapture() для нашего исходного элемента, чтобы получить все дальнейшие события. Затем обработайте начало жеста.
  3. Обработка событий перемещения.
  4. В конечном событии удалите перемещение мыши и завершите прослушиватели из документа и завершите жест.

Ниже приведен фрагмент нашего метода handleGestureStart() , который добавляет в документ события перемещения и завершения:

// Handle the start of gestures
this.handleGestureStart = function(evt) {
  evt.preventDefault();

  if(evt.touches && evt.touches.length > 1) {
    return;
  }

  // Add the move and end listeners
  if (window.PointerEvent) {
    evt.target.setPointerCapture(evt.pointerId);
  } else {
    // Add Mouse Listeners
    document.addEventListener('mousemove', this.handleGestureMove, true);
    document.addEventListener('mouseup', this.handleGestureEnd, true);
  }

  initialTouchPos = getGesturePointFromEvent(evt);

  swipeFrontElement.style.transition = 'initial';
}.bind(this);

Попробуй

Конечный обратный вызов, который мы добавляем, — это handleGestureEnd() , который удаляет прослушиватели событий перемещения и завершения из документа и освобождает захват указателя после завершения жеста следующим образом:

// Handle end gestures
this.handleGestureEnd = function(evt) {
  evt.preventDefault();

  if (evt.touches && evt.touches.length > 0) {
    return;
  }

  rafPending = false;

  // Remove Event Listeners
  if (window.PointerEvent) {
    evt.target.releasePointerCapture(evt.pointerId);
  } else {
    // Remove Mouse Listeners
    document.removeEventListener('mousemove', this.handleGestureMove, true);
    document.removeEventListener('mouseup', this.handleGestureEnd, true);
  }

  updateSwipeRestPosition();

  initialTouchPos = null;
}.bind(this);

Попробуй

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

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

Иллюстрация привязки событий касания к документу в Touchstart.

Эффективное реагирование на прикосновения

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

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

В следующем примере проверяется, произошло ли событие из TouchEvent , проверяя, существует ли targetTouches . Если да, то он извлекает clientX и clientY с первого касания. Если событие является PointerEvent или MouseEvent , оно извлекает clientX и clientY непосредственно из самого события.

function getGesturePointFromEvent(evt) {
    var point = {};

    if (evt.targetTouches) {
      // Prefer Touch Events
      point.x = evt.targetTouches[0].clientX;
      point.y = evt.targetTouches[0].clientY;
    } else {
      // Either Mouse event or Pointer Event
      point.x = evt.clientX;
      point.y = evt.clientY;
    }

    return point;
  }

Попробуй

TouchEvent имеет три списка, содержащих данные касания:

  • touches : список всех текущих касаний экрана, независимо от того, на каком элементе DOM они находятся.
  • targetTouches : список касаний в данный момент к элементу DOM, к которому привязано событие.
  • changedTouches : список касаний, которые изменились, что привело к запуску события.

В большинстве случаев targetTouches дает вам все, что вам нужно и чего вы хотите. (Дополнительную информацию об этих списках см. в разделе Сенсорные списки ).

Используйте запросAnimationFrame

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

Используя requestAnimationFrame() , у нас есть возможность обновить пользовательский интерфейс непосредственно перед тем, как браузер собирается отрисовать кадр, и это поможет нам перенести часть работы с обратными вызовами наших событий.

Если вы не знакомы с requestAnimationFrame() , вы можете узнать больше здесь .

Типичная реализация — сохранить координаты x и y из событий начала и перемещения и запросить кадр анимации внутри обратного вызова события перемещения.

В нашей демонстрации мы сохраняем начальную позицию касания в handleGestureStart() (ищем initialTouchPos ):

// Handle the start of gestures
this.handleGestureStart = function(evt) {
  evt.preventDefault();

  if (evt.touches && evt.touches.length > 1) {
    return;
  }

  // Add the move and end listeners
  if (window.PointerEvent) {
    evt.target.setPointerCapture(evt.pointerId);
  } else {
    // Add Mouse Listeners
    document.addEventListener('mousemove', this.handleGestureMove, true);
    document.addEventListener('mouseup', this.handleGestureEnd, true);
  }

  initialTouchPos = getGesturePointFromEvent(evt);

  swipeFrontElement.style.transition = 'initial';
}.bind(this);

Метод handleGestureMove() сохраняет позицию своего события перед запросом кадра анимации, если это необходимо, передавая нашу функцию onAnimFrame() в качестве обратного вызова:

this.handleGestureMove = function (evt) {
  evt.preventDefault();

  if (!initialTouchPos) {
    return;
  }

  lastTouchPos = getGesturePointFromEvent(evt);

  if (rafPending) {
    return;
  }

  rafPending = true;

  window.requestAnimFrame(onAnimFrame);
}.bind(this);

Значение onAnimFrame — это функция, которая при вызове изменяет наш пользовательский интерфейс для его перемещения. Передавая эту функцию в requestAnimationFrame() , мы сообщаем браузеру вызвать ее непосредственно перед обновлением страницы (т. е. отрисовкой любых изменений на странице).

В обратном вызове handleGestureMove() мы сначала проверяем, имеет ли rafPending значение false, что указывает на то, вызывалась ли onAnimFrame() функцией requestAnimationFrame() с момента последнего события перемещения. Это означает, что у нас есть только один requestAnimationFrame() , ожидающий запуска в любой момент времени.

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

function onAnimFrame() {
  if (!rafPending) {
    return;
  }

  var differenceInX = initialTouchPos.x - lastTouchPos.x;
  var newXTransform = (currentXPosition - differenceInX)+'px';
  var transformStyle = 'translateX('+newXTransform+')';

  swipeFrontElement.style.webkitTransform = transformStyle;
  swipeFrontElement.style.MozTransform = transformStyle;
  swipeFrontElement.style.msTransform = transformStyle;
  swipeFrontElement.style.transform = transformStyle;

  rafPending = false;
}

Управление жестами с помощью сенсорных действий

Свойство CSS touch-action позволяет вам управлять поведением элемента при касании по умолчанию. В наших примерах мы используем touch-action: none чтобы запретить браузеру что-либо делать при касании пользователя, что позволяет нам перехватывать все события касания.

/* Pass all touches to javascript: */
button.custom-touch-logic {
  touch-action: none;
}

Использование touch-action: none — это своего рода ядерный вариант, поскольку он предотвращает все действия браузера по умолчанию. Во многих случаях один из приведенных ниже вариантов является лучшим решением.

touch-action позволяет отключить жесты, реализованные браузером. Например, IE10+ поддерживает жест двойного касания для масштабирования. Установив touch-action для manipulation , вы предотвратите поведение двойного касания по умолчанию.

Это позволяет вам самостоятельно реализовать жест двойного касания.

Ниже приведен список часто используемых значений touch-action :

Параметры сенсорного действия
touch-action: none Браузер не будет обрабатывать никакие сенсорные взаимодействия.
touch-action: pinch-zoom Отключает все взаимодействия браузера, такие как «touch-action: none», кроме «pinch-zoom», который все еще обрабатывается браузером.
touch-action: pan-y pinch-zoom Обрабатывайте горизонтальную прокрутку в JavaScript, не отключая вертикальную прокрутку или масштабирование (например, карусели изображений).
touch-action: manipulation Отключает жест двойного касания, что позволяет избежать задержки щелчка браузером. Оставляет прокрутку и масштабирование до браузера.

Поддержка старых версий IE

Если вы хотите поддерживать IE10, вам потребуется обрабатывать версии PointerEvents с префиксом поставщика.

Чтобы проверить поддержку PointerEvents вы обычно ищете window.PointerEvent , но в IE10 вам нужно искать window.navigator.msPointerEnabled .

Имена событий с префиксами поставщиков: 'MSPointerDown' , 'MSPointerUp' и 'MSPointerMove' .

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

var pointerDownName = 'pointerdown';
var pointerUpName = 'pointerup';
var pointerMoveName = 'pointermove';

if (window.navigator.msPointerEnabled) {
  pointerDownName = 'MSPointerDown';
  pointerUpName = 'MSPointerUp';
  pointerMoveName = 'MSPointerMove';
}

// Simple way to check if some form of pointerevents is enabled or not
window.PointerEventsSupport = false;
if (window.PointerEvent || window.navigator.msPointerEnabled) {
  window.PointerEventsSupport = true;
}

Для получения дополнительной информации ознакомьтесь с этой статьей обновлений от Microsoft .

Ссылка

Псевдоклассы для состояний касания

Сорт Пример Описание
:наведите курсор
Кнопка в нажатом состоянии
Вводится, когда курсор находится над элементом. Изменения в пользовательском интерфейсе при наведении помогают побудить пользователей взаимодействовать с элементами.
:фокус
Кнопка с состоянием фокуса
Вводится, когда пользователь перемещается по элементам на странице. Состояние фокуса позволяет пользователю узнать, с каким элементом он в данный момент взаимодействует; также позволяет пользователям легко перемещаться по пользовательскому интерфейсу с помощью клавиатуры.
:активный
Кнопка в нажатом состоянии
Вводится при выборе элемента, например, когда пользователь щелкает или касается элемента.

Полный справочник по событиям касания можно найти здесь: События касания W3C .

События касания, мыши и указателя

Эти события являются строительными блоками для добавления новых жестов в ваше приложение:

События касания, мыши, указателя
touchstart , mousedown , pointerdown Это вызывается, когда палец впервые касается элемента или когда пользователь нажимает кнопку мыши.
touchmove , mousemove , pointermove Это вызывается, когда пользователь перемещает палец по экрану или перетаскивает мышью.
touchend , mouseup , pointerup Это вызывается, когда пользователь отрывает палец от экрана или отпускает мышь.
pointercancel touchcancel касанияотмена Это вызывается, когда браузер отменяет сенсорные жесты. Например, пользователь касается веб-приложения, а затем переключает вкладки.

Сенсорные списки

Каждое событие касания включает три атрибута списка:

Атрибуты сенсорного события
touches Список всех текущих касаний экрана, независимо от того, к каким элементам прикасаются.
targetTouches Список касаний, начавшихся с элемента, являющегося целью текущего события. Например, если вы привязываетесь к <button> , в данный момент вы будете получать касания только этой кнопки. Если вы привяжетесь к документу, вы получите все касания, которые в данный момент есть в документе.
changedTouches Список касаний, которые изменились, что привело к запуску события:
  • Для события touchstart — список точек касания, которые только что стали активными при текущем событии.
  • Для события touchmove — список точек касания, которые переместились с момента последнего события.
  • Для событий touchend и touchcancel — список точек касания, которые только что были удалены с поверхности.

Включение активной государственной поддержки на iOS

К сожалению, Safari на iOS не применяет активное состояние по умолчанию. Чтобы оно заработало, вам необходимо добавить прослушиватель событий touchstart в тело документа или к каждому элементу.

Вы должны сделать это после теста пользовательского агента, чтобы он запускался только на устройствах iOS.

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

window.onload = function() {
  if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
    document.body.addEventListener('touchstart', function() {}, false);
  }
};

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

window.onload = function() {
  if (/iP(hone|ad)/.test(window.navigator.userAgent)) {
    var elements = document.querySelectorAll('button');
    var emptyFunction = function() {};

    for (var i = 0; i < elements.length; i++) {
        elements[i].addEventListener('touchstart', emptyFunction, false);
    }
  }
};