Мобильная революция: как работать с touch-событиями на сайтах
Для кого эта статья:
- веб-разработчики, стремящиеся улучшить свои навыки в разработке мобильных интерфейсов
- студенты курсов по веб-разработке, желающие освоить работу с touch-событиями
профессионалы в области UX/UI, нацеленные на создание качественных мобильных приложений
Мобильные устройства кардинально изменили подход к взаимодействию с интерфейсами, сделав касания и жесты основным способом управления. Разработчики, не освоившие touch-события, создают интерфейсы, раздражающие пользователей задержками, неточными откликами и непредсказуемым поведением. Но стоит научиться грамотно обрабатывать эти события — и ваше приложение приобретет ту плавность и отзывчивость, которую пользователи ожидают от премиальных продуктов. Давайте разберем, как превратить мобильный опыт из головной боли в конкурентное преимущество. 📱✨
Хотите не просто читать о touch-событиях, а научиться создавать профессиональные мобильно-оптимизированные веб-приложения? Программа Обучение веб-разработке от Skypro поможет вам освоить все тонкости работы с сенсорными интерфейсами и другие аспекты современной веб-разработки. Наши студенты создают проекты, которые превосходно работают на всех устройствах — от десктопа до смартфонов. Присоединяйтесь к тем, кто уже создаёт будущее мобильного веба!
Основы событий touch для мобильных устройств
Touch API открыл новую эру в веб-разработке, позволив создавать интерактивные интерфейсы, реагирующие на прикосновения пользователей. В отличие от традиционных событий мыши, touch-события специально разработаны для работы с сенсорными экранами и обеспечивают доступ к расширенной информации о взаимодействии.
Основные touch-события, которые необходимо знать разработчику:
- touchstart — срабатывает при касании пользователем экрана
- touchmove — срабатывает при движении пальца по экрану
- touchend — срабатывает при отрыве пальца от экрана
- touchcancel — срабатывает, когда касание прерывается системой
Каждое touch-событие содержит три основных списка касаний:
- touches — все текущие касания на экране
- targetTouches — касания, относящиеся к целевому элементу события
- changedTouches — касания, участвовавшие в текущем событии
Объект Touch содержит богатую информацию о каждом касании, что делает его гораздо более информативным, чем стандартные события мыши:
| Свойство | Описание | Применение |
|---|---|---|
| identifier | Уникальный идентификатор касания | Отслеживание конкретного пальца в мультитач-жестах |
| clientX/clientY | Координаты касания относительно окна браузера | Определение положения касания в интерфейсе |
| pageX/pageY | Координаты касания с учетом прокрутки страницы | Расчет позиции на длинных страницах |
| screenX/screenY | Координаты касания относительно экрана устройства | Взаимодействие с элементами на уровне устройства |
| radiusX/radiusY | Радиус эллипса касания | Определение площади касания для точности |
| force | Сила нажатия (от 0 до 1) | Создание интерфейсов с реакцией на силу нажатия |
Вот простой пример обработки touch-события:
document.addEventListener('touchstart', function(event) {
// Предотвращаем стандартное поведение браузера
event.preventDefault();
// Получаем первое касание
const touch = event.touches[0];
// Выводим координаты касания
console.log(`Touch started at X: ${touch.clientX}, Y: ${touch.clientY}`);
}, false);
Алексей Вершинин, технический директор мобильной разработки
Когда мы разрабатывали приложение для интерактивной музейной выставки, столкнулись с неожиданной проблемой. На экране 55 дюймов пользователи часто случайно касались поверхности ладонью или несколькими пальцами одновременно, что полностью путало нашу логику обработки событий. Решение пришло, когда мы детально изучили свойство touchType и научились фильтровать нежелательные касания.
Разработали систему, которая анализировала размер области касания через radiusX/radiusY и отбрасывала слишком большие контакты как случайные. Также использовали разные обработчики для одиночных и множественных касаний. После этих изменений количество ошибок взаимодействия снизилось на 87%, а время, которое посетители проводили с экспонатом, увеличилось вдвое.

Работа с touchstart, touchmove и touchend в JavaScript
Правильная обработка триады основных touch-событий — touchstart, touchmove и touchend — формирует основу любого качественного мобильного интерфейса. Рассмотрим, как эффективно работать с каждым из этих событий в JavaScript.
Создание отзывчивого мобильного интерфейса начинается с глубокого понимания жизненного цикла касания. Каждое взаимодействие пользователя с сенсорным экраном проходит через последовательность событий, которые можно эффективно обрабатывать.
Для начала работы с touch-событиями, необходимо зарегистрировать соответствующие обработчики:
const element = document.getElementById('interactive-area');
element.addEventListener('touchstart', handleTouchStart);
element.addEventListener('touchmove', handleTouchMove);
element.addEventListener('touchend', handleTouchEnd);
Рассмотрим детально, как работать с каждым из этих событий:
1. Обработка touchstart
Событие touchstart идеально подходит для инициализации взаимодействия и сохранения начальных данных:
function handleTouchStart(event) {
// Предотвращаем прокрутку страницы
event.preventDefault();
// Сохраняем начальную позицию
const touch = event.touches[0];
startX = touch.clientX;
startY = touch.clientY;
// Отмечаем начало взаимодействия
isInteracting = true;
// Фиксируем время начала касания для определения жестов
touchStartTime = Date.now();
// Можно добавить визуальную обратную связь
this.classList.add('active');
}
2. Обработка touchmove
touchmove дает возможность реагировать на движение пальца и обновлять интерфейс в реальном времени:
function handleTouchMove(event) {
// Продолжаем только если взаимодействие активно
if (!isInteracting) return;
// Предотвращаем прокрутку страницы
event.preventDefault();
const touch = event.touches[0];
const currentX = touch.clientX;
const currentY = touch.clientY;
// Вычисляем дистанцию перемещения
const deltaX = currentX – startX;
const deltaY = currentY – startY;
// Применяем трансформацию или обновляем интерфейс
this.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
// Можно ограничить перемещение определенной областью
if (Math.abs(deltaX) > maxDistance || Math.abs(deltaY) > maxDistance) {
// Логика при достижении границ
}
}
3. Обработка touchend
touchend позволяет завершить взаимодействие и выполнить финальные действия:
function handleTouchEnd(event) {
if (!isInteracting) return;
// Определяем итоговое перемещение
const touch = event.changedTouches[0]; // Используем changedTouches!
const finalX = touch.clientX;
const finalY = touch.clientY;
// Вычисляем финальное смещение
const deltaX = finalX – startX;
const deltaY = finalY – startY;
// Вычисляем скорость жеста (для определения "броска")
const touchTime = Date.now() – touchStartTime;
const speed = Math.sqrt(deltaX * deltaX + deltaY * deltaY) / touchTime;
// Определяем тип жеста на основе данных
if (Math.abs(deltaX) > 50 && speed > 0.5) {
// Горизонтальный свайп
if (deltaX > 0) {
// Свайп вправо
handleRightSwipe();
} else {
// Свайп влево
handleLeftSwipe();
}
} else {
// Возвращаем элемент в исходное положение
this.style.transform = 'translate(0, 0)';
}
// Сбрасываем состояние взаимодействия
isInteracting = false;
this.classList.remove('active');
}
Важные моменты при работе с touch-событиями:
- В touchend используйте changedTouches вместо touches, так как список touches может быть уже пуст
- Всегда учитывайте возможность мультитач-взаимодействия
- При обработке позиции учитывайте масштаб страницы (window.devicePixelRatio)
- Внимательно подходите к вызову preventDefault() — он может заблокировать нативную прокрутку страницы
| Событие | Когда использовать | Типичные задачи | Особенности |
|---|---|---|---|
| touchstart | Начало взаимодействия | Инициализация, захват исходных данных | Срабатывает один раз в начале касания |
| touchmove | Отслеживание движения | Перетаскивание, зум, вращение | Может вызываться десятки раз в секунду |
| touchend | Завершение взаимодействия | Финализация действия, анимация, валидация жеста | Используйте changedTouches вместо touches |
| touchcancel | Обработка прерываний | Очистка состояния, сброс переменных | Срабатывает при системных прерываниях |
Жесты мультитач: распознавание и обработка свайпов
Мультитач-взаимодействие — одна из важнейших возможностей мобильных устройств, позволяющая создавать интуитивные и мощные интерфейсы. Реализация сложных жестов требует понимания основных паттернов и техник их обнаружения.
Начнем с самого распространенного жеста — свайпа. Свайп определяется как быстрое движение пальца в определенном направлении. Вот простая и эффективная реализация детектора свайпов:
class SwipeDetector {
constructor(element, options = {}) {
this.element = element;
this.options = {
threshold: options.threshold || 50, // Минимальное расстояние для свайпа
restraint: options.restraint || 100, // Максимальное отклонение в перпендикулярном направлении
allowedTime: options.allowedTime || 300, // Максимальное время свайпа в мс
onSwipeLeft: options.onSwipeLeft || function() {},
onSwipeRight: options.onSwipeRight || function() {},
onSwipeUp: options.onSwipeUp || function() {},
onSwipeDown: options.onSwipeDown || function() {}
};
this.startX = 0;
this.startY = 0;
this.startTime = 0;
// Привязываем методы к контексту
this.handleTouchStart = this.handleTouchStart.bind(this);
this.handleTouchEnd = this.handleTouchEnd.bind(this);
// Регистрируем обработчики
this.element.addEventListener('touchstart', this.handleTouchStart, false);
this.element.addEventListener('touchend', this.handleTouchEnd, false);
}
handleTouchStart(event) {
const touch = event.touches[0];
this.startX = touch.clientX;
this.startY = touch.clientY;
this.startTime = Date.now();
}
handleTouchEnd(event) {
const touch = event.changedTouches[0];
const elapsedTime = Date.now() – this.startTime;
if (elapsedTime <= this.options.allowedTime) {
const distX = touch.clientX – this.startX;
const distY = touch.clientY – this.startY;
if (Math.abs(distX) >= this.options.threshold &&
Math.abs(distY) <= this.options.restraint) {
// Горизонтальный свайп
distX < 0 ? this.options.onSwipeLeft() : this.options.onSwipeRight();
} else if (Math.abs(distY) >= this.options.threshold &&
Math.abs(distX) <= this.options.restraint) {
// Вертикальный свайп
distY < 0 ? this.options.onSwipeUp() : this.options.onSwipeDown();
}
}
}
destroy() {
// Очистка обработчиков при необходимости
this.element.removeEventListener('touchstart', this.handleTouchStart);
this.element.removeEventListener('touchend', this.handleTouchEnd);
}
}
Использование этого класса предельно просто:
const carousel = document.querySelector('.carousel');
const swipeDetector = new SwipeDetector(carousel, {
onSwipeLeft: () => carousel.goToNextSlide(),
onSwipeRight: () => carousel.goToPrevSlide()
});
Для более сложных мультитач-жестов, таких как масштабирование (pinch-zoom) и вращение, требуется отслеживать несколько точек касания одновременно:
class PinchDetector {
constructor(element, options = {}) {
this.element = element;
this.options = {
onPinchIn: options.onPinchIn || function() {},
onPinchOut: options.onPinchOut || function() {},
onRotate: options.onRotate || function() {}
};
this.initialDistance = 0;
this.initialAngle = 0;
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this));
this.element.addEventListener('touchmove', this.handleTouchMove.bind(this));
}
handleTouchStart(event) {
if (event.touches.length === 2) {
// Сохраняем начальное расстояние между двумя пальцами
this.initialDistance = this.getDistance(
event.touches[0].clientX,
event.touches[0].clientY,
event.touches[1].clientX,
event.touches[1].clientY
);
// Сохраняем начальный угол
this.initialAngle = this.getAngle(
event.touches[0].clientX,
event.touches[0].clientY,
event.touches[1].clientX,
event.touches[1].clientY
);
}
}
handleTouchMove(event) {
if (event.touches.length === 2) {
event.preventDefault(); // Предотвращаем прокрутку при мультитач
// Вычисляем текущее расстояние
const currentDistance = this.getDistance(
event.touches[0].clientX,
event.touches[0].clientY,
event.touches[1].clientX,
event.touches[1].clientY
);
// Вычисляем текущий угол
const currentAngle = this.getAngle(
event.touches[0].clientX,
event.touches[0].clientY,
event.touches[1].clientX,
event.touches[1].clientY
);
// Вычисляем масштаб изменения
const scale = currentDistance / this.initialDistance;
// Вычисляем угол поворота
const rotation = currentAngle – this.initialAngle;
// Определяем тип жеста и вызываем соответствующий обработчик
if (scale > 1.1) {
this.options.onPinchOut(scale);
} else if (scale < 0.9) {
this.options.onPinchIn(scale);
}
if (Math.abs(rotation) > 10) {
this.options.onRotate(rotation);
}
}
}
getDistance(x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x2 – x1, 2) + Math.pow(y2 – y1, 2));
}
getAngle(x1, y1, x2, y2) {
return Math.atan2(y2 – y1, x2 – x1) * 180 / Math.PI;
}
}
Основные типы мультитач-жестов, которые стоит поддерживать в мобильных приложениях:
- Свайп (Swipe) — быстрое движение в одном направлении, часто используется для навигации
- Масштабирование (Pinch) — сведение или разведение двух пальцев для изменения масштаба
- Вращение (Rotate) — круговое движение двух пальцев для поворота объекта
- Длительное нажатие (Long Press) — удержание пальца на экране для вызова контекстного меню
- Двойное касание (Double Tap) — два быстрых последовательных касания для масштабирования или активации
Михаил Овчинников, UX-директор
Помню, мы разрабатывали приложение для редактирования фотографий, и главной головной болью стала реализация плавного масштабирования и вращения изображений. Первая версия работала ужасно: изображение дёргалось, запаздывало за движением пальцев, а иногда и вовсе "улетало" за пределы экрана.
После множества итераций мы обнаружили, что проблема была в частоте обновления — мы пытались обрабатывать каждое событие touchmove, что создавало огромную нагрузку на основной поток JavaScript. Решением стало использование requestAnimationFrame вместо непосредственного изменения DOM в обработчиках событий. Мы собирали данные о касаниях, но применяли трансформации только в момент следующего кадра анимации.
Разница была поразительной — теперь приложение работало с плавностью 60 FPS даже на бюджетных смартфонах. Пользователи отметили это в отзывах, а время, проводимое в приложении, увеличилось на 28%. Иногда оптимизация важнее самого функционала.
Решение проблем с задержками и оптимизация отклика
Одна из наиболее раздражающих проблем мобильных веб-приложений — задержка между касанием и реакцией интерфейса. По умолчанию многие браузеры вводят задержку около 300 мс после tap-события, чтобы определить, является ли это касание частью двойного нажатия. Эта задержка создает ощущение "залипания" и существенно ухудшает пользовательский опыт. 🐢
Рассмотрим основные проблемы и их решения:
1. Устранение 300 мс задержки
Существует несколько способов устранения этой задержки:
- Использование метатега viewport с width=device-width:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- Применение CSS-правила touch-action: manipulation:
.button { touch-action: manipulation; }
- Использование библиотеки FastClick или собственной реализации быстрого касания
Вот простая реализация обработчика быстрого касания:
class FastTap {
constructor(element, callback) {
this.element = element;
this.callback = callback;
this.startX = 0;
this.startY = 0;
this.threshold = 10; // Допустимое смещение для распознавания нажатия
this.isMoving = false;
this.element.addEventListener('touchstart', this.onTouchStart.bind(this));
this.element.addEventListener('touchmove', this.onTouchMove.bind(this));
this.element.addEventListener('touchend', this.onTouchEnd.bind(this));
}
onTouchStart(event) {
this.isMoving = false;
const touch = event.touches[0];
this.startX = touch.clientX;
this.startY = touch.clientY;
}
onTouchMove(event) {
const touch = event.touches[0];
const diffX = Math.abs(touch.clientX – this.startX);
const diffY = Math.abs(touch.clientY – this.startY);
// Если пользователь переместил палец больше порогового значения, считаем это движением, а не нажатием
if (diffX > this.threshold || diffY > this.threshold) {
this.isMoving = true;
}
}
onTouchEnd(event) {
// Вызываем callback только если это было касание, а не движение
if (!this.isMoving) {
event.preventDefault(); // Предотвращаем стандартный клик
this.callback(event);
}
}
}
// Использование:
const button = document.querySelector('.fast-button');
new FastTap(button, () => console.log('Быстрое нажатие!'));
2. Оптимизация производительности анимаций
Медленные или дёргающиеся анимации — верный способ испортить впечатление от приложения. Вот ключевые оптимизации:
- Используйте CSS-свойства, которые задействуют GPU: transform и opacity вместо left/top и visibility
- Применяйте will-change для предупреждения браузера о будущих изменениях
- Используйте requestAnimationFrame вместо setTimeout для плавных анимаций
- Минимизируйте reflow и repainting, группируя изменения DOM
Пример оптимизации перемещения элемента:
// Неоптимально: вызывает reflow на каждой итерации
function moveElementSlow(element, x, y) {
element.style.left = x + 'px';
element.style.top = y + 'px';
}
// Оптимально: использует GPU-ускорение
function moveElementFast(element, x, y) {
element.style.transform = `translate3d(${x}px, ${y}px, 0)`;
}
3. Дебаунсинг и троттлинг для touch-событий
События touchmove могут генерироваться сотни раз в секунду, что создает излишнюю нагрузку на JavaScript-поток. Используйте техники дебаунсинга и троттлинга для контроля частоты обработки событий:
// Функция троттлинга – ограничивает частоту вызовов
function throttle(func, limit) {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() – lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now();
}
}, limit – (Date.now() – lastRan));
}
}
}
// Пример использования
element.addEventListener('touchmove', throttle(function(event) {
// Обработка события touchmove
updateElementPosition(event);
}, 16)); // Ограничиваем до ~60 FPS (1000ms / 60 ≈ 16ms)
Производительность touch-событий можно оценить по следующим ключевым метрикам:
| Метрика | Целевое значение | Влияние на UX | Способ улучшения |
|---|---|---|---|
| Задержка отклика | < 100 мс | Критически важное | Устранение 300мс задержки, использование passive: true |
| Частота кадров | 60 FPS (16.7 мс/кадр) | Высокое | requestAnimationFrame, CSS-анимации вместо JS |
| Time to Interactive | < 3.8 секунды | Среднее | Оптимизация загрузки JavaScript, разделение кода |
| Время обработки событий | < 50 мс | Высокое | Оптимизация обработчиков, троттлинг, использование веб-воркеров |
4. Использование passive listeners
Passive listeners — мощный инструмент для оптимизации прокрутки на мобильных устройствах. Он сообщает браузеру, что обработчик событий не будет вызывать preventDefault(), что позволяет браузеру немедленно начать прокрутку:
// Старый способ, который может блокировать прокрутку
document.addEventListener('touchstart', function(e) {
// Какой-то код...
});
// Оптимизированный способ с passive listener
document.addEventListener('touchstart', function(e) {
// Какой-то код...
}, { passive: true }); // Браузер будет знать, что preventDefault не вызывается
Этот простой флаг может значительно повысить плавность прокрутки, особенно на устройствах с низкой производительностью. 🚀
Создание адаптивных интерфейсов с поддержкой touch
Создание по-настоящему адаптивного интерфейса требует не только корректного отображения на разных экранах, но и тщательно продуманной поддержки различных типов ввода — от мыши и клавиатуры до сенсорных экранов. Рассмотрим ключевые принципы разработки таких интерфейсов. 🖥️ ↔️ 📱
1. Определение возможностей устройства
Перед реализацией интерфейса необходимо определить, поддерживает ли устройство touch-ввод:
const isTouchDevice = () => {
return (('ontouchstart' in window) ||
(navigator.maxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0));
};
const touchSupport = isTouchDevice();
// Применяем соответствующие стили и поведение
document.body.classList.add(touchSupport ? 'touch-device' : 'no-touch');
Однако современные устройства часто поддерживают несколько типов ввода одновременно (например, ноутбуки с сенсорным экраном). Поэтому лучше использовать обнаружение по фактическим событиям:
let lastTouchTime = 0;
// Устанавливаем обработчик касаний
window.addEventListener('touchstart', function() {
lastTouchTime = new Date();
document.body.classList.add('touch-device');
});
// Проверяем, было ли недавнее касание при движении мыши
window.addEventListener('mousemove', function() {
const now = new Date();
if (now – lastTouchTime > 500) {
// Если последнее касание было давно, считаем, что пользователь использует мышь
document.body.classList.remove('touch-device');
document.body.classList.add('mouse-device');
}
});
2. Проектирование с учетом размера целей касания
Элементы интерфейса должны быть достаточного размера для комфортного взаимодействия пальцем:
- Минимальный размер целей касания: 44×44 пикселя (рекомендация Apple)
- Оптимальный размер для большинства элементов: 48×48 пикселей (рекомендация Google Material Design)
- Расстояние между интерактивными элементами: минимум 8 пикселей
Используйте медиа-запросы для адаптации размеров элементов на разных устройствах:
.button {
padding: 12px 20px; /* Базовый размер для десктопа */
font-size: 16px;
}
/* Для устройств с touch-экраном увеличиваем целевую область */
@media (pointer: coarse) {
.button {
padding: 16px 24px;
font-size: 18px;
}
}
3. Реализация гибридного интерфейса
Современный адаптивный интерфейс должен корректно работать с любым типом ввода, автоматически адаптируясь к текущему способу взаимодействия. Вот пример класса для создания интерактивного элемента с поддержкой как мыши, так и касаний:
class HybridInteraction {
constructor(element) {
this.element = element;
this.isTouching = false;
// Регистрируем обработчики мыши
this.element.addEventListener('mouseenter', this.onMouseEnter.bind(this));
this.element.addEventListener('mouseleave', this.onMouseLeave.bind(this));
this.element.addEventListener('click', this.onClick.bind(this));
// Регистрируем обработчики касаний
this.element.addEventListener('touchstart', this.onTouchStart.bind(this));
this.element.addEventListener('touchend', this.onTouchEnd.bind(this));
this.element.addEventListener('touchcancel', this.onTouchCancel.bind(this));
}
// Обработчики мыши
onMouseEnter() {
if (!this.isTouching) {
this.element.classList.add('hover');
}
}
onMouseLeave() {
this.element.classList.remove('hover');
}
onClick(event) {
// Обработка клика общая для мыши и касания
this.activateElement();
}
// Обработчики касаний
onTouchStart(event) {
this.isTouching = true;
this.element.classList.add('active');
// Запоминаем начальную позицию для определения свайпа
if (event.touches.length === 1) {
const touch = event.touches[0];
this.touchStartX = touch.clientX;
this.touchStartY = touch.clientY;
}
}
onTouchEnd(event) {
this.isTouching = false;
this.element.classList.remove('active');
// Проверяем, был ли это свайп или нажатие
if (event.changedTouches.length === 1) {
const touch = event.changedTouches[0];
const deltaX = touch.clientX – this.touchStartX;
const deltaY = touch.clientY – this.touchStartY;
if (Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10) {
// Это было нажатие, а не свайп
this.activateElement();
}
}
}
onTouchCancel() {
this.isTouching = false;
this.element.classList.remove('active');
}
// Общая функция активации элемента
activateElement() {
this.element.classList.add('activated');
// Выполняем нужное действие
console.log('Элемент активирован!');
// Удаляем класс активации через небольшую задержку
setTimeout(() => {
this.element.classList.remove('activated');
}, 200);
}
}
// Применяем к элементам интерфейса
document.querySelectorAll('.interactive-element').forEach(element => {
new HybridInteraction(element);
});
4. Применение паттернов для сенсорного интерфейса
Для создания действительно удобного сенсорного интерфейса используйте проверенные паттерны:
- Нижнее меню вместо верхнего — для доступа большим пальцем на больших экранах
- Свайп для действий — архивирование, удаление, отметка выполнено
- Pull-to-refresh — обновление контента свайпом сверху вниз
- Визуальная обратная связь — анимации нажатия, изменения состояния
- Floating Action Button — быстрый доступ к основным действиям
Вот пример реализации pull-to-refresh с использованием touch-событий:
class PullToRefresh {
constructor(element, options = {}) {
this.element = element;
this.options = {
threshold: options.threshold || 60, // Порог для активации обновления
callback: options.callback || (() => console.log('Обновление!')),
};
this.startY = 0;
this.currentY = 0;
this.isRefreshing = false;
this.indicator = this.createIndicator();
this.element.addEventListener('touchstart', this.onTouchStart.bind(this));
this.element.addEventListener('touchmove', this.onTouchMove.bind(this));
this.element.addEventListener('touchend', this.onTouchEnd.bind(this));
}
createIndicator() {
const indicator = document.createElement('div');
indicator.className = 'pull-indicator';
indicator.innerHTML = '⟳';
indicator.style.position = 'absolute';
indicator.style.top = '-50px';
indicator.style.left = '50%';
indicator.style.transform = 'translateX(-50%)';
indicator.style.transition = 'transform 0.2s';
this.element.prepend(indicator);
return indicator;
}
onTouchStart(event) {
if (this.isRefreshing) return;
// Проверяем, находимся ли мы в верхней части элемента
const scrollTop = this.element.scrollTop;
if (scrollTop <= 0) {
this.startY = event.touches[0].clientY;
this.element.style.transition = 'transform 0.1s';
}
}
onTouchMove(event) {
if (this.isRefreshing) return;
const scrollTop = this.element.scrollTop;
if (scrollTop > 0) return; // Не активируем, если страница прокручена вниз
this.currentY = event.touches[0].clientY;
const pull = this.currentY – this.startY;
if (pull > 0) {
// Предотвращаем стандартное поведение, только если тянем вниз
event.preventDefault();
// Применяем нелинейное сопротивление, чтобы ограничить расстояние
const resistance = 0.4;
const translateY = Math.pow(pull, resistance);
this.element.style.transform = `translateY(${translateY}px)`;
// Обновляем индикатор
const progress = Math.min(translateY / this.options.threshold, 1);
this.indicator.style.transform = `translateX(-50%) rotate(${progress * 360}deg)`;
}
}
onTouchEnd() {
if (this.isRefreshing) return;
const pull = this.currentY – this.startY;
if (pull > this.options.threshold) {
// Активируем обновление
this.isRefreshing = true;
this.element.style.transform = `translateY(50px)`;
this.indicator.classList.add('refreshing');
this.indicator.style.animation = 'spin 1s infinite linear';
// Вызываем функцию обновления
this.options.callback().then(() => {
// После завершения обновления
this.reset();
}).catch(() => {
// В случае ошибки также сбрасываем состояние
this.reset();
});
} else {
// Возвращаем в исходное положение
this.reset();
}
}
reset() {
this.element.style.transition = 'transform 0.3s';
this.element.style.transform = '';
this.indicator.style.animation = '';
this.indicator.classList.remove('refreshing');
setTimeout(() => {
this.isRefreshing = false;
}, 300);
}
}
// Пример использования:
const contentElement = document.querySelector('.content-container');
new PullToRefresh(contentElement, {
callback: () => new Promise(resolve => {
// Имитация запроса к серверу
setTimeout(resolve, 1500);
})
});
Ключевое правило разработки адаптивных интерфейсов: проектируйте сначала для мобильных устройств (mobile-first design), а затем расширяйте функциональность для десктопа. Это гарантирует, что основные функции будут работать на любом устройстве. 📱→🖥️
Грамотная работа с touch-событиями открывает новый уровень взаимодействия пользователя с вашими мобильными приложениями. Освоив принципы, описанные в этой статье, вы сможете создавать интерфейсы, которые не просто работают на мобильных устройствах, а используют их уникальные возможности для создания лучшего пользовательского опыта. Не забывайте, что производительность и отзывчивость интерфейса часто важнее богатого функционала — ведь лучший код тот, который пользователь не замечает, потому что всё просто работает так, как должно. 💪