Сенсорные экраны доступны на все большем количестве устройств, от телефонов до экранов настольных компьютеров. Ваше приложение должно реагировать на их прикосновения интуитивно и красиво.
Сенсорные экраны доступны на все большем количестве устройств, от телефонов до экранов настольных компьютеров. Когда ваши пользователи решают взаимодействовать с вашим пользовательским интерфейсом, ваше приложение должно реагировать на их прикосновения интуитивно понятным образом.
Реагировать на состояния элементов
Вы когда-нибудь касались или щелкали элемент на веб-странице и задавались вопросом, действительно ли сайт обнаружил его?
Простое изменение цвета элемента, когда пользователи касаются или взаимодействуют с частями вашего пользовательского интерфейса, дает базовую уверенность в том, что ваш сайт работает. Это не только облегчает разочарование, но также может дать ощущение резкости и отзывчивости.
Элементы 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;
}
Реализация пользовательских жестов
Если у вас есть идея индивидуального взаимодействия и жестов для вашего сайта, следует учитывать две темы:
- Как поддерживать все браузеры.
- Как поддерживать высокую частоту кадров.
В этой статье мы рассмотрим именно эти темы, охватывающие API, которые нам необходимо поддерживать, чтобы работать со всеми браузерами, а затем рассмотрим, как мы эффективно используем эти события.
В зависимости от того, что вы хотите, чтобы ваш жест делал, вы, вероятно, захотите, чтобы пользователь взаимодействовал с одним элементом за раз , или вы хотите, чтобы он мог взаимодействовать с несколькими элементами одновременно.
В этой статье мы рассмотрим два примера, оба демонстрирующие поддержку всех браузеров и способы поддержания высокой частоты кадров.
Первый пример позволит пользователю взаимодействовать с одним элементом. В этом случае вы можете захотеть, чтобы все события касания передавались этому одному элементу, если жест изначально начинался на самом элементе. Например, отведя палец от элемента, по которому можно проводить пальцем, можно по-прежнему управлять элементом.
Это полезно, поскольку обеспечивает большую гибкость для пользователя, но налагает ограничения на то, как пользователь может взаимодействовать с вашим пользовательским интерфейсом.
Однако если вы ожидаете, что пользователи будут взаимодействовать с несколькими элементами одновременно (с использованием мультитач), вам следует ограничить касание конкретным элементом.
Это более гибко для пользователей, но усложняет логику управления пользовательским интерфейсом и менее устойчиво к ошибкам пользователя.
Добавить прослушиватели событий
В 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.
Для событий перемещения и завершения мыши мы добавляем прослушиватели событий в метод начала жеста и добавляем прослушиватели в документ, что означает, что он может отслеживать курсор до тех пор, пока жест не будет завершен.
Для реализации этого были предприняты следующие шаги:
- Добавьте все прослушиватели TouchEvent и PointerEvent. Для MouseEvents добавьте только стартовое событие.
- Внутри обратного вызова начального жеста привяжите события перемещения мыши и завершения к документу. Таким образом, все события мыши принимаются независимо от того, происходит ли событие в исходном элементе или нет. Для PointerEvents нам нужно вызвать
setPointerCapture()
для нашего исходного элемента, чтобы получить все дальнейшие события. Затем обработайте начало жеста. - Обработка событий перемещения.
- В конечном событии удалите перемещение мыши и завершите прослушиватели из документа и завершите жест.
Ниже приведен фрагмент нашего метода 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);
Следуя этому шаблону добавления события перемещения в документ, если пользователь начинает взаимодействовать с элементом и перемещает свой жест за пределы элемента, мы продолжим получать движения мыши независимо от того, где они находятся на странице, поскольку события получаются из документа.
На этой диаграмме показано, что делают события касания, когда мы добавляем в документ события перемещения и завершения после начала жеста.
Эффективное реагирование на прикосновения
Теперь, когда мы позаботились о начальных и конечных событиях, мы можем фактически реагировать на события касания.
Для любого события запуска и перемещения вы можете легко извлечь из события 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
:
Поддержка старых версий 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 .
События касания, мыши и указателя
Эти события являются строительными блоками для добавления новых жестов в ваше приложение:
Сенсорные списки
Каждое событие касания включает три атрибута списка:
Включение активной государственной поддержки на 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);
}
}
};