Стивен Вудс, интерфейсный инженер Flickr, объясняет, как создать простой лайтбокс с поддержкой жестов, и дает советы по улучшению удобства и производительности сенсорных интерфейсов.
Требуемые знания: Средний CSS, средний-продвинутый JavaScript Требования: Устройство Android или IOS Затраты времени: 2-3 часа Скачать исходники Посмотреть демо
Виджеты лайтбокса стали стандартом в Интернете с тех пор, как в 2005 году была выпущена первая версия Lightbox.js. Лайтбокс создает модальное диалоговое окно для просмотра больших изображений, обычно с кнопками «Далее» и «Предыдущий» для перехода между слайдами.
После бума использования сенсорных устройств веб-сайты обновили лайтбоксы для поддержки взаимодействия с помощью жестов с разной степенью успеха.
В этом уроке я покажу вам, как создать простой лайтбокс с поддержкой жестов.
В процессе вы узнаете немного об улучшении воспринимаемой производительности сенсорных интерфейсов, а также о некоторых простых приемах повышения фактической производительности.
Написание кода для сенсорных устройств существенно отличается от написания кода для настольных компьютеров.
Вы можете (и должны) объединить как можно больше кода рабочего стола, но между ними всегда будут существенные различия.
Сравнительные тесты показывают, что производительность наиболее распространенных сенсорных устройств сопоставима с настольными компьютерами 1998 года.
Обычно они имеют около 256 МБ оперативной памяти, а производительность процессора находится на уровне оригинального iMac. Методы, которые мы используем для «просто работы» на настольных компьютерах, не будут корректно работать на мобильных телефонах и планшетах.
К счастью для нас, эти устройства обычно хорошо оптимизированы для графики, особенно для перемещения элементов на экране.
Устройства iOS и Android 3.0+ имеют аппаратное ускорение графики.
На самом деле, вы можете думать об этих устройствах как о паршивых компьютерах с приличными видеокартами.
Мы одинаково взаимодействуем с нашими компьютерами на протяжении последних 20 лет. Двигаем указатель мыши и нажимаем на кнопки управления.
Кнопки, блоки закрытия, ссылки и полосы прокрутки — вторая натура пользователей и разработчиков.
Сенсорные интерфейсы представляют собой совершенно другой набор соглашений.
Одним из самых распространенных является «свайп».
При «скольжении» несколько элементов отображаются так, как если бы они были расположены в ряд, и пользователь может использовать жест «скольжения» для перемещения между ними.
Скольжение — настолько распространенный шаблон, что нам даже не нужно сообщать об этом пользователям: когда пользователь видит что-то похожее на список, он инстинктивно пытается его прокрутить.
Часто мы не можем заставить наш код работать быстрее, особенно когда мы имеем дело с медленными соединениями и медленными устройствами.
Но мы можем сделать интерфейс более быстрым, сосредоточив внимание на оптимизации восприятия.
Мой любимый пример оптимизации воспринимаемой производительности — TiVo. Тринадцать лет назад, когда вышел первый TiVo, он был невероятно медленным (16 МБ ОЗУ и процессор 54 МГц!).
После того, как вы нажали что-то на пульте дистанционного управления, что-либо могло произойти, может пройти мучительно много времени, особенно если вы начали что-то воспроизводить или записывать.
Однако никто никогда не жаловался, что TiVo медленный.
Я думаю, это из-за звука.
Самая знакомая часть интерфейса TiVo — это мелодия, играющая при нажатии любой кнопки.
Этот звук заиграл мгновенно.
Инженеры TiVo позаботились о том, чтобы звук загружался как можно быстрее, чтобы, что бы ни случилось дальше, пользователь знал, что интерфейс не умер.
Эта короткая мелодия сообщала пользователям, что их просьба услышана.
Мы разработали онлайн-конвенцию, которая делает то же самое: счетчик.
После нажатия сразу появляется спиннер и таким образом пользователь получает сообщение о том, что его услышали.
В мобильных телефонах нам приходится действовать по-другому.
Жесты — это не отдельные действия, такие как щелчки.
Однако, чтобы интерфейс казался быстрым, нам нужно дать пользователям обратную связь.
Когда они жестикулируют, мы каким-то образом перемещаем интерфейс, чтобы они знали, что мы их «слышим».
Инструменты
Адаптивные интерфейсы требуют, чтобы элементы перемещались как можно быстрее.Движением мы показываем пользователю, что интерфейс отвечает на его запрос.
Использование анимации JavaScript для этого слишком медленное.
Вместо этого мы используем преобразования и переходы CSS: преобразования для повышения производительности и переходы, позволяющие анимации запускаться без блокировки выполнения JavaScript. В этом уроке я буду использовать CSS-преобразования и переходы для всех движений и анимации.
Еще одна оптимизация, которую я хочу использовать как можно чаще, — это то, что я называю «DOM только для записи».
Чтение свойств и значений из DOM сложное и обычно ненужное.
Что касается лайтбокса, я постараюсь объединить все чтения на этапе инициализации.
После этого я буду поддерживать состояние в JavaScript и при необходимости выполнять простые арифметические действия.
Создание лайтбокса
Для этого урока мы создадим страницу с несколькими миниатюрами.Нажатие (или нажатие) на миниатюры запустит лайтбокс.
Попав в лайтбокс, пользователь может пролистнуть изображения пальцем, а затем коснуться изображения, чтобы выйти из лайтбокса.
Создавая интерфейс жестов, помните о важности воспринимаемой производительности.
В лайтбоксе это означает, что слайды должны перемещаться вместе с пальцем по экрану.
Когда пользователь перестанет жестикулировать, слайды должны переместиться в следующую позицию или вернуться в предыдущую, если длины слайда оказалось недостаточно.
Анимация возврата имеет решающее значение.
Благодаря этому пользователь никогда не подумает, что интерфейс мертв.
Начинать
Создадим следующие файлы:lightbox/ reset.css slides.css slides.html slides.js
Образец
HTML будет простым.Это не только для демонстрационных целей.
Сложное дерево DOM по определению медленнее.
Стилизация, извлечение элементов DOM и визуальные эффекты становятся все более сложными с более сложным деревом DOM. Поскольку мы нацелены на «паршивые компьютеры», каждый бит имеет значение, поэтому важно с самого начала сохранять простоту.
Я использую reset.css от Эрика Мейера, чтобы начать сброс CSS. Я также настраиваю область просмотра, чтобы она не масштабировалась.
Я отключил встроенную функцию «нажмите, чтобы увеличить», чтобы она не мешала жестам.
Правильный ответ на клик будет реализован в JavaScript. «Нажмите, чтобы увеличить» заслуживает отдельного урока, поэтому мы его пока пропустим.
Что касается JS, я использую zepto.js, очень легкий фреймворк с синтаксисом jQuery. На самом деле нет необходимости в какой-либо платформе, но она немного ускорит некоторые задачи.
Для фактического взаимодействия с помощью жестов мы будем использовать встроенные API. <div class="main">
<div class="welcome">
<h1>Welcome to an amazing carousel!</h1>
<p>This is an example of a nice touch interface</p>
</div>
<div class="carousel">
<ul>
<li>
<a href=" http://www.flickr.com/photos/protohiro/6664939239/in/photostream/ ">
<img data-full-height="427" data-full-width="640" src=" http://farm8.staticflickr.com/7142/6664939239_7a6c846ec9_s.jpg ">
</a>
</li>
<li>
<a href=" http://www.flickr.com/photos/protohiro/6664957519/in/photostream ">
<img data-full-height="424" data-full-width="640" src=" http://farm8.staticflickr.com/7001/6664957519_582f716e38_s.jpg ">
</a>
</li>
<li>
<a href=" http://www.flickr.com/photos/protohiro/6664955215/in/photostream ">
<img data-full-height="640" data-full-width="427" src=" http://farm8.staticflickr.com/7019/6664955215_d49f2a0b18_s.jpg ">
</a>
</li>
<li>
<a href=" http://www.flickr.com/photos/protohiro/6664952047/in/photostream ">
<img data-full-height="426" data-full-width="640" src=" http://farm8.staticflickr.com/7017/6664952047_6955870ecb_s.jpg ">
</a>
</li>
<li>
<a href=" http://www.flickr.com/photos/protohiro/6664948305/in/photostream ">
<img data-full-height="428" data-full-width="640" src=" http://farm8.staticflickr.com/7149/6664948305_fb5a6276e5_s.jpg ">
</a>
</li>
</ul>
</div>
</div>
</body>
<script src="zepto.min.js" type="text/javascript" charset="utf-8"></script>
<script src="slides.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">
//this code initializes the lightbox and shows it when the user
//clicks on a slide
$(document).
ready(function(){ var lightbox = new saw.Lightbox('.
carousel'); $(document).
on('click', 'a', function(e){
e.preventDefault();
lightbox.show(this.href);
});
});
</script>
</html>
Стилизация миниатюр
Теперь давайте добавим несколько симпатичных миниатюр и другие визуальные эффекты: html {
background: #f1eee4;
font-family: georgia;
color: #7d7f94;
}
h1 {
color: #ba4a00;
}
.
welcome {
text-align: center;
text-shadow: 1px 1px 1px #fff;
}
.
welcome h1 {
font-size: 20px;
font-weight: bold;
}
.
welcome {
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
-moz-box-sizing: border-box; /* Firefox, other Gecko */
box-sizing: border-box; /* Opera/IE 8+ */
margin:5px;
padding:10px;
box-shadow: 2px 2px 5px rgba(0,0,0,0.5);
border-radius: 5px;
}
.
carousel {
margin:5px;
}
.
carousel ul li {
height: 70px;
width: 70px;
margin: 5px;
overflow: hidden;
display: block;
float: left;
border-radius: 5px;
box-shadow: 1px 1px 2px rgba(0,0,0,0.5), -1px -1px 2px rgba(255,255,255,1);
}
Основной лайтбокс
JavaScript лайтбокса должен делать несколько разных вещей:- Соберите данные лайтбокса и инициализируйте
- Скрыть и показать лайтбокс
- Создайте HTML для скина лайтбокса.
- Создание слайдов
- Обработка событий для жестов
Сервисные функции
Вместо того, чтобы снова и снова вводить «-webkit-transform» и «translate3d», я создам несколько служебных функций, которые сделают всю работу за меня.
function prefixify(str) {
var ua = window.navigator.userAgent;
if(ua.indexOf('WebKit') !== -1) {
return '-webkit-' + str;
}
if(ua.indexOf('Opera') !== -1) {
return '-o-' + str;
}
if(ua.indexOf('Gecko') !== -1) {
return '-moz-' + str;
}
return str;
}
function setPosition(node, left) {
// node.css('left', left +'px');
node.css(prefixify('transform'), "translate3d("+left+"px, 0, 0)");
}
function addTransitions(node){
node.css(prefixify('transition'), prefixify('transform') + ' .
25s ease-in-out'); node[0].
addEventListener('webkitTransitionEnd', function(e){ window.setTimeout(function(){ $(e.target).
css('-webkit-transition', 'none');
}, 0)
})
}
function cleanTransitions(node){
node.css(prefixify('transition'), 'none');
}
Наш виджет лайтбокса будет инициализирован при загрузке страницы, чтобы ускорить процессы.
Инициализация заключается в поиске всех миниатюр на странице и создании модели данных.
Мы подождем, пока будет предоставлен лайтбокс, чтобы сгенерировать HTML и прикрепить обработчики событий.
Инициализация
Для лайтбокса я использую конструктор, который принимает селектор блоков в качестве единственного параметра.
//clean namespacing
window.saw = (function($){
//the lightbox constructor
function Lightbox (selector) {
var container_node = $(selector),
wrapper,
chromeBuilt,
currentSlide = 0,
slideData =[],
boundingBox = [0,0],
slideMap = {};
function init(){
//init function
}
return {
show: show,
hide: hide
};
}
return {
Lightbox:Lightbox
};
}($));
Функция «init» захватывает все теги «li», находит миниатюры и записывает информацию в массив «slideData».
В то же время я создаю объект «slideMap», который отображает «href» миниатюры в массиве «slideData».
Это позволяет мне быстро искать данные одним щелчком мыши, без необходимости перебирать все данные в массиве или украшать DOM дополнительной информацией.
function init(){
var slides = container_node.find('li');
slides.each(function(i, el){
var thisSlide = {}, thisImg = $(el).
find('img'); thisSlide.url = thisImg.attr('src'); thisSlide.height = thisImg.attr('data-full-height'); thisSlide.width = thisImg.attr('data-full-width'); thisSlide.link = $(el).
find('a').
attr('href');
//push the slide info into the slideData array while recording the array index in the slideMap object.
slideMap[thisSlide.link] = slideData.push(thisSlide) - 1;
});
}
Остальная часть инициализации происходит в методе «show».
//this is the function called from the inline script
function show(startSlide){
if(!chromeBuilt){
buildChrome();
attachEvents();
}
wrapper.show();
//keep track of the viewport size
boundingBox = [ window.innerWidth, window.innerHeight ];
goTo(slideMap[startSlide]);
}
Создание оболочки
Функция buildChrome создает HTML-оболочку для лайтбокса, а затем устанавливает семафор, чтобы оболочка не перестраивалась каждый раз, когда пользователь скрывает или показывает лайтбокс.
Для удобства использования я создал отдельную функцию шаблона для самого HTML: var wrapperTemplate = function(){
return '<div class="slidewrap">'+
'<div class="controls"><a class="prev" href="#">prev</a> | <a class="next" href="#">next</a></div>'+
'</div>';
}
function buildChrome(){
wrapper = $(wrapperTemplate()).
addClass('slidewrap'); $('body').
append(wrapper);
chromeBuilt = true;
}
Последний шаг в создании оболочки — добавление обработчика событий для ссылок «Следующая» и «Предыдущая»: function handleClicks(e){
e.preventDefault();
var targ = $(e.target);
if(targ.hasClass('next')) {
goTo(currentSlide + 1);
} else if(targ.hasClass('prev')){
goTo(currentSlide - 1);
} else {
hide();
}
}
function attachEvents(){
wrapper.on('click', handleClicks, false);
}
Теперь оболочка лайтбокса готова к работе с несколькими слайдами.
В моей функции «показать» я вызываю «goTo()», чтобы загрузить первый слайд. Эта функция показывает слайд, определенный параметрами, но также медленно создает слайды, когда они мне нужны.
(Важно: не указывайте функцию «goTo» строчными буквами, поскольку «goto» — зарезервированное слово в JavaScript).
Создание слайдов
Теперь слайд, на который я смотрю, находится в области просмотра, а предыдущий и следующий слайды — слева и справа от видимой области экрана соответственно.Когда пользователь касается кнопки «Далее», текущий слайд перемещается влево и заменяется следующим слайдом.
//for the slides, takes a "slide" object
function slideTemplate(slide){
return '<div class="slide"><span>'+slide.id+'</span><div style="background-image:url('+slide.url.replace(/_s|_q/, '_z')+')"></div></div>';
}
Я использую вместо этого, потому что (по крайней мере, на данный момент) мобильные браузеры обрабатывают гораздо медленнее, чем фоновое изображение.
При работе с мобильными устройствами обычно предпочтение отдается скорости.
Проблемы доступности можно легко решить с помощью " АРИЯ Роль ".
Сама по себе функция buildSlide более сложна.
Помимо перемещения данных через шаблон слайда, код должен помещать слайды в область просмотра.
Это простая задача — выяснить, насколько масштабировать изображение, если оно не помещается.
Мы можем позволить браузеру обрабатывать изменение размера.
function buildSlide (slideNum) {
var thisSlide, s, img, scaleFactor = 1, w, h;
if(!slideData[slideNum] || slideData[slideNum].
node){
return false;
}
var thisSlide = slideData[slideNum];
var s = $(slideTemplate(thisSlide));
var img = s.children('div');
//image is too big! scale it!
if(thisSlide.width > boundingBox[0] || thisSlide.height > boundingBox[1]){
if(thisSlide.width > thisSlide.height) {
scaleFactor = boundingBox[0]/thisSlide.width;
} else {
scaleFactor = boundingBox[1]/thisSlide.height;
}
w = Math.round(thisSlide.width * scaleFactor);
h = Math.round(thisSlide.height * scaleFactor);
img.css('height', h + 'px');
img.css('width', w + 'px');
}else{
img.css('height', thisSlide.height + 'px');
img.css('width', thisSlide.width + 'px');
}
thisSlide.node = s;
wrapper.append(s);
//put the new slide into the start poisition
setPosition(s, boundingBox[0]);
return s;
}
идти к
«goTo» перемещает запрошенный слайд и соседние слайды в область просмотра.
function goTo(slideNum){
var thisSlide;
//if the slide we are looking for doesn't exist, lets just go
//back to the current slide. This has the handy effect of providing
//"snap back" feedback when gesturing, the slide will just animate
//back into position
if(!slideData[slideNum]){
return;
}
thisSlide = slideData[slideNum];
//build adjacent slides
buildSlide(slideNum);
buildSlide(slideNum + 1);
buildSlide(slideNum - 1);
//make it fancy
addTransitions(thisSlide.node);
//put the current slide into position
setPosition(thisSlide.node, 0);
//slide the adjacent slides away
if(slideData[slideNum - 1] && slideData[slideNum-1].
node){ addTransitions(slideData[slideNum - 1 ].
node); setPosition( slideData[slideNum - 1 ].
node , (0 - boundingBox[0]) ); } if(slideData[slideNum + 1] && slideData[slideNum + 1].
node){ addTransitions(slideData[slideNum + 1 ].
node); setPosition(slideData[slideNum + 1 ].
node, boundingBox[0] );
}
//update the state
currentSlide = slideNum;
}
На данный момент лайтбокс более-менее функционален.
Мы можем перейти к следующему и предыдущему слайду, можем его скрыть и показать.
Было бы идеально знать, когда мы доходим до первого или последнего слайда: мы могли бы, например, отобразить элементы управления серым цветом.
Это касается как настольных компьютеров, так и сенсорных устройств.
Добавление поддержки жестов
Большинство сенсорных устройств имеют встроенные средства просмотра фотографий.Эти различные приложения, следуя оригинальному средству просмотра фотографий для iPhone, создали условный интерфейс для интерфейсов: смахивание пальца влево открывает следующий слайд. Я видел несколько реализаций этого взаимодействия, которые вообще не дают обратной связи — слайды просто заменяются после завершения жеста.
Правильный подход — давать живую обратную связь.
Когда пользователь проводит пальцем, слайды должны двигаться вместе с ним и, в зависимости от направления, должен появляться следующий или предыдущий слайд. Это создает иллюзию, что пользователь просматривает полосу фотографий.
Обработка событий касания
Многие библиотеки, включая Zepto, включают поддержку событий касания.В целом я не рекомендую их использовать.
Когда вы обрабатываете события касания, вы обновляете элементы вместе с жестами пользователя.
Когда задержка заметна для пользователя, интерфейс становится медленным.
Одна из основных причин, по которой мы использовали библиотеки событий, — это обеспечение нормализации в браузерах.
Все мобильные браузеры, поддерживающие сенсорные события, имеют одинаковые API. В этом примере мы рассмотрим три события касания: «touchstart», «touchmove» и «touchend».
Существует также событие «touchcancel», когда жест по какой-либо причине прерывается (например, push-сообщением).
При разработке необходимо правильно с ними обращаться.
function attachTouchEvents() {
var bd = document.querySelector('html');
bd.addEventListener('touchmove', handleTouchEvents);
bd.addEventListener('touchstart', handleTouchEvents);
bd.addEventListener('touchend', handleTouchEvents);
}
Обработчик событий получает объект TouchEvent. События touchstart и touchmove содержат свойство touches, которое является объектом массива Touch. Для скольжения пальца необходимо только одно свойство: «clientX».
Он содержит значение положения касания относительно верхнего левого угла страницы.
Устройства iOS поддерживают до одиннадцати одновременных касаний.
Android (до Ice Cream Sandwich) поддерживает только один.
Для большинства взаимодействий требуется всего одно касание.
Более сложные жесты заставляют вас беспокоиться о множественных касаниях.
Функция «handleTouchEvents»
Во-первых, давайте определим несколько переменных вне этой функции для поддержки состояний: var startPos, endPos, lastPos;
Следующая ветвь основана на свойстве типа объекта события:
function handleTouchEvents(e){
var direction = 0;
//you could also use a switch statement
if(e.type == 'touchstart') {
} else if(e.type == 'touchmove' ) {
} else if(e.type == 'touchend) {
}
Событие «touchstart» срабатывает в начале любого события касания, поэтому используйте его для записи места начала жеста, что пригодится позже.
Избавьтесь от любых переходов, которые все еще могут быть в тегах.
if(e.type == 'touchstart') {
//record the start clientX
startPos = e.touches[0].
clientX; //lastPos is startPos at the beginning lastPos = startPos; //we'll keep track of direction as a signed integer. // -1 is left, 1 is right and 0 is staying still direction = 0; //now we clean off the transtions if(slideData[currentSlide] && slideData[currentSlide].
node){ cleanTransitions(slideData[currentSlide].
node); } if(slideData[currentSlide + 1] && slideData[currentSlide + 1].
node){ cleanTransitions(slideData[currentSlide + 1].
node); } if(slideData[currentSlide - 1] && slideData[currentSlide -1].
node){ cleanTransitions(slideData[currentSlide -1].
node);
}
} else if(e.type == 'touchmove' ) {
В «touchmove» определите, как далеко продвинулось касание вдоль «clientX», а затем переместите текущий слайд на такое же расстояние.
Если слайд перемещается влево, переместите также следующий слайд; если вправо, соответственно переместите предыдущий слайд. Таким образом вы перемещаете только два блока, но создается иллюзия, что движется вся полоса.
}else if(e.type == 'touchmove'){
e.preventDefault();
//figure out the direction
if(lastPos > startPos){
direction = -1;
}else{
direction = 1;
}
//make sure the slide exists
if(slideData[currentSlide]){
//move the current slide into position
setPosition(slideData[currentSlide].
node, e.touches[0].
clientX - startPos); //make sure the next or previous slide exits if(direction !== 0 && slideData[currentSlide + direction]){ //move the next or previous slide. if(direction < 0){ //I want to move the next slide into the right position, which is the same as the //current slide, minus the width of the viewport (each slide is as wide as the viewport) setPosition(slideData[currentSlide + direction].
node, (e.touches[0].
clientX - startPos) - boundingBox[0]); }else if(direction > 0){ setPosition(slideData[currentSlide + direction].
node, (e.touches[0].
clientX - startPos) + boundingBox[0]);
}
}
}
Теги: #lightbox #lightbox #html5 #JavaScript #css3 #CSS #HTML