Приветствую, друзья технологии!
Сегодня в мире постоянно меняющихся технологий и уникальных разработок смартфонов, планшетов и других устройств, оказаться "в тренде" - это как настоящее искусство. Каждый из нас хочет использовать устройства, которые позволяют нам легко и интуитивно взаимодействовать с миром цифровых возможностей. Одной из фантастических новинок, которая взрывает сознание пользователей и разработчиков, является свайп-сайдбар – это гениальное решение для эффективной навигации и управления контентом!
Вам, наверняка, приходилось сталкиваться с ситуацией, когда приложение или веб-сайт предлагают свернутый сайдбар, который появляется с одного края экрана после легкого свайпа пальцем. Это действительно захватывающий опыт, который добавляет удобство и стиль в нашу повседневную жизнь.
В этой увлекательной статье мы окунемся в мир свайп-сайдбаров, расскажу, как они работают, как создать свой собственный сервис для свайпов, прикрутим все это дело к Vue + Typescript. Не волнуйтесь, если вы новичок в программировании или разработке, я проведу вас через каждый шаг, чтобы вы могли освоить это волшебство свайпов!
Немного логики
Представьте себе, что весь этот процесс — это настоящее путешествие в мир магии и волшебства, где вы становитесь волшебником своего приложения или веб-страницы, способным управлять им лишь прикосновением вашего пальца.
Ваш сервис на TypeScript, будь-то мудрый старец или мудрая старица, отвечает за отслеживание каждого движения пальца по горизонтали и вертикали. И когда он распознает ваше магическое движение, он возвращает это знание Vue компоненту, чтобы выполнить заклинание открытия сайдбара!
В Vue компоненте, ваша роль — это как раз таинственный архитектор, который создает уникальный контейнер для отображения сайдбара. Вы создаете экземпляр класса сервиса для свайпа в любом месте, что пожелаете. Теперь ваш компонент обладает магической силой, способной обрабатывать жесты касания!
И вот, когда пользователь делает первый шаг, касаясь вашего компонента, вы ловите этот момент, как настоящий охотник за тенями, и активируете обработчик touchstart. Как будто вы дотрагиваетесь до магической сферы, которая раскрывается перед вами!

Затем, каждое движение пальца, зафиксированное вашими сенсорами, отправляется в обработчик touchmove. Это, как путь волшебной энергии, направляющий вас на свои глубины.

Когда вашему взору предстанет финальное место, куда ведет жест, вы с умением оракула применяете заклинание touchend. Теперь, когда ваш сервис TypeScript передаст всю информацию о касании, вы открываете свой сокровищницу — ваш сайдбар!

Таким образом, волшебное сочетание сервиса TypeScript и компонента Vue дает вам возможность создать увлекательное путешествие для пользователей, где они могут легко и интуитивно взмахом пальца открывать и закрывать сайдбар. Вы — настоящий маг в своем роде, привносящий чудеса в каждое касание!
К главному
Ну, надеюсь я вас заинтересовал таким загадочным текстом :) Теперь перейдем к самому коду!:
В коде определено два класса: RootSwipeable и SwipeableService.
RootSwipeable - это класс, представляющий простой объект, отвечающий за обработку свайпов. Этот класс не взаимодействует с непосредственно с интерфейсом пользователя и предназначен для внутреннего использования в SwipeableService.
RootSwipeable содержит следующие поля:
p_offset: число, представляющее смещение (порог), которое определяет, насколько далеко нужно свайпнуть, чтобы считать это свайпом влево или вправо.
p_previous: объект с полем x, которое содержит предыдущее положение по оси X. Изначально установлено в null.
private readonly p_offset: number; // Минимальное смещение для определения свайпа
private p_previous: { x: number | null }; // Предыдущее положение по оси X
constructor({ offset }: { offset: number }) {
this.p_offset = offset; // Установка минимального смещения
this.p_previous = {x: null}; // Инициализация предыдущего положения
}updatePreviousState - это метод класса RootSwipeable, который обновляет значение p_previous.x, записывая в него текущее положение по оси X из переданного события TouchEvent.
// Обновление предыдущего положения на основе события TouchEvent
updatePreviousState(event: TouchEvent): void {
this.p_previous.x = event.changedTouches[0].screenX;
}init - это метод класса RootSwipeable, предназначенный для инициализации начального состояния свайпа. Если p_previous.x равно null, вызывается метод updatePreviousState с переданным событием TouchEvent.
// Инициализация начального состояния свайпа
init(event: TouchEvent): void {
!this.p_previous.x && this.updatePreviousState(event);
}handleGesture - это метод класса RootSwipeable, который обрабатывает жесты (свайпы) на основе изменений TouchEvent. Он принимает объект со свойствами onLeft и onRight, которые представляют собой функции обратного вызова (колбэки) для обработки свайпов влево и вправо соответственно.
Метод handleGesture сравнивает текущее положение по оси X с предыдущим положением p_previous.x. Если разница между ними превышает значение p_offset, то считается, что был совершен свайп влево или вправо. В зависимости от направления свайпа вызывается соответствующий колбэк onLeft или onRight, и обновляется значение p_previous.x на текущее положение.
// Обработка жестов (свайпов) на основе изменений TouchEvent
handleGesture(event: TouchEvent, {
onLeft = () => {}, // Колбэк для свайпа влево
onRight = () => {} // Колбэк для свайпа вправо
}: {
onLeft: () => void,
onRight: () => void
}) {
if (!this.p_previous.x) {
return; // Если предыдущее положение не определено, выход из метода
}
let screenX = event.changedTouches[0].screenX; // Текущее положение по оси X
// Если смещение вправо превышает минимальное значение
if (this.p_previous.x + this.p_offset < screenX) {
this.updatePreviousState(event); // Обновление предыдущего положения
onRight(); // Вызов колбэка для свайпа вправо
return;
}
// Если смещение влево превышает минимальное значение
if (this.p_previous.x - this.p_offset > screenX) {
this.updatePreviousState(event); // Обновление предыдущего положения
onLeft(); // Вызов колбэка для свайпа влево
}
}Метод kill обнуляет значение p_previous.x, что означает завершение жеста.
И теперь подрезюмируем весь класс:
class RootSwipeable {
private readonly p_offset: number; // Минимальное смещение для определения свайпа
private p_previous: { x: number | null }; // Предыдущее положение по оси X
constructor({ offset }: { offset: number }) {
this.p_offset = offset; // Установка минимального смещения
this.p_previous = {x: null}; // Инициализация предыдущего положения
}
// Обновление предыдущего положения на основе события TouchEvent
updatePreviousState(event: TouchEvent): void {
this.p_previous.x = event.changedTouches[0].screenX;
}
// Инициализация начального состояния свайпа
init(event: TouchEvent): void {
!this.p_previous.x && this.updatePreviousState(event);
}
// Обработка жестов (свайпов) на основе изменений TouchEvent
handleGesture(event: TouchEvent, {
onLeft = () => {}, // Колбэк для свайпа влево
onRight = () => {} // Колбэк для свайпа вправо
}: {
onLeft: () => void,
onRight: () => void
}) {
if (!this.p_previous.x) {
return; // Если предыдущее положение не определено, выход из метода
}
let screenX = event.changedTouches[0].screenX; // Текущее положение по оси X
// Если смещение вправо превышает минимальное значение
if (this.p_previous.x + this.p_offset < screenX) {
this.updatePreviousState(event); // Обновление предыдущего положения
onRight(); // Вызов колбэка для свайпа вправо
return;
}
// Если смещение влево превышает минимальное значение
if (this.p_previous.x - this.p_offset > screenX) {
this.updatePreviousState(event); // Обновление предыдущего положения
onLeft(); // Вызов колбэка для свайпа влево
}
}
// Обнуление предыдущего положения, завершение свайпа
kill() {
this.p_previous = {x: null};
}
}
SwipeableService - это класс, который предоставляет обертку над RootSwipeable для удобного использования в интерфейсе пользователя.
root: RootSwipeable; // Экземпляр класса RootSwipeable
pageWidth: number; // Ширина страницы
minSwipe: number; // Минимальное значение для определения свайпа
touchstart: { x: number }; // Начальное положение пальца по оси X
touchend: { x: number }; // Конечное положение пальца по оси X
constructor({offset}: { offset: number }) {
this.root = new RootSwipeable({ offset }); // Создание экземпляра RootSwipeable
this.pageWidth = window.innerWidth || document.body.clientWidth; // Определение ширины страницы
this.minSwipe = Math.max(1, Math.floor(0.01 * (this.pageWidth))); // Вычисление минимального значения для свайпа
this.touchstart = {x: 0}; // Инициализация начального положения
this.touchend = {x: 0}; // Инициализация конечного положения
}touchStart - это метод класса SwipeableService, который вызывается при событии touchstart. Он инициализирует начальное состояние свайпа, вызывая init у RootSwipeable и записывая начальное положение по оси X в touchstart.x.
// Обработчик события touchstart
touchStart(event: TouchEvent) {
this.root.init(event); // Инициализация начального состояния свайпа
this.touchstart.x = event.changedTouches[0].screenX; // Сохранение начального положения пальца
}touchMove - это метод класса SwipeableService, который вызывается при событии touchmove. Он обновляет touchend.x и затем вызывает метод handleGesture у RootSwipeable с переданными колбэками onLeft и onRight.
// Обработчик события touchmove
touchMove(event: TouchEvent, {
onLeft = () => {}, // Колбэк для свайпа влево
onRight = () => {} // Колбэк для свайпа вправо
}) {
this.touchend.x = event.changedTouches[0].screenX; // Сохранение конечного положения пальца
this.handleGesture(event, {onLeft, onRight}); // Вызов метода handleGesture для обработки свайпа
}touchEnd - это метод класса SwipeableService, который вызывается при событии touchend. Он завершает жест, вызывая kill у RootSwipeable и обнуляя touchstart.x и touchend.x.
export default class SwipeableService {
root: RootSwipeable; // Экземпляр класса RootSwipeable
pageWidth: number; // Ширина страницы
minSwipe: number; // Минимальное значение для определения свайпа
touchstart: { x: number }; // Начальное положение пальца по оси X
touchend: { x: number }; // Конечное положение пальца по оси X
constructor({offset}: { offset: number }) {
this.root = new RootSwipeable({ offset }); // Создание экземпляра RootSwipeable
this.pageWidth = window.innerWidth || document.body.clientWidth; // Определение ширины страницы
this.minSwipe = Math.max(1, Math.floor(0.01 * (this.pageWidth))); // Вычисление минимального значения для свайпа
this.touchstart = {x: 0}; // Инициализация начального положения
this.touchend = {x: 0}; // Инициализация конечного положения
}
// Обработчик события touchstart
touchStart(event: TouchEvent) {
this.root.init(event); // Инициализация начального состояния свайпа
this.touchstart.x = event.changedTouches[0].screenX; // Сохранение начального положения пальца
}
// Обработчик события touchmove
touchMove(event: TouchEvent, {
onLeft = () => {}, // Колбэк для свайпа влево
onRight = () => {} // Колбэк для свайпа вправо
}) {
this.touchend.x = event.changedTouches[0].screenX; // Сохранение конечного положения пальца
this.handleGesture(event, {onLeft, onRight}); // Вызов метода handleGesture для обработки свайпа
}
// Обработчик жестов (свайпов) на основе изменений TouchEvent
handleGesture(event: TouchEvent,
{onLeft, onRight}:
{
onLeft: (e: TouchEvent, x: number) => void,
onRight: (e: TouchEvent, x: number) => void
}
) {
let x = this.touchend.x - this.touchstart.x; // Вычисление смещения по оси X
if (Math.abs(x) > this.minSwipe) { // Если смещение превышает минимальное значение для свайпа
this.root.handleGesture(event, {
onRight: () => onRight(event, x), // Вызов колбэка для свайпа вправо с параметрами события и смещения
onLeft: () => onLeft(event, x) // Вызов колбэка для свайпа влево с параметрами события и смещения
});
}
}
// Обработчик события touchend
touchEnd() {
this.root.kill(); // Завершение свайпа
this.touchstart = {x: 0}; // Обнуление начального положения
this.touchend = {x: 0}; // Обнуление конечного положения
}
}Важно отметить, что SwipeableService делегирует основную функциональность свайпов в RootSwipeable, а самостоятельно обрабатывает только начальное и завершающее состояния свайпов, а также вызывает соответствующие колбэки при определенных условиях свайпа.
В принципе, на этом наш сервис готов, осталось только его прикрутить к Vue компоненту
Связываем наш сервис с Vue
Небольшая отметочка - сайдбар я задумывал слева, поэтому вся логика будет относиться к случаю, когда сайдбар слева :) Но не трудно переписать и на правый, просто инвертировать обработчики left, right.
Для начала напишем структуру для компонента:
<template>
<div class="main">
<div class="main-page">
<main class="main-page_content">
<div class="sidebar"
style="transition: all .2s linear"
ref="sidebar"
@touchstart="touchStart($event)"
@touchend="touchEnd($event)"
@touchmove="touchMove($event)">
<button style="background:#2654ac; padding: 5px; color:#fff;" class="toggler" @click="toggleSidebar($event)">X</button>
</div>
</main>
</div>
</div>
</template>И да, у Vue под капотом уже есть встроенная функция для работы с touch событиями, нам остается только пришабашить наш новоиспеченный сервис как нибудь к этим событиям. Делается это дов��льно несложно, логика никакая не меняется, т.к. нужно просто вызывать методы класса сервиса. Добавятся лишь стили, которые будут убирать или выдвигать наш сайдбар!
Нам потребуется всего лишь 3 переменные:
swipeable: new Swipeable({offset: 2}), // Экземпляр класса
currentPosition: 0, // текущая позиция касания
isSidebarOpened: true, // открыт ли сайдбар?Как я и говорил выше, логика в плане касаний не меняется, мы просто вызываем те же самые функции, только уже с прикрутом touch событий по компоненту:
// как раз те самые функции, которые @touchStart="touchStart($event)"
touchStart(e) {
this.swipeable.touchStart(e)
},
touchMove(e) {
this.swipeable.touchMove(e, {onLeft: this.handleLeftTouch, onRight: this.handleRightTouch})
},
touchEnd(e) {
this.currentPosition = parseInt(e.target.style.left, 10)
this.swipeable.touchEnd(e)
},Теперь давайте напишем логику для стилей сайдбара, чтобы он красиво улетал за пределы экрана и обратно, для этого нам потребуется обрабатывать события влево и вправо, помним, что такой функционал уже есть и мы можем передать свои функции-обработчики. Осталось написать сами обработчики:
handleLeftTouch(e, x) {
const blockInstance = e.target;
const blockInstanceWidth = blockInstance.offsetWidth
if (x + this.currentPosition > 0) {
// Если свайп пытается выйти за пределы
blockInstance.style.left = `0`
} else if (Math.abs(x + this.currentPosition) > blockInstanceWidth / 2) {
// Если наш свайп был больше чем на половину, закроем его
blockInstance.style.left = `-${blockInstanceWidth}px`
this.isSidebarOpened = false;
} else {
// В остальных случаях просто посмотрим, на сколько свайпнули и изменим положение сайдбара
blockInstance.style.left = `${x + this.currentPosition}px`
console.log('свайп на ', x, 'px влево')
}
},
handleRightTouch(e, x) {
const blockInstance = e.target;
// Та же проверка на пределы
if (x + this.currentPosition > 0) {
blockInstance.style.left = `0`
return;
}
blockInstance.style.left = `${x + this.currentPosition}px`
console.log('свайп на ', x, 'px вправо')
},Также еще сделаем обработку кнопки, которая будет отвечать за открытие сайдбара, когда он закрыт:
toggleSidebar(e) {
const sidebarNode = e.target.offsetParent
if (this.isSidebarOpened) {
sidebarNode.style.left = `-${sidebarNode.offsetWidth}px`
this.isSidebarOpened = false;
} else {
sidebarNode.style.left = "0";
this.currentPosition = 0;
this.isSidebarOpened = true;
}
},Вот и подошли к итоговой версии кода! Поздравляю всех, кто дочитал до этого места :)
Демонстрация

Полный код на моем гитхабе (в нем также есть и JS версия)
Вот и весь секрет свайпов!
Надеюсь статья была полезна для вас! Удачи!
