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 и кросс-портальных проектов

ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/165213/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *