Инерционные устройства ввода — это такие устройства, как тачскрины, трекпады, magic mouse и пр., По своей работе трекпады и magic mouse напоминают тачскрины мобильных устройств, т.е. продолжают генерировать события мышинного колеса после того, как пользователь закончил жест. Но в отличие от оных у нас отсутствует нативное событие touchstart. Все, что мы имеем, это объект события wheel. Touchstart часто бывает необходим, чтоб комфортно реализовать работу так называемых fullpage-сайтов, где при скроллинге происходит переход между экранами. Примером такого сайта может послужить alfabank. На нем же присутствует проблема прокрутки двух экранов подряд при использовании magic mouse или трекпада (особенно на макбуках). Достаточно слабый жест скроллинга вниз прокручивает ко второму, а потом сразу к третьему экрану. Чтоб попасть на второй экран, приходится пользоваться скроллбаром. Именно такого рода проблемы мы попытались решить используя лишь объект события wheel.
Итак, как же реализовать touchstart?
Начнем с самого просто и будем двигаться по нарастающей. Простейший вариант реализации touchstart:
0. Тишина.
1. Начало генерироваться событие wheel. Это и есть наш touchstart
2. Предположим, что между событиями никак не может быть больше 200мс (на самом деле от 10 до 30 мс), поэтому просто через 200 мс после последнего события снова генерируем touchstart при появлении новых объектов событий.
Уже неплохо, две страницы точно не проскролятся за одно движение. Но есть существенная проблема: попытка начать новую итерацию скроллинга до конца текущей приведет только к удлинению текущей.
Чтоб решить эту проблему, необходимо понять момент, когда пользователь произвел новый жест до окончания работы предыдущей итерации.
Проанализировав объект события wheel, мы сразу обратили внимание на поле deltaY. Оно отражает силу, с которой в текущий момент времени работает устройство.
Если отобразить значения deltaY на графике, то жест будет иметь примерно такой вид:
А это то, что нам надо отловить.
Таким образом задача сводится к тому, чтобы сравнивать предыдущее значение deltaY с текущим. И если оно больше, значит пользователь начал новое осознанное движение, то есть новый touchstart произошел.
Вроде бы все отлично, интерфейс стал более отзывчив, стало можно делать любое кол-во жестов подряд, не дожидаясь окончания работы предыдущий итерации. Но на практике алгоритм давал сбой: touchstart зачастую генерировался чаще необходимого. Иногда по 2-3 раза за одну итерацию. Почему же так происходило? Анализе числового ряда deltaY из одной итерации показал, что иногда несмотря на спад итерации (то есть замедлении работы инерции, выраженным во все меньших значениях deltaY в каждом следующем событии wheel) иногда текущий deltaY может быть равен или больше предыдущего. Причем иногда это происходило через раз:
21, 17, 15, 18, 12, 14, 10, 7…
либо два подряд увеличения
21, 17, 15, 18, 19, 14, 10…
Многочисленные опыты показали, что таких ситуаций практически не бывает больше трех подряд для обоих случаев. Вносим корректировки в алгоритм: теперь touchstart генерируется только если текущий deltaY больше предыдущего и следующий deltaY больше текущего. Теперь все работает неплохо, и явных проблем больше нет.
Взяв за основу этот подход, мы написали плагин wheel-indicator. В анализе также используются другие факторы, но описывать их все в рамках этой статьи нет смысла.
Плагин также можно использовать, как легкую замену известному jquery-mousewheel в случаях, когда вам требуется только кроссбраузерно определить направление работы колесика. Если мышка пользователя триггерит черезмерно много событий, плагин также будет это нормализовать. Иногда это бывает полезно, например, скроллить колесом такую карусель не очень комфортно. Кроме этого на основе плагина можно реализовывать неблокирующие интерфейсы. Например, здесь можно скроллить в процессе анимации в любую сторону, и кол-во переходов будет равно кол-ву жестов пользователя.
Почитать о подключении, документацию и скачать плагин вы можете на странице репозитория.
Итак, как же реализовать touchstart?
Начнем с самого просто и будем двигаться по нарастающей. Простейший вариант реализации touchstart:
0. Тишина.
1. Начало генерироваться событие wheel. Это и есть наш touchstart
2. Предположим, что между событиями никак не может быть больше 200мс (на самом деле от 10 до 30 мс), поэтому просто через 200 мс после последнего события снова генерируем touchstart при появлении новых объектов событий.
Уже неплохо, две страницы точно не проскролятся за одно движение. Но есть существенная проблема: попытка начать новую итерацию скроллинга до конца текущей приведет только к удлинению текущей.
Чтоб решить эту проблему, необходимо понять момент, когда пользователь произвел новый жест до окончания работы предыдущей итерации.
Проанализировав объект события wheel, мы сразу обратили внимание на поле deltaY. Оно отражает силу, с которой в текущий момент времени работает устройство.
Если отобразить значения deltaY на графике, то жест будет иметь примерно такой вид:
А это то, что нам надо отловить.
Таким образом задача сводится к тому, чтобы сравнивать предыдущее значение deltaY с текущим. И если оно больше, значит пользователь начал новое осознанное движение, то есть новый touchstart произошел.
Вроде бы все отлично, интерфейс стал более отзывчив, стало можно делать любое кол-во жестов подряд, не дожидаясь окончания работы предыдущий итерации. Но на практике алгоритм давал сбой: touchstart зачастую генерировался чаще необходимого. Иногда по 2-3 раза за одну итерацию. Почему же так происходило? Анализе числового ряда deltaY из одной итерации показал, что иногда несмотря на спад итерации (то есть замедлении работы инерции, выраженным во все меньших значениях deltaY в каждом следующем событии wheel) иногда текущий deltaY может быть равен или больше предыдущего. Причем иногда это происходило через раз:
21, 17, 15, 18, 12, 14, 10, 7…
либо два подряд увеличения
21, 17, 15, 18, 19, 14, 10…
Многочисленные опыты показали, что таких ситуаций практически не бывает больше трех подряд для обоих случаев. Вносим корректировки в алгоритм: теперь touchstart генерируется только если текущий deltaY больше предыдущего и следующий deltaY больше текущего. Теперь все работает неплохо, и явных проблем больше нет.
Взяв за основу этот подход, мы написали плагин wheel-indicator. В анализе также используются другие факторы, но описывать их все в рамках этой статьи нет смысла.
Обычные мышки
Плагин также можно использовать, как легкую замену известному jquery-mousewheel в случаях, когда вам требуется только кроссбраузерно определить направление работы колесика. Если мышка пользователя триггерит черезмерно много событий, плагин также будет это нормализовать. Иногда это бывает полезно, например, скроллить колесом такую карусель не очень комфортно. Кроме этого на основе плагина можно реализовывать неблокирующие интерфейсы. Например, здесь можно скроллить в процессе анимации в любую сторону, и кол-во переходов будет равно кол-ву жестов пользователя.
Тестирование
Т.к. этот алгоритм не окончательный и возможно будет улучшаться, захотелось иметь возможность его тестировать. По сути на вход плагин принимает числовой ряд из deltaY и анализирует его. А значит для написания тестов достаточно nodejs и travis-ci.org для тестирования коммитов.
Чтоб иметь возможность тестировать плагин в nodejs, необходимо, чтоб он мог экспортировать себя в формате commonjs.
Для этого добавляем проверку и экспорт констурктора:
Входные данные для плагина поступают через объект события после создания обработчика при помощи addEventListener. Таким образом, в тестах нам необходимо «замокать» этот метод:
, где delta — это массив тестового числового ряда из deltaY. Для удобного получения таких рядов с различных устройств и ОС мы сделали тестовый стенд.
Вот собственно и все, теперь только остается зареквайрить плагин, создать инстанс и сверить полученные от плагина данные с эталонными.
Пример входящих данных для теста:
Чтоб иметь возможность тестировать плагин в nodejs, необходимо, чтоб он мог экспортировать себя в формате commonjs.
Для этого добавляем проверку и экспорт констурктора:
if (typeof exports === 'object') {
module.exports = WheelIndicator;
}
Входные данные для плагина поступают через объект события после создания обработчика при помощи addEventListener. Таким образом, в тестах нам необходимо «замокать» этот метод:
global.document = {
addEventListener: function(type, handler){
currentDeltaArr.forEach(function(delta){
handler({
deltaY: delta
});
});
}
};
, где delta — это массив тестового числового ряда из deltaY. Для удобного получения таких рядов с различных устройств и ОС мы сделали тестовый стенд.
Вот собственно и все, теперь только остается зареквайрить плагин, создать инстанс и сверить полученные от плагина данные с эталонными.
Пример входящих данных для теста:
down: {
moves: [ 'down' ],
delta: [1,4,12,32,55,69,154,156,158,148,137,130,122,116,111,108,103,97,93,88,84,80,74,71,65,61,57,54,50,46,42,39,36,33,31,27,25,23,21,18,17,15,14,13,12,11,9,8,8,7,6,6,14,4,4,3,3,3,2,2,4,1,2,1,1,1,1,1,1,1,1,1,1],
device: 'Mac OSX notebook trackpad'
}
Почитать о подключении, документацию и скачать плагин вы можете на странице репозитория.