Как создать компонент мобильного смахивания в React
Сегодня каждый хочет иметь доступ к Интернету со своих мобильных телефонов. Поэтому важно учитывать доступность вашего веб-приложения на Android и iPhone.
Самая сложная часть — создание навигации, и особенно смахивание может вызвать много головной боли. Есть библиотеки, которые вы можете использовать, чтобы помочь в этом, но почему бы не создать их самостоятельно?
В этом уроке я научу вас, как создать собственный компонент мобильного смахивания в React. Вы можете сделать это в 50 строках JavaScript.
Изначально я создал этот компонент для своего любимого проекта — игры 2048 в React, а теперь решил поделиться тем, как я это сделал.
Если вы хотите опробовать ее, прежде чем читать всю статью, вы можете поиграть в игру здесь (используйте свое мобильное устройство). В этом уроке мы сосредоточимся только на мобильных устройствах.
На GitHub вы можете найти следующие ресурсы:
Вот краткий обзор (качество не самое лучшее, потому что я старался сохранить небольшой размер):
🛠️ Подготовка
Прежде чем мы начнем, убедитесь, что вы немного знаете о React и JavaScript. Вам не потребуются какие-либо сложные инструменты, но если вы хотите запустить пример проекта на своем компьютере, вам сначала потребуется установить Node.js.
Кроме того, я использую Google Chrome для имитации мобильного устройства на своем компьютере. Имейте в виду, что если вы используете другой браузер, действия могут немного отличаться.
🔍 Как имитировать мобильное устройство в браузере
Прежде чем мы начнем программировать, мне нужно показать вам, как имитировать мобильное устройство в Google Chrome.
Откройте браузер, щелкните правой кнопкой мыши на своей странице и выберите «Проверить» в раскрывающемся меню.
Теперь представление вашего браузера должно быть разделено на две части. Вторая часть содержит инструменты разработчика Chrome.
Теперь нажмите значок «Ноутбук и мобильный телефон» (1 на изображении ниже) в верхнем левом углу инструментов разработчика. Затем выберите «Размеры» (2) устройства, которое вы хотите смоделировать. Я выбрал iPhone SE, потому что у этого устройства наименьшее разрешение, поддерживаемое моей игрой.
Теперь мы готовы имитировать смахивание с помощью мыши или тачпада. Просто обратите внимание на гифку ниже — мой курсор превратился в круг, который должен визуализировать область, покрытую пальцем человека.
Когда я пытаюсь провести пальцем по экрану, мой браузер думает, что я на самом деле пролистываю сайт. В моем случае это не ожидаемое поведение. Вместо этого я хотел бы использовать свайпы для игры.
🤓 Компонент MobileSwiper
Сначала нам нужно создать файл mobile-swiper.jsx
. В этом файле мы объявляем стандартный компонент React. Наш компонент должен принимать два свойства — Children
и onSwipe
.
export default function MobileSwiper({ children, onSwipe }) {
return null
}
Позвольте мне кратко их объяснить:
- Children позволяет
MobileSwiper
принимать и отображать любой контент, помещенный между открывающим и закрывающим тегами. Это позволяет вам вставить всю плату внутрь этого компонента, но мы вернемся к этому позже.
- onSwipe — это обратный вызов, который наш компонент будет запускать каждый раз, когда пользователь проводит пальцем по «перелистываемой» области.
Давайте теперь определим область перелистывания. Во-первых, вам нужно добавить ссылку на элемент DOM, в котором вы хотите разрешить перелистывание. Вы можете сделать это, используя хук useRef
. Итак, объявите константу с именем WrapperRef
:
import { useRef } from "react"
export default function MobileSwiper({ children, onSwipe }) {
const wrapperRef = useRef(null)
return null
}
Примечание: useRef
— это React Hook, который позволяет вам ссылаться на значение, которое не требуется для рендеринга. Его особенно часто используют для манипулирования DOM. В React есть встроенная поддержка для этого.
Теперь вам просто нужно вернуть <div />
, который ссылается на константу, созданную вами с помощью перехватчика useRef.
import { useRef } from "react"
export default function MobileSwiper({ children, onSwipe }) {
const wrapperRef = useRef(null)
return <div ref={wrapperRef}>{children}</div>
}
Давайте на мгновение задумаемся, как мы можем обнаружить смахивание. Я считаю, что самый простой способ — сравнить начальное и конечное положение пальца пользователя.
Это означает, что нам нужно сохранить в состоянии начальное положение пальца пользователя. По сути, мы будем хранить координаты x
и y
, используя хук useState
.
import { useState, useRef } from "react"
export default function MobileSwiper({ children, onSwipe }) {
const wrapperRef = useRef(null)
const [startX, setStartX] = useState(0)
const [startY, setStartY] = useState(0)
return <div ref={wrapperRef}>{children}</div>
}
Теперь нам нужно создать два обратных вызова событий:
handleTouchStart
установит начальную позицию пальца пользователя.
handleTouchEnd
установит конечное положение пальца пользователя и рассчитает сдвиг (дельту) на основе начальной точки.
Начнем с обработчика событий handleTouchStart
:
import { useCallback, useState, useRef } from "react"
export default function MobileSwiper({ children, onSwipe }) {
const wrapperRef = useRef(null)
const [startX, setStartX] = useState(0)
const [startY, setStartY] = useState(0)
const handleTouchStart = useCallback((e) => {
if (!wrapperRef.current.contains(e.target)) {
return
}
e.preventDefault()
setStartX(e.touches[0].clientX)
setStartY(e.touches[0].clientY)
}, [])
return <div ref={wrapperRef}>{children}</div>
}
Позвольте мне кратко объяснить это:
- Я включил этот помощник в хук
useCallback
, чтобы кэшировать и повысить его производительность. Если вы не знаете этот крючок, вы можете прочитать о нем в официальной документации React.
- Оператор
if
проверяет, проводит ли пользователь пальцем по перелистываемой области. Если они проведут пальцем за пределы этой области, мы продолжим прокрутку.
e.preventDefault()
отключает событие прокрутки по умолчанию.
- Последние две строки хранят координаты
x
иy
в состоянии.
Теперь давайте реализуем обработчик события handleTouchEnd
:
import { useCallback, useState, useRef } from "react"
export default function MobileSwiper({ children, onSwipe }) {
const wrapperRef = useRef(null)
const [startX, setStartX] = useState(0)
const [startY, setStartY] = useState(0)
const handleTouchStart = useCallback((e) => {
if (!wrapperRef.current.contains(e.target)) {
return
}
e.preventDefault()
setStartX(e.touches[0].clientX)
setStartY(e.touches[0].clientY)
}, [])
const handleTouchEnd = useCallback((e) => {
if (!wrapperRef.current.contains(e.target)) {
return
}
e.preventDefault()
const endX = e.changedTouches[0].clientX
const endY = e.changedTouches[0].clientY
const deltaX = endX - startX
const deltaY = endY - startY
onSwipe({ deltaX, deltaY })
}, [startX, startY, onSwipe])
return <div ref={wrapperRef}>{children}</div>
}
Как видите, шаги 1–3 точно такие же, как и в обратном вызове handleTouchStart
. Теперь мы возьмем окончательные координаты x
и y
пальца пользователя и вычтем из них начальные x
и y
. Благодаря этому мы можем рассчитать горизонтальное и вертикальное смещение пальца пользователя (дельт).
Затем мы передаем эти отклонения в обратный вызов onSwipe
. Если вы помните, мы объявили это в свойствах компонента вначале.
Теперь нам нужно подключить эти обратные вызовы к прослушивателю событий. Для этого мы можем использовать хук useEffect
из React.
import { useCallback, useEffect, useState, useRef } from "react"
export default function MobileSwiper({ children, onSwipe }) {
const wrapperRef = useRef(null)
const [startX, setStartX] = useState(0)
const [startY, setStartY] = useState(0)
const handleTouchStart = useCallback((e) => {
if (!wrapperRef.current.contains(e.target)) {
return
}
e.preventDefault()
setStartX(e.touches[0].clientX)
setStartY(e.touches[0].clientY)
}, [])
const handleTouchEnd = useCallback(
(e) => {
if (!wrapperRef.current.contains(e.target)) {
return
}
e.preventDefault()
const endX = e.changedTouches[0].clientX
const endY = e.changedTouches[0].clientY
const deltaX = endX - startX
const deltaY = endY - startY
onSwipe({ deltaX, deltaY })
}, [startX, startY, onSwipe])
useEffect(() => {
window.addEventListener("touchstart", handleTouchStart)
window.addEventListener("touchend", handleTouchEnd)
return () => {
window.removeEventListener("touchstart", handleTouchStart)
window.removeEventListener("touchend", handleTouchEnd)
}
}, [handleTouchStart, handleTouchEnd])
return <div ref={wrapperRef}>{children}</div>
}
Подробнее о хуке useEffect
можно прочитать в официальной документации React.
Компонент MobileSwiper
готов.
🔌 Давайте сделаем его перелистываемым
Последнее, что нам нужно сделать, это подключить наш компонент к приложению. Как я уже упоминал, я буду использовать этот компонент в своей игре 2048 (исходный код). Если вы захотите использовать его где-то еще, помощник handleSwipe
и компонент MobileSwiper
останутся прежними.
Давайте подключим его к компоненту Board:
import { useCallback, useContext, useEffect, useRef } from "react"
import { Tile as TileModel } from "@/models/tile"
import styles from "@/styles/board.module.css"
import Tile from "./tile"
import { GameContext } from "@/context/game-context"
import MobileSwiper from "./mobile-swiper"
export default function Board() {
const { getTiles, moveTiles, startGame } = useContext(GameContext);
// ... removed irrelevant parts ...
const handleSwipe = useCallback(({ deltaX, deltaY }) => {
if (Math.abs(deltaX) > Math.abs(deltaY)) {
if (deltaX > 0) {
moveTiles("move_right")
} else {
moveTiles("move_left")
}
} else {
if (deltaY > 0) {
moveTiles("move_down")
} else {
moveTiles("move_up")
}
}
}, [moveTiles])
// ... removed irrelevant parts ...
return (
<MobileSwiper onSwipe={handleSwipe}>
<div className={styles.board}>
<div className={styles.tiles}>{renderTiles()}</div>
<div className={styles.grid}>{renderGrid()}</div>
</div>
</MobileSwiper>
)
}
Начнем с обработчика handleSwipe
. Как видите, мы сравниваем, больше ли deltaX
, чем deltaY
, чтобы определить, провел ли пользователь горизонтально (влево/вправо) или вертикально (вверх/вниз).
Если это был горизонтальный свайп, то:
- отрицательное
deltaX
означает, что они провели пальцем влево.
- положительное
deltaX
означает, что они провели вправо.
Если это был вертикальный свайп, то:
- отрицательная дельта означает, что они провели пальцем вверх.
- положительная дельта означает, что они смахнули вниз.
Теперь давайте сосредоточимся на компоненте MobileSwiper
. Вы можете найти его в операторе возврата. Мы передаем вспомогательный метод handleSwipe
свойству onSwipe
и обертываем весь HTML-код компонента Board, чтобы включить возможность пролистывания по нему.
Теперь, когда мы пробуем это, результат не идеален. События прокрутки смешаны с мобильными пролистываниями, как вы можете видеть ниже:
Это происходит потому, что современные браузеры используют пассивные прослушиватели событий для улучшения качества прокрутки на мобильных устройствах. Это означает, что preventDefault
, которое мы добавили в наши обратные вызовы событий, никогда не произойдет.
Чтобы отключить поведение прокрутки, нам нужно отключить пассивные прослушиватели в компоненте MobileSwiper
:
import { useCallback, useEffect, useState, useRef } from "react"
export default function MobileSwiper({ children, onSwipe }) {
// ... removed to improve visibility ...
useEffect(() => {
window.addEventListener("touchstart", handleTouchStart, { passive: false })
window.addEventListener("touchend", handleTouchEnd, { passive: false })
// ... removed to improve visibility ...
}, [handleTouchStart, handleTouchEnd])
// ... removed to improve visibility ...
}
Теперь поведение прокрутки исчезло, и игра 2048 выглядит просто потрясающе:
🏁 Краткое содержание
Сегодня я показал вам, что вам не всегда нужны библиотеки для обработки мобильных жестов в React. Некоторые простые события, такие как перелистывание, можно реализовать с помощью базовых функций React. Мы просто использовали кучу хуков React и написали два простых обработчика событий.
Вся реализация имеет ровно 50 строк кода. Надеюсь, я вдохновил вас попробовать разобраться с мобильными событиями самостоятельно.
Если эта статья помогла вам, пожалуйста, дайте мне знать в Twitter. Преподавателям вроде меня часто кажется, что мы говорим в вакууме, и никого не волнует, чему мы учим. Простое «приветствие» показывает, что усилия того стоили, и вдохновляет меня на создание большего количества подобного контента.
Пожалуйста, поделитесь этой статьей в своих социальных сетях. Спасибо!
🎥 Создайте свою собственную игру 2048
Эта статья является частью моего курса по Udemy, где я учу, как создать полнофункциональную игру 2048 в Next.js с нуля.
🧑🎓 Присоединяйтесь к моему курсу Next.js на Udemy.
Используйте код FREECODECAMP для регистрации и получите скидку 60%.
Я считаю, что программирование должно приносить удовольствие и раскрывать творческий потенциал. Вам не нужно создавать еще один список дел или корзину покупок. Вместо этого вы можете создать что-то, что сможете показать своим друзьям или, возможно, даже менеджеру по найму!
ПС. Если вы предпочитаете смотреть скринкасты, то этот урок доступен на Udemy бесплатно. Вы можете найти его в разделе «Адаптивный макет и недостающая функция игры» лекции «Макет игры и мобильные смахивания».