Как разработать плагин «бесшовной прокрутки» со скоростью 60 кадров в секунду

JavaScript

Что такое «бесшовная прокрутка»

Так называемая «бесшовная прокрутка» означает, что процесс переключения между несколькими экранами является непрерывным и зацикленным, а не останавливает воспроизведение на последнем экране. Этот тип бизнес-сценария очень распространен в реальной разработке.Следующие — «Taobao» и «Jingdong».H5Скриншот главной страницы версии, «баннер» и «заголовок» — типичные сцены с плавной прокруткой. Но испытав это, вы обнаружите, что между ними и эффектом в нативном приложении все еще есть определенный разрыв. Вы можете отсканировать код, чтобы открыть его и испытать на своем мобильном телефоне, а затем открыть их приложение и попробовать его, проведя пальцем по экрану.ты найдешьH5В версии чего-то не хватает!

  

Таобао:  Цзиндон:

Возможно, вы нашли его!H5Похоже, что в этой версии не хватает суждения о намерении жеста пользователя: например, сцена на рисунке ниже, если вы находитесь на Taobao.H5версия баннера, вы медленно качаете влево и вправо, это будет только простое сравнениеtouchstartа такжеtouchendАбсцисса при запуске события определяет, в каком направлении перемещаться на один экран. И если вы сделаете это в собственном приложении, в конце оно вернется к экрану, который занимает большую часть текущего экрана, то есть к первому экрану, и когда вы быстро проведете пальцем по тому же месту, он переключится на второй экран.

По сравнению с JingdongH5Впечатления будут чуть хуже, он просто "не следит" за вами, когда вы скользите, а определяет направление только после того, как вы остановитесь (данная статья написана в марте 2019 года, с обновлением сайта опыт может быть другим. разные).

Как производитель бенчмаркинга, одни и те же продукты находятся на стороне приложения иH5Они должны знать о различиях в сквозной производительности, но почему они несовместимы? Если вы хотите использовать интерфейсную технологию для достижения этой цели, это должно потребовать небольших дополнительных затрат на разработку или столкнуться с такими проблемами, как зависание. Давайте примерно проанализируем, как они в настоящее время добиваются «бесшовной прокрутки». Но прежде чем мы начнем, давайте посмотрим на:

Основы бесшовной прокрутки

Как показано на рисунке выше, мы располагаем три картинки в ряд, а именно № 1, № 2 и № 3. Из окна первой появляется картинка № 1. После непродолжительной остановки пролистайте до № 1. 2, а затем идите назад по очереди. , который является номером 3. Если мы хотим добиться «бесшовной прокрутки», а не заканчиваться на 3-м, то 1-й должен появиться следующим, потому что это может сформировать визуальную круговую прокрутку, поэтому нам нужно быть на 3-м. №1 позже. Когда эта «фальшивая» картинка №1 полностью прокручивается до заполнения экрана, мы быстро переводим все в начальное состояние. За счет этой «телепортации» из окна отображается полная картина №1, поэтому визуально «мутацию» за ней не почувствовать. Так как пользователь может листать влево и вправо посредством жестов, в свою очередь, в стартовой позиции должна быть добавлена ​​картинка № 3, которая здесь повторяться не будет, таким образом, независимо от того, прокручивая влево или вправо, она будет формировать визуально бесшовный эффект.

Итак, каковы технические моменты, связанные с реализацией этого с точки зрения внешнего интерфейса?

  1. Реализация смещения: мы можем использоватьpositionПозиционирование +left/topзначение, вы также можете использоватьtransform: translate(x, y)метод, который лучше или хуже, ответ таков, что последний лучше. Заинтересованные могут прочитать эту справочную статьюWhy Moving Elements With Translate() Is Better Than Pos:abs Top/left.
  2. «Одна команда и одно действие»: запуск и остановка, запуск и остановка. . . Понятно, что нужен таймер.
  3. "Движется как кролик": То есть анимация переключения перед двумя экранами. Например, стиль на первом экране на картинке выше:transform: translate(0px, 0px), а на втором экране этоtransform: translate(-200px, 0px). Обычно для этого изменения требуется быстрая анимация перехода, а не мгновенная «мутация» с №1 на №2, в этом случае нужно использоватьtransition: translate 0.3s ease;чтобы указать, что вы хотите, чтобы этот переход был плавным процессом. Здесь есть проблема: когда два экрана №1 нужно соединить и сбросить позиции, им нужно «мутировать», как на картинке выше.transform: translate(-600px, 0px)прибытьtransform: translate(0px, 0px)процесс. Это означает, что элемент спискаtransformАтрибуты не являются статическими, поэтому вам нужно определить, является ли ток критическим состоянием до того, как таймер «One-Shot» начинает переключаться на следующий экран, чтобы установить разныеtransitionпродолжительность.
  4. Работа с жестами под мобильным терминалом: нам нужно использовать три события, связанные с прикосновением, а именно:touchstart(пальцем касаясь экрана),touchmove(при скольжении он будет срабатывать постоянно) иtouchend(пальцем от экрана). И что нам нужно сделать, это пройтиevent.touchesилиevent.changedTouchesПолучите их информацию о координатах, когда эти события запускаются. например, когда пользовательtouchstartПри срабатывании мы записываем началоxкоординаты оси,touchmoveПри срабатывании мы сравниваем это времяxРазница между координатами оси и координатами в начале, с помощьюtranslateПеремещайтесь на такое же расстояние, чтобы добиться эффекта «следующей руки». и когдаtouchendПри срабатывании мы по-прежнему подтверждаем, хочет ли пользователь провести пальцем влево или вправо, сравнивая разницу с начальной координатой.

Все дороги ведут в Рим

Приведенное выше введение — лишь одна из наиболее распространенных идей для достижения «бесшовной прокрутки», Taobao кажется умнее: мы знаем, что палец пользователя на экране, самое дальнее расстояние непрерывного пролистывания не может превышать ширину экрана, например как и я. В это время на экране 2 я могу перейти на экран 1 или максимум на экран 3. В любом случае, я не могу перейти на экран 4 за раз. То есть, независимо от того, сколько у нас всего экранов, мы появляемся на экране одновременно.DOMВ лучшем случае он принадлежит двум соседним экранам. В этом случае мы можем поместить остальную часть экрана вочередь ожидания, пусть они остаются в фиксированном положении за пределами экрана. Таким образом, каждый раз, когда вы прокручиваете, область, перерисовываемая браузером, составляет всего два экрана — текущий экран и следующий экран. вместоn + 2, что несомненно намекает на производительность. Я считаю, что благодаря следующей динамической картинке вы сможете понять, что я имею в виду:

Из приведенного выше рисунка видно, что одновременно перемещаются только два экрана, а позиции DOM трех экранов менялись во время каждой волны переключения. Следующие два скриншота также очень хорошо подтверждают мою догадку:

В конце концов, Али есть Али, а босс есть босс, я должен восхищаться этим! Напротив, JD.com немного грубее, используя основные принципы, которые я представил в начале. Хотя существуют различия в принципах бесшовной прокрутки, в основном используются четыре метода, которые я перечислил выше. Алгоритм реализации Таобао очень хорош, но есть фатальная проблема, его трудно удовлетворитьНажмите, чтобы переключитьсяЕсли на "индикатор в виде маленькой точки" внизу можно нажать, чтобы прыгнуть, то представьте, как он прыгает со второго экрана на четвертый экран? Но эта потребность характерна для «бесшовной прокрутки» на ПК, и как разработчик вы должны думать наперед. Оба также страдают от непонимания намерения жеста пользователя, о котором я упоминал в начале, поэтому пришло время представить новое решение:

seamless-scroll

Это подключаемый модуль бесшовной прокрутки, который я недавно использовал. Он подходит как для мобильных устройств, так и для сценариев разработки ПК.requestAnimationFrameа такжеtranslateвыполнить. Обеспечивает собственный интерфейс, подобный приложению, добавляя обработку сценариев жестов, таких как «быстрое пролистывание для переключения» и «медленное перетаскивание». Не зависит от каких-либо существующих фреймворков или библиотек компонентов, чистыйJS, а это значит, что где бы вы ни находилисьVueещеReactМожно использовать прямо в проекте. служба поддержкиnpmустановить иCDN-ссылкаимпорт,📦Gzip Size< 3KB,служба поддержкиIE10+,IOS9+,Andorid5+и современные браузеры. Он также прост в использовании, он предоставляетSeamlessScrollконструктор, вы можете использоватьnewКлючевое слово создает экземпляр "бесшовной прокрутки". Передавая параметры, вы можете настроить скорость анимации, автоматическое воспроизведение и другие варианты поведения. Созданный экземпляр также обеспечиваетstart,stop,goА другие методы позволяют легко управлять началом и остановкой воспроизведения или напрямую переходить к индексной позиции.

Адрес репозитория на гитхабеОтсканируйте код, чтобы испытать мобильный терминалНажмите, чтобы просмотреть на ПКПример кода, используемого в React

Протестировано на реальном iPhone и Xiaomi 5, работа по-прежнему очень плавная, на картинке ниже Google Chrome.PerformanceСкриншот панели вверхуFPSКолонна образует непрерывные и стабильные 5 зеленых маленьких блоков, отражающих 5 ходов во времяFPSИзменение. Чем выше эти зеленые полосы, тем выше частота кадров и более плавное воспроизведение.Наоборот, если есть красные полосы, вероятно, будет отставание.

Ниже я представлю свои идеи реализации:

   Сначала выберите основной принцип реализации: два принципа «бесшовной прокрутки» «стиль Taobao» и «стиль Jingdong», представленные выше, потому что для удовлетворения потребностей прямого перехода мы выбрали последний.

Переосмысление выбора технологии: выше представлены четыре технических момента, которые необходимо использовать для реализации «бесшовной прокрутки». ссылка "заяц". Выше мы говорили, что эту анимацию перехода можно использоватьtransitionдля достижения, он работает очень плавно. Но мы знаемСуть анимации — это на самом деле группа непрерывных движущихся картинок., в этом случае мы можем пройтиНепрерывное перемещение на небольшое расстояние за короткий промежуток времениЧтобы добиться эффекта анимации? конечно может. Мы могли бы также абстрагировать процесс «бесшовной прокрутки», как иЦиклическая комбинация двух состояний - статического состояния и состояния анимации.

  Стационарное состояниеДалее мы запустим следующую волну после задержки на время через таймер.состояние анимации, и для этогосостояние анимацииподтверждатьцелевое местоположение, пока всостояние анимацииДалее аккуратно «движемся» шаг за шагом, следя за тем, чтобы мы пришли в любое времяцелевое местоположение, если оно достигнуто, останавливаемся и возвращаемсяСтационарное состояние, и это подтверждает нашу следующую волну ходов. Идея уже ясна! Значит ли это, что мы можем пройти уже дваsetTimeoutсделать это?No, потому что расстояние между теорией и реальностью, как любовь.

   Рендеринг в браузере не происходит в одночасье — проблема в том, что "Непрерывное перемещение на небольшое расстояние за короткий промежуток времени», знайте, что в этом процессе вы должны подтвердить в режиме реального времени, прибыли ли выцелевое местоположение, то он будет включать чтение текущегоtranslateсместить и установить новыйtranslateработай. так частоDOMЧтение и запись неизбежно приведут к отставанию! мы все знаемJSПрямое управлениеDOMэто очень дорого! иначеVueне нужноVNode, правильно? Так как оптимизация процесса чтения и записи стала ключом к обеспечению плавности «анимации»!

  читатьПроблема легко решается, мы можем поддерживать значение состояния смещения внутри, любоеDOMсерединаtranslateИзменение значения должно быть сначала отражено в этом значении, аналогичноVueа такжеReactвиртуальныйDOMРоль дерева, но наша проще, просто отображение фактического смещения, чтобы текущее смещение не нужно было каждый раз считывать из фактического DOM.

  НапишитеПроцесс неизбежен, без измененийDOMПользователь не увидит никаких изменений, так почему бы не начать с анимации!

   Мы уже знаем, что черезtranslateСместить элемент по сравнению с Position +left/topметод, преимущество которого заключается в том, что браузер непереставлять. И в этом сценарии используйтеtranslate3dбудет только хуже, потому чтоJSЧасто меняйте это свойство, браузер должен каждый раз сравниватьxyzпреобразование по трем осям, принуждениеGPUУскорение, кажется, стало метафизическим. Поэтому, когда "бесшовная прокрутка" идет вдольXнаправлении, то лучший способ писать на самом делеtranslateX, по аналогииYНаправление осиtranslateY.

Время написания    является нашей главной задачей. Если вы хотите, чтобы пользователь видел непрерывное изображение, это означает, что каждый1000 / 60 msто есть16.67 msЭта запись делается один раз или около того. мы знаемsetTimeoutНа самом деле он не точен, он зависит от частоты обновления встроенных часов браузера, и он также сталкивается с проблемой этой асинхронной очереди, как и следующий фрагмент кода, который мы ожидаемsetTimeoutПечать через 3 секундыDone!, но на самом деле это занимает 10 секунд, он будет «заблокирован» процессом синхронизации!

// 期望 3 秒后打印 Done!
setTimeout(function () {
    console.log("Done!");
}, 1000 * 3);
// 这个同步进程需要 10s 才能从执行栈里推出,所以 10s 后才会打印 Done!
function waitSeconds(wait) {
    var start = Date.now();
    while (start + 1000 * wait > Date.now()) {}
}
waitSeconds(10);

благодаряrequestAnimationFrameСуществование этого API позволяет нам добиться плавной «бесшовной прокрутки» благодаря этой идее.

window.requestAnimationFrame()Сообщите браузеру, что вы хотите выполнить анимацию, и попросите браузер вызвать указанную функцию обратного вызова, чтобы обновить анимацию перед следующей перерисовкой. Этот метод должен передать функцию обратного вызова в качестве параметра, функция обратного вызова будет выполнена до следующей перерисовки браузера.

правильноtransformМодификация вызовет перерисовку, что означает, что мы можем сформировать непрерывный набор анимаций рекурсивным способом. Скопируйте следующий код в консоль браузера, чтобы испытать ощущение дрейфа страниц.

var target = 200
var offset = 0
function moveBody(){
	document.body.style.transform = `translateX(${++offset}px)`
	if(offset<target){
		requestAnimationFrame(moveBody)
	}
}
requestAnimationFrame(moveBody)

Таким образом,seamless-scrollбыл рожден. Есть больше деталей дизайна, например, как реализовать паузу и продолжить, как уведомить об изменении значения внешнего текущего индекса, как выяснить намерение жеста пользователя, если вы выбираете оптимальный путь перемещения, например, с 5-го экрана на 2-й экран, следуйте5,1,2Последовательный ход лучше, чем5,4,3,2порядок, потому что это то, что действительно создает визуально «бесшовный» эффект, а не движение назад. Если вам интересно, вы можете прочитать мойисходный код. Я также делал такие вещи, как добавлениеwill-changeПопытки оптимизировать свойства и т.д., но эффект не кажется очевидным. Критика и поправки от больших ребят приветствуются.Конечно, больше приветствуется пиар, особенно такой, который может значительно повысить производительность 😝. Далее краткое введение в использование этого плагина

Установить

npm i seamless-scroll
# 或者
yarn add seamless-scroll

быстрый старт

Рекомендуется обратиться к этомуDemoпроект, который включаетPCEnd + пример кода для мобильных устройств

Чтобы плагин работал лучше, структура DOM страницы должна быть установлена ​​в соответствии со следующими соглашениями:

<!-- 容器 -->
<div id="box">
  <!-- 列表 -->
  <ul>
    <!-- 子元素们 -->
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
  </ul>
  <!-- 此处可以添加“小圆点指示器”或“前进后退箭头”等 DOM 元素-->
</div>

Инициализация экземпляра с «бесшовной прокруткой» так же проста 🍳, и потрясающая 💯 карусель баннеров готова:

// 引入插件
import SeamlessScroll from 'seamless-scroll';

// 创建实例
const scroller = new SeamlessScroll({
  el: 'box',
  direction: 'left',
  width: 375,
  height: 175,
  autoPlay: false
});

// 用户点击“开始按钮”时,调用实例的 start 方法,开始播放
const startBtn = document.getElementById('start-btn');
startBtn.addEventListener('click', function() {
  scroller.start();
});

параметр

имя параметра иллюстрировать необязательное значение По умолчанию Необходимый
el контейнерный элемент. уже можно получитьDOMпредмет или элементid DOMElementилиString без да
direction направление прокрутки left, right, up, down left нет
width Ширина контейнера, в ед.px Number без да
height Высота контейнера, в ед.px Number без да
delay Время, проведенное на каждом экране, блокms Number 3000 нет
duration Время, необходимое для прокрутки одного экрана, ед.ms Number 300 нет
activeIndex Индекс отображаемого элемента по умолчанию в списке, начиная с0Начинать Number 0 нет
autoPlay Начинать ли воспроизведение автоматически, если установленоfalse, экземплярstartметод запуска вручную Boolean true нет
prevent Запрет прокрутки страницы, обычно используемый для вертикального воспроизведения, установлен наtrueКогда жест смахивания пользователя внутри компонента заставляет страницу прокручиваться вверх и вниз Boolean true нет
onChange Функция обратного вызова при переключении между экранами, входным параметром является индекс текущего экрана, который можно использовать для таких сценариев, как настройка индикатора маленькой точки Function без нет

метод экземпляра

start

При отсутствии автоматического воспроизведения вызовите этот метод, чтобы запустить воспроизведение вручную. можно вызвать только один раз, толькоautoPlayдляfalseИ использовать его, не запуская.

stop

Остановить игру.

continue

Возобновить воспроизведение. Сотрудничатьstopметод для использования.

go

Прокрутите непосредственно до позиции указателя или прокрутите один экран в определенном направлении. Вы можете использовать этот метод для быстрого перехода или переключения вперед и назад в бизнес-сценариях. Логика этого метода перехода заключается в выборе кратчайшего расстояния между целевым экраном и текущим экраном для смещения, например, от第5屏прибыть第2屏, последует5,1,2движется по порядку вместо5,4,3,2порядок, преимущество этого в том, что он действительно формирует визуальный "бесшовный" Эффект.

  • Пример:scroller.go(0)илиscroller.go('left')
  • Тип параметра:Numberилиleft, right, up, down

resize

Обновите ширину и высоту контейнера.

  • Пример:scroller.resize(375, 175) // width, height
  • Тип параметра:Number,единица измеренияpx

Например, следующий код сбрасывает ширину и высоту контейнера после отслеживания изменения размера окна браузера.

(function(vm) {
  var resizing,
    resizeTimer,
    requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;

  vm.resizeHandler = function() {
    if (!resizing) {
      // 第一次触发,停止 scroller 的滚动
      resizing = true;
      scroller.stop();
    }
    resizeTimer && clearTimeout(resizeTimer);
    resizeTimer = setTimeout(() => {
      // 停下来后,重设 scroller 的宽高,并继续之前的播放
      resizing = false;
      scroller.resize(document.body.clientWidth, 300);
      requestAnimationFrame(function() {
        scroller.continue();
      });
    }, 100);
  };
  window.addEventListener('resize', vm.resizeHandler);
})(this);

Не забудьте очистить слушателя при выходе со страницы! Ниже приведенVueизbeforeDestroyПример очистки прослушивателя смены окна в хуке

beforeDestroy(){
  window.removeEventListener('resize', this.resizeHandler);
}

destroy

Уничтожить экземпляр, восстановив стиль элемента по умолчанию

Ниже приведенReactизcomponentWillUnmountПример вызова метода в хуке:

componentWillUnmount(){
  this.scroller.destroy()
}

Суммировать

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