company_banner

Touch-web: Swipe

    Этим постом мы продолжаем серию статей на тему разработки веб-интерфейсов для touch-устройств.

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

    Сегодня речь пойдет о swipe, в простонародье – листалке. Swipe позволяет перелистывать «страницы» привычным движением пальца. О том, как грамотно реализовать swipe, я расскажу на примере блока новостей на главной странице портала Mail.Ru.





    Начнем с ключевых моментов, на которые надо обратить внимание при разработке листалки:
    • Во-первых, нужно точно определить, чего хочет пользователь — перелистнуть страницу блока или проскроллить
    • Во-вторых, нужно перемещать страницу при движении пальца вслед за ним
    • В-третьих, если листать некуда, нужно дать пользователю это понять
    • В-четвертых, реагировать на касание только первым пальцем
    • И, наконец, перелистнуть страницу в нужную сторону, когда пользователь отпустил палец


    Touch events


    Специально для работы с touch-устройствами ввода в браузерах реализованы следующие события:
    • touchstart – коснулись пальцем
    • touchmove – подвинули палец
    • touchend – отпустили палец
    • и touchcancel – почти как touchend, но происходит в момент, когда мы палец не отпускали, но касаемся уже другого элемента. Например, поступил входящий звонок, и на экране уже не браузер.

    Но некоторые браузеры на touch-устройствах — например, WP IE — не поддерживают эти события.

    В целом ничего, казалось бы, не мешает имитировать аналогичное поведение через мышиные события — как, например, в десктопном drag’n’drop:
    • touchstart – mousedown
    • touchmove – mousemove
    • touchend, touchcancel – mouseup

    Но мышиные события в мобильных браузерах работают довольно странно ¬–
    события происходят, только если был click (да и то сначала click, а лишь затем — mousedown, mousemove и mouseup).

    Потому swipe реализован только на touch-событиях.

    В touch-событие падает полезная информация о касаниях:
    • touches – коллекция всех касаний, происходящих в данный момент
    • changedTouches – касания, по которым есть изменения, т.е. непосредственно те, которые вызвали событие
    • targetTouches – касания, ассоциированные с target события. Касания, произошедшие внутри элемента, на который навешан текущий слушатель

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

    Также объект касания содержит информацию о координатах относительно viewport, экрана и страницы.

    Логика довольно простая:

    Touchstart

    /*
    	Проверяем флаг started (не был ли уже начат swipe) и кол-во touches. Если swipe уже идет или toches не один, значит, пользователь орудует несколькими пальцами, и реагировать на это не надо.
    */
    if (e.touches.length != 1 || started){
    	return;
    }
    
    /*
    	Ставим флаг detecting = true, означающий, что далее на touchmove надо определить, что имел в виду пользователь — хотел ли он перелистнуть страницу, просто прокрутить или тапнуть по ссылке
    */
    detecting = true;
    
    // Запоминаем текущее касание и его координаты
    touch = e.changedTouches[0];
    x = touch.pageX;
    y = touch.pageY;
    


    Touchmove

    /*
    	Первым делом проверяем: если не стоит ни started, ни detecting, не делаем ничего. Не наш случай
    */
    if (!started && !detecting){
    	return;
    }
    
    /*
    	В зависимости от установленного состояния запускаем определение перелистывания или его отрисовку
    */
    if (detecting){
    	detect();
    }
    
    if (started){
    	draw();
    }
    
    /*
    	Определение
    */
    function detect(){
    	/*
    		Получаем сохраненное ранее касание из changedTouches. Если среди касаний нет нашего, значит, пользователь коснулся экрана еще одним пальцем. На это касание реагировать не надо
    	*/
    	if (e.changedTouches.indexOf(touch) == -1){
    		return;
    	}
    
    	/*
    		Самым простым способом определения того, хотел ли пользователь перелистнуть страницу, является сравнение смещений пальца по осям. 
    		Если смещение больше по оси х, чем по у, значит, пользователь листает.
    	*/
    	if (abs(x - newX) >= abs(y - newY)){
    		/*
    			Если не отменить поведение по умолчанию, то второго touchmove может и не быть (например, в Android). Поэтому необходимо определить swipe с первого раза и отменить поведение по умолчанию – скроллинг страницы
    		*/
    		e.preventDefault();
    
    		// Запоминаем, что началось перелистывание
    		started = true;
    	}
    
    	// В любом случае заканчиваем определение, т.к. шанс определить у нас один
    	detecting = false;
    }
    
    // Отрисовка реакции на движение пальца
    function draw(){
    	/*
    		Отменяем поведение по умолчанию, дабы в дальнейшем срабатывали обработчики touchmove и не срабатывал скролл
    	*/
    	e.preventDefault();
    
    	/*
    		Получаем сохраненное ранее касание из changedTouches. Если среди касаний нет нашего, значит, пользователь коснулся экрана еще одним пальцем. На это касание реагировать не надо
    	*/
    	if (e.changedTouches.indexOf(touch) == -1){
    		return;
    	}
    
    	/*
    		Вычисляем смещение пальца относительно исходных координат касания.
    		На эту величину надо сместить страницу, чтобы она «следовала» за пальцем
    	*/
    	delta = x – newX;
    
    	/*
    		Если листать некуда, делим смещение на некоторую величину для создания визуального эффекта «сопротивления движению» страницы.
    		Таким образом, даем пользователю понять, что дальше страниц нет
    	*/
    	if (delta > 0 && !leftPage || delta < 0 && !rightPage){
    		delta = delta / 5;
    	}
    
    	// Отрисовываем смещение, о чем чуть позже
    	moveTo(delta);
    }
    


    Touchend/Touchcancel

    // Как и ранее, отсекаем ненужные касания
    if (e.changedTouches.indexOf(touch) == -1 || !started){
    	return;
    }
    
    /*
    	Отменяем поведение по умолчанию. Например, если пользователь отпустил палец на ссылке, то может произойти переход по ней, чего нам не надо.
    */
    e.preventDefault();
    
    /*
    	Определяем, в какую сторону нужно произвести перелистывание
    */
    swipeTo = delta < 0 ? 'left' : 'right';
    
    // Отрисовываем перелистывание, о чем чуть позже 
    swipe(swipeTo);
    

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

    Рендеринг




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



    Каждая из них должна быть шириной не больше и не меньше родителя.



    Центральная всегда видна, левые смещены на 100% влево, а правые – на 100% вправо.



    Многим хочется использовать таблицу, но она не подходит по нескольким причинам.
    Чтобы получить ширину страницы 100% родителя, нужно каким-то образом задать ширину всей таблицы в <кол-во страниц>*100%, и самим страницам в 100%/<кол-во страниц>. Без дополнительных JS-манипуляций это невозможно и грозит погрешностями и неровностями, а так же дополнительными расчетами.

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

    Мы выбрали другой вариант:

    .swipe {
    	position: relative;
    	overflow: hidden;
    	height: 300px;
    }
    

    Обертке блока указали высоту, position:relative и overflow:hidden, тем самым значительно снизив нагрузку на браузер при рендеринге и расчетах – мы получаем обособленную ветку DOM-дерева внутри листалки, браузер не пересчитывает другие части дерева при изменениях внутри.



    Страницы же были разбиты на 3 логичные группы:
    1. страницы слева
    2. центральная страница – всегда одна
    3. и страницы справа

    .swipe__page {
    	overflow: hidden;
    	position: absolute;
    	left: 0;
    	top: 0;
    	width: 100%;
    	height: 100%;
    }
    

    Чтобы ускорить рендеринг, нужно максимально обособить страницы друг от друга, потому для каждой страницы указаны position:absolute; и соответствующие координаты и размеры.

    .swipe__page_animating {
    	transition: transform 200ms linear;
    }
    

    Далее встает вопрос смещения страниц вправо и влево.
    Анимируются страницы средствами CSS-transition.
    Если анимировать left, получается очень медленно. Гораздо быстрее работает translate, а еще быстрее — translate3d из CSS-трансформаций.

    .swipe__page_left {
    	transform: translate(-100%, 0);
    	transform: translate3d(-100%, 0, 0);
    }
    
    .swipe__page_center {
    	transform: translate(0, 0);
    	transform: translate3d(0, 0, 0);
    }
    
    .swipe__page_right {
    	transform: translate(100%, 0);
    	transform: translate3d(100%, 0, 0);
    }
    

    Поэтому всем страницам указан left:0 и оба варианта translate — на случай, если браузер не поддерживает 3d-трансформации, со значениями смещения по горизонтали 100%, 0 и 100% для левых, центральной и правых страниц соответственно.



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



    Из всех страниц выбираются три:
    1. центральная, видимая
    2. следующая страница слева
    3. следующая страница справа



    transform: translate(delta, 0);
    

    Изначально класс swipe__page_animating (добавляет CSS-transition) не установлен. По событию touchmove для страницы устанавливается соответствующий translate, со значением смещения по горизонтали равным смещению пальца (либо смещению, деленному на «коэффициент сопротивления»). Т.е. страница просто двигается вместе с пальцем.



    На самом деле все несколько сложнее. Одновременно двигаются три страницы: текущая видимая, и по странице слева и справа, что создает эффект непрерывной ленты. Соответственно, для правой и левой страниц смещение равно его ширине с соответствующим положению знаком (для правой – плюс, для левой – минус) плюс смещение пальца.



    По событию touchend или touchcancel страницам устанавливается класс swipe__page_animating, включающий CSS-анимации, и величины смещений, соответствующие финальному положению – либо перелистываем, либо устанавливаем исходные значения.



    По событию transitionend, т.е. по окончанию анимации:
    • удаляется класс swipe__page_animating
    • заново выбираются «следующие» страницы, переключаются классы _left, _center, _right в соответствии с новым положением. Например, если пользователь перелистнул влево, то центральная страница становится правой, левая – центральной, правая просто кладется в стопку правых, а из стопки левых выбирается новая «следующая»
    • сбрасываются установленные скриптом смещения для того, чтобы при изменении ширины всей страницы — например, при смене ориентации устройства — страницы блока автоматически адаптировались браузером к новым условиям
    • очищаются установленные флаги вроде started

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

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



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

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

    На этом на сегодня все. Вопросы и обсуждение приветствуются.

    Егор Дыдыкин,
    лидер команды разработки главной страницы Mail.Ru и кросс-портальных проектов
    Mail.Ru Group 789,16
    Строим Интернет
    Поделиться публикацией
    Комментарии 27
    • +4
      Это только для webkit актуально, проверялось на gecko, presto?
      Отличная статья! Спасибо!
      • +3
        По большому счету это актуально для всех. За исключением своей модели тач-событий в IE.
    • 0
      а насколько отличается модель WP IE?
      • +1
        Довольно сильно отличается. От самих событий (например, у IE есть MSGestureTap и MSPointerHover) до данных в объекте события.
        Как всегда заточено под Windows.
        Подробнее почитать можно здесь.
        Поддерживается, начиная с IE10
        • +2
          Как всегда, IE впереди всей планеты.
      • 0
        >> лидер команды разработки главной страницы Mail.Ru
        Сорри за оффтоп, но если не секрет, сколько вас трудится над ней?
        • +2
          Если интересуют именно frontend разработчики то двое.
          Егор по совместительству frontend разработчик.

          Команда в целом сильно варируется от текущих задач.
        • +1
          Да, статья отличная, такое решение можно портировать и на другие платформы.
          • +6
            На фразе «Но некоторые браузеры на touch-устройствах — например, WP IE — не поддерживают эти события» зубы сами собой скрипнули, а на глаза навернулись слёзы от бессилия. Ну как! Опять! 21-й век же! Тач! Windows Phone! Нет же никаких оправданий в стиле «это для совместимости с IE 3.0 в Win 3.11 для рабочих групп»!
            • +1
              В-четвертых, реагировать на касание только первым пальцем

              Было бы красиво реагировать на касание 2 (и более) пальцами, переходя сразу в начало или сразу в конец, в зависимости от направления жеста.
              • 0
                Интересный ход. Возьмем на заметку.
              • 0
                >> Этим постом мы продолжаем серию статей на тему разработки веб-интерфейсов для touch-устройств.

                Почему-то не получилось найти предыдущие статьи серии)
                • 0
                  Промашечка вышла. Имелась ввиду серия статей о разработке мобильных веб-интерфейсов в целом. В частности, для touch-устройств.
                • 0
                  blog.kojo.com.au/flickable-zepto-plugin/

                  Плагин для Zepto, выполняющий аналогичную функцию.
                  • 0
                    Всегда приятно реализировать функционал самому :)
                    К тому же чрезмерное использование чревато последствиями
                    • 0
                      Это для тех, кому не так приятно писать своё :)
                    • 0
                      Пост про меанику работы с тач устройствами на примере реальной задачи.
                      Интересен тем кто любит копать глубоко и понимать что происходит у него на проекте.
                      • +1
                        Неплохо слайдится. Но. Попробуйте проскроллить, например, первую демку по вертикали, начав движение касанием кастомизированного блока.
                        Не получится. Будет пытаться перелистнуть карточку.
                        Представьте себе страницу с такими блоками на всю ширину и без больших отступов между ними. Epic fail надо сказать.
                        Именно по-этому пишутся свои решения. Как говориться, «хочешь сделать хорошо то, что надо...»
                        Не без приключений и багов, конечно.
                        • 0
                          У плагина есть некоторые настройки. Вроде flickDirection и preventDefault должны решить проблему скролла. Точно не помню. Я как раз боролся ровно с обратной проблемой, чтобы вертикального скролла не было :)
                          • 0
                            Хорошо, если так.
                            Но смысл моего посыла, я думаю, все равно проглядывается :)
                            • 0
                              Да, посыл-то понятен, но т.к. пост без ссылок, то может показаться, что существующие решения не рассматривались :)
                              Кстати, у вас работает хорошо, кроме, пожалуй, этого «странного» эффекта с запаздывающим заголовком )
                              • 0
                                Изначально запаздывания не было. Но Samsung сделал подарок. У него в Galaxy S III (к сожалению, не помню в какой версии прошивки) при паралелльных анимациях (перелистывание страницы и прокрутка табов/заголовков) половина страницы попросту скрывалась на время анимации. Потому разнесли по времени.
                                Надо будет перепроверить на досуге, вдруг поправили в каком из обновлений.
                            • 0
                              пробовал я его настраивать, толку особенно нет. при скроллинге контент всёравно дергается. пытается перелистнуть.

                              в итоге — закомментировал кусок самого плагина (метода move частично).
                        • 0
                          А я не понял где это работает… Какой урл? С сайта главного на новости и мобильная версия — не то совсем
                          • 0
                            mail.ru с touch-смартфона (Android, iOS, etc), блоки с погодой и новостями.
                          • 0
                            При исследовании на iPad-е, свойство event.changedTouches не является массивом, поэтому event.changedTouches.indexOf(touch) не работает. Приходится сравнивать напрямую event.changedTouches[0] === touch.

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

                            Самое читаемое