Приветствую, друзья технологии!

Сегодня в мире постоянно меняющихся технологий и уникальных разработок смартфонов, планшетов и других устройств, оказаться "в тренде" - это как настоящее искусство. Каждый из нас хочет использовать устройства, которые позволяют нам легко и интуитивно взаимодействовать с миром цифровых возможностей. Одной из фантастических новинок, которая взрывает сознание пользователей и разработчиков, является свайп-сайдбар – это гениальное решение для эффективной навигации и управления контентом!

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

В этой увлекательной статье мы окунемся в мир свайп-сайдбаров, расскажу, как они работают, как создать свой собственный сервис для свайпов, прикрутим все это дело к 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 версия)

Вот и весь секрет свайпов!

Надеюсь статья была полезна для вас! Удачи!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как вам статья?
60%Отлично! Хороший материал6
20%Хорошо! Материал неплохой2
10%Удовлетворительно, материал средне написан1
10%Ничего не понял, но очень интересно!1
Проголосовали 10 пользователей. Воздержались 5 пользователей.