Реализация интерактивного масштабирования изображений (pinch/pan) на мобильных устройствах
- Показывать справа: 0
В современной веб-разработке, особенно при создании адаптивных интерфейсов для мобильных устройств, возможность детально рассмотреть изображения — важная часть пользовательского опыта. Этот урок покажет, как реализовать интуитивно понятное pinch zoom (увеличение двумя пальцами) и pan (перемещение) изображений, создавая эффект, подобный работе с галереями в нативных приложениях.
Зачем нужны pinch zoom и pan?
На мобильных устройствах, где экраны меньше, чем на настольных компьютерах, пользователям часто требуется рассмотреть детали изображений. Функция pinch zoom позволяет масштабировать картинку, а pan — перемещать ее в увеличенном состоянии. Реализация этих жестов значительно повышает удобство использования вашего сайта или веб-приложения.
- Улучшенный UX: Пользователи могут свободно изучать содержимое, не теряя деталей.
- Привычный интерфейс: Эффект "как в приложении" делает взаимодействие более естественным.
- Интерактивность: Добавляет динамичности и современности вашему проекту.
Необходимые инструменты
Для реализации этой функциональности мы будем использовать:
- JavaScript: Основной язык программирования для логики.
- Hammer.js: Мощная JavaScript-библиотека для работы с сенсорными жестами (touch events), которая значительно упрощает реализацию pinch-to-zoom и drag-and-drop двумя пальцами.
- HTML: Для структуры и размещения элементов.
Подключение Hammer.js
Прежде всего, подключите библиотеку Hammer.js. Это можно сделать, добавив следующую строку в секцию <head> или перед закрывающим тегом </body> вашего HTML-файла:
<script src="https://cdnjs.cloudflare.com/ajax/libs/hammer.js/2.0.8/hammer.min.js"></script>
Основной JavaScript-скрипт для pinch/pan
Вот код, который реализует функциональность масштабирования и перемещения. Мы разберем его по частям.
// Объект для хранения экземпляров Hammer.js, связанных с уникальными ID элементов.
// Это позволяет нам управлять ими (например, удалять).
const hammerInstances = {};
// Объект для хранения самих HTML-элементов, с которыми работает Hammer.js.
const hammerelms = {};
// --- Вспомогательные функции ---
// Генерирует или получает уникальный ID для HTML-элемента.
// Это важно для корректного управления экземплярами Hammer.js.
function getElementUniqueId(elm) {
if (!elm.id) {
// Генерируем ID, если у элемента его нет, чтобы связать с ним экземпляр Hammer.js.
elm.id = 'hammer-generated-id-' + Math.random().toString(36).substr(2, 9);
}
return elm.id;
}
// Функция для полного удаления всех примененных обработчиков Hammer.js
// и сброса стилей элементов. Полезна при смене состояний (например, переход на десктоп).
function removeHammerFromElementAll() {
// Уничтожаем все активные экземпляры Hammer.js.
Object.keys(hammerInstances).forEach(function(key) {
const hammertimeInstance = hammerInstances[key];
if (hammertimeInstance) {
hammertimeInstance.destroy(); // Освобождаем ресурсы.
}
});
// Сбрасываем примененные CSS-трансформации и другие стили,
// чтобы вернуть элемент в исходное состояние.
Object.keys(hammerelms).forEach(function(key) {
let elm = document.getElementById(key);
if (elm) {
elm.style.webkitTransform = ''; // Сбрасываем трансформацию.
elm.style.touchAction = 'manipulation'; // Возвращаем стандартное поведение касаний.
elm.style.userSelect = ''; // Сбрасываем выбор текста.
elm.style.webkitUserDrag = ''; // Сбрасываем перетаскивание.
}
});
// Очищаем объекты, чтобы избежать утечек памяти.
Object.keys(hammerInstances).forEach(key => delete hammerInstances[key]);
Object.keys(hammerelms).forEach(key => delete hammerelms[key]);
}
// --- Основная функция для применения pinch/pan ---
// Применяет интерактивную функциональность (pinch zoom и pan) к указанному HTML-элементу.
function pinch_web(elm) {
// Создаем новый экземпляр Hammer.js для элемента.
const hammertime = new Hammer(elm, {});
// Включаем поддержку жеста 'pinch' (масштабирование двумя пальцами).
hammertime.get('pinch').set({
enable: true
});
// Получаем или генерируем уникальный ID для элемента.
const elementId = getElementUniqueId(elm);
// Сохраняем экземпляр Hammer.js и сам элемент для последующего управления.
hammerInstances[elementId] = hammertime;
hammerelms[elementId] = elm;
// Инициализируем переменные для отслеживания текущего состояния трансформации:
var posX = 0, // Текущая позиция по оси X.
posY = 0, // Текущая позиция по оси Y.
scale = 1, // Текущий масштаб.
last_scale = 1, // Предыдущий масштаб (для расчета изменения).
last_posX = 0, // Предыдущая позиция по X (для расчета изменения).
last_posY = 0, // Предыдущая позиция по Y (для расчета изменения).
max_pos_x = 0, // Максимально допустимое смещение по X.
max_pos_y = 0, // Максимально допустимое смещение по Y.
transform = "", // Строка с CSS-трансформацией.
el = elm; // Ссылка на текущий элемент.
// Обработчик различных жестов (событий) от Hammer.js.
hammertime.on('doubletap pan pinch panend pinchend', function(ev) {
// --- Обработка двойного касания (doubletap) ---
// При двойном касании мы либо увеличиваем изображение в 2 раза,
// либо сбрасываем его к исходному масштабу (1x).
if (ev.type == "doubletap") {
// Проверяем текущее состояние трансформации.
// Если оно не равно исходному (matrix(1, 0, 0, 1, 0, 0)), то сбрасываем.
if (window.getComputedStyle(el, null).getPropertyValue('-webkit-transform').toString() != "matrix(1, 0, 0, 1, 0, 0)") {
transform = "translate3d(0, 0, 0) scale3d(1, 1, 1)"; // Сброс к 1x.
scale = 1;
last_scale = 1;
} else { // Иначе, устанавливаем масштаб 2x.
transform = "translate3d(0, 0, 0) scale3d(2, 2, 1)"; // Увеличение до 2x.
scale = 2;
last_scale = 2;
}
}
// --- Обработка перемещения (pan) ---
// Этот блок выполняется, только если изображение увеличено (scale != 1).
if (scale != 1) {
// Рассчитываем новую позицию элемента, добавляя смещение текущего жеста (ev.deltaX, ev.deltaY)
// к последней сохраненной позиции.
posX = last_posX + ev.deltaX;
posY = last_posY + ev.deltaY;
// Ограничиваем перемещение, чтобы контент не выходил за пределы видимой области.
// Максимальное смещение рассчитывается на основе текущего масштаба и размеров элемента.
max_pos_x = Math.ceil((scale - 1) * el.clientWidth / 2);
max_pos_y = Math.ceil((scale - 1) * el.clientHeight / 2);
// Применяем ограничения: если posX/posY превышает максимум, устанавливаем максимум.
if (posX > max_pos_x) posX = max_pos_x;
if (posX < -max_pos_x) posX = -max_pos_x;
if (posY > max_pos_y) posY = max_pos_y;
if (posY < -max_pos_y) posY = -max_pos_y;
}
// --- Обработка масштабирования (pinch) ---
// Этот блок выполняется только при жесте 'pinch'.
if (ev.type == "pinch") {
// Рассчитываем новый масштаб: берем предыдущий масштаб (last_scale)
// и умножаем на коэффициент изменения текущего жеста (ev.scale).
// Ограничиваем масштаб значением от 0.999 (чтобы избежать полного сброса при минимальном движении)
// до 4 (максимальное значение масштабирования).
scale = Math.max(.999, Math.min(last_scale * (ev.scale), 4));
}
// Когда жест 'pinch' заканчивается (pinchend), сохраняем текущий масштаб
// как последний масштаб для следующего события.
if(ev.type == "pinchend"){
last_scale = scale;
}
// --- Обработка завершения перемещения (panend) ---
// Когда жест 'pan' заканчивается, сохраняем текущую позицию (posX, posY)
// как последнюю позицию (last_posX, last_posY), учитывая примененные ограничения.
if(ev.type == "panend"){
last_posX = posX < max_pos_x ? posX : max_pos_x;
last_posY = posY < max_pos_y ? posY : max_pos_y;
}
// Формируем строку CSS-трансформации, если масштаб отличается от 1.
if (scale != 1) {
transform = `translate3d(${posX}px,${posY}px, 0) scale3d(${scale}, ${scale}, 1)`;
} else {
// Если масштаб вернулся к 1, сбрасываем трансформацию.
transform = "";
}
// Применяем сформированную трансформацию к элементу.
if (transform) {
el.style.webkitTransform = transform;
} else {
el.style.webkitTransform = ''; // Явный сброс, если transform пуст.
}
});
}
// --- Дополнительная функция для определения мобильной ширины ---
// Простая функция, возвращающая true, если ширина окна браузера <= 768px (типичная мобильная ширина).
function isMobileWidth() {
return window.innerWidth <= 768;
}
Пример использования
Для применения функциональности pinch zoom и pan к вашим изображениям, вам нужно найти нужные элементы на странице и передать их в функцию pinch_web.
HTML-структура (пример):
<div class="product-page__image">
<div class="product-page__image-main">
<div class="product-page__image-main-carousel owl-carousel owl-loaded owl-drag">
<div class="owl-stage-outer" style="overflow: inherit">
<div class="owl-stage">
<div class="owl-item">
<img src="/" alt="" title="" data-thumb="" data-full="" width="500" height="500" class="" />
</div>
<div class="owl-item">
<img src="/" alt="" title="" data-thumb="" data-full="" width="500" height="500" class="" />
</div>
</div>
</div>
</div>
</div>
</div>
JavaScript для применения pinch_web:
Привязываем элемнты .owl-item к функции pinch_web
let parentBlock_js = document.querySelector(".product-page__image");
// 2. Проверяем, найден ли родительский блок
if (parentBlock_js) {
// 3. Ищем все элементы .owl-item внутри найденного родительского блока
let imageContainers = parentBlock_js.querySelectorAll(".owl-item");
// 4. Проверяем, есть ли найденные элементы
if (imageContainers.length > 0) {
// 5. Перебираем найденные элементы и вызываем pinch_web для каждого
imageContainers.forEach((container) => {
pinch_web(container);
});
} else {
let imageContainers2 = parentBlock_js.querySelectorAll(".product-page__image-main-img");
imageContainers2.forEach((container) => {
pinch_web(container);
});
}
}
Удаляем элементы .owl-item от функции pinch_web
//webalan
setTimeout(() => {
//webalan
let parentBlock_js = document.querySelector(".product-page__image");
$(parentBlock).find(".owl-stage-outer").css("overflow", "hidden");
// 2. Проверяем, найден ли родительский блок
if (parentBlock_js) {
// 3. Ищем все элементы .owl-item внутри найденного родительского блока
let imageContainers = parentBlock_js.querySelectorAll(".owl-item");
// 4. Проверяем, есть ли найденные элементы
if (imageContainers.length > 0) {
removeHammerFromElementAll();
} else {
let imageContainers2 = parentBlock_js.querySelectorAll(".product-page__image-main-img");
imageContainers2.forEach((container) => {
container.style = "";
removeHammerFromElementAll();
});
}
}
}, 500);
Важные замечания
- CSS
touch-action: Для корректной работы Hammer.js, свойство CSStouch-actionна масштабируемых элементах часто устанавливают в'manipulation'или'none'. Это предотвращает конфликты с нативными жестами браузера. - Производительность: На страницах с большим количеством интерактивных изображений важно следить за производительностью.
- Управление состоянием: При разработке SPA, убедитесь, что вы корректно удаляете экземпляры Hammer.js при уничтожении компонента, чтобы избежать утечек памяти.
- Адаптивность: Используйте проверку
isMobileWidth()или медиа-запросы, чтобы применять интерактивность только на устройствах, где это действительно нужно.
Заключение
Реализация pinch zoom и pan с помощью Hammer.js — это мощный инструмент для создания интерактивных и интуитивно понятных интерфейсов на мобильных устройствах. Этот урок предоставляет вам основу для добавления функциональности "drag and drop двумя пальцами как в приложении" к вашим изображениям, улучшая пользовательский опыт и делая ваш сайт более современным.