Фронтенду тоже нужно понимать физику — инерционная прокрутка статей

JavaScript анимация
Фронтенду тоже нужно понимать физику — инерционная прокрутка статей

Автор: Bumpman-Акридин

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

惯性滚动(Также известен как滚动回弹,momentum-based scrolling) впервые появился в системе iOS, ссылаясь наКогда пользователь прокручивает страницу на терминале, а затем убирает палец, страница не останавливается сразу, а продолжает прокручиваться в течение определенного периода времени, а скорость и продолжительность прокрутки пропорциональны интенсивности скользящего жеста.. В абстрактном понимании это похоже на скоростной поезд, который после торможения все равно пройдет определенное расстояние, прежде чем окончательно остановиться. А в системе iOS при прокрутке страницы вверх/вниз также можно вызвать эффект «отскока». Вот запись эффекта инерционной прокрутки собственного селектора времени iOS на странице WeChat APP [Billing]:

微信原生 date-picker

Студенты, знакомые с разработкой CSS, могут знать, что в браузере Safari есть такое правило CSS:

-webkit-overflow-scrolling: touch;

Когда его значение стиля равноtouch, браузер будет использовать эффект прокрутки с эффектом отскока, то есть «когда палец убирается с сенсорного экрана, содержимое будет продолжать прокручиваться в течение определенного периода времени». Кроме того, в богатой и красочной интерфейсной веб-экосистеме взаимодействие многих классических компонентов в определенной степени использует эффект инерционной прокрутки, как, например, в нескольких популярных библиотеках компонентов H5, упомянутых ниже.

Эффекты популярной библиотеки пользовательского интерфейса

Для удобства сравнения давайте сначала посмотрим на производительность прокрутки обычного длинного списка H5 в системе iOS (с включенным отскоком прокрутки):

iOS 下长列表滚动表现

  • weuiкомпонент выбора

    weui picker

    Очевидно, что инерционный эффект прокрутки селектора weui очень слабый, в основном прокрутка быстро останавливается после того, как рука убрана с экрана, и опыт не очень хороший.

  • vantкомпонент выбора

    vant picker

    Напротив, эффект инерционной прокрутки селектора ванта намного четче, но, поскольку отскок вверх/вниз по-прежнему поддерживает коэффициент или продолжительность нормальной прокрутки, общий эффект отскока немного разъединен.

Прикладные физические модели

惯性Этот термин происходит от закона инерции в физике (т.е.первый закон Ньютона): Когда на все объекты не действует сила, состояние движения не изменится, и это свойство, которым обладают объекты, называется инерцией. Можно предположить, что сущностью инерционной качки является инерционное явление в физике, поэтому мы можем правильно использовать滑块模型Описать весь процесс инерционной прокрутки.

Для удобства описания мы имитируем цель прокрутки в браузере с эффектом инерционной прокрутки (например, элементы страницы в браузере) как модель слайдера.滑块. Более того, анализ показывает, что весь процесс инерционной качки можно смоделировать как процесс скольжения (людьми) ползунка на определенное расстояние и последующего его отпускания, тогда весь процесс можно разобрать на следующие два этапа:

  • Первый этап,Сдвиньте ползунок, чтобы ускориться с места;

    滑块模型第一阶段

    На этом этапе ползунок подвергаетсяF拉больше, чемF摩Заставьте его ускоряться равномерно слева направо.

    Следует отметить, что для инерционной прокрутки браузера мы обычно ориентируемся на небольшой этап до того, как пользователь отпускает палец, а не на весь процесс прокрутки (весь процесс не имеет большого значения).Этот мгновенный этап может быть просто смоделировано.Сделайте для баланса силы ползункаравномерное ускорение.

  • вторая стадия,Отпустите ползунок, чтобы он продолжал скользить только под действием трения, пока, наконец, не остановится;

    滑块模型第二阶段

    На этом этапе ползунок подвергается только обратной силе трения, которая будет поддерживать направление движения слева направо для замедления, а затем для остановки.

На основе модели слайдера нам необходимо найти подходящие количественные показатели для построения системы расчета инерционной прокрутки. Сочетая модели и конкретные реализации, мы должны сосредоточиться на滚动距离,速度曲线а также滚动时长Эти ключевые показатели будут проанализированы один за другим ниже.

расстояние прокрутки

Для первой стадии модели скольжения ползунок совершает равномерное ускорение, мы также можем задать расстояние скольжения ползунка какs1, время скольженияt1, скорость критической точки (конечная скорость) в конце равнаv1, по формуле перемещения

位移公式

Соотношение скоростей может быть получено

第一阶段末速度

На втором этапе ползунок подвергается трениюF拉Чтобы сделать движение с равномерным замедлением, мы могли бы также установить расстояние скольжения, какs2, время скольженияt2, ускорение скольжения равноa, а начальная скоростьv1, конечная скорость0m/s, объединяя формулу перемещения и формулу ускорения

加速度公式

Расстояние скольжения можно рассчитатьs2

第二阶段滑动距离

Поскольку ускорение равномерного тормозного движения отрицательно (т.a < 0), может потребоваться установить константу ускоренияA, так что он удовлетворяетA = -2aотношения, то скользящее расстояние

第二阶段滑动距离和常量关系

Однако в реальном приложении браузераv1Вычисление квадрата приведет к тому, что окончательное вычисленное инерционное расстояние прокрутки будет слишком большим (то есть оно слишком чувствительно к силе жеста прокрутки), мы могли бы также удалить квадратную операцию:

两阶段关系

Итак, находим расстояние инерционной качки (т.е.s2), нам нужно только записать прокрутку пользователярасстояниеs1ивремя прокруткиt1, и установите подходящийконстанта ускоренияAВот и все.

После обширных испытаний постоянная ускоренияAПодходящее значение для0.003.

Кроме того, следует отметить, что для реального эффекта инерционной прокрутки браузера обсуждаемые здесь расстояние и продолжительность прокрутки относятся к расстоянию и продолжительности, которые могут воздействовать на диапазон инерционной прокрутки, а не ко всему процессу прокрутки элементов страницы пользователем. Подробнее см. в разделе [Условия запуска и остановки].

Кривая скорости прокрутки инерции

Для стадии инерционной прокатки, т. е. равномерного торможения на второй стадии, разность перемещений и временной интервал можно получить по формуле перемещенийTОтношение

位移差和时间关系

Выйти не сложно,При условии одного и того же временного интервала разность перемещений между двумя соседними участками будет становиться все меньше и меньше, иными словами, скорость нарастания смещения инерционной прокатки будет становиться все меньше и меньше.. Это то же самое, что CSS3transition-timing-functionсередина ease-outКривая скорости подходит очень хорошо,ease-out(которыйcubic-bezier(0, 0, .58, 1)), кривая Безье

ease-out 贝塞尔曲线

график изРисовать кривые Безье онлайн сайт.

Среди них вертикальная ось на диаграмме относится кПрогресс анимации, абсцисса относится квремя, координаты начала координат(0, 0), координаты конечной точки(1, 1), предполагая, что продолжительность анимации составляет 2 секунды,(1, 1)Координатная точка означает, что анимация завершена (100%) через 2 секунды после начала анимации. По диаграмме можно сделать вывод, что скорость продвижения процесса анимации замедляется с течением времени, что соответствует характеристикам равномерного замедления движения.

Попробуем практическое применениеease-outКривая скорости:

ease-out 曲线应用

Очевидно, что такая кривая скорости слишком линейна и плавна, и эффект замедления не очевиден. Мы повторяем тест со ссылкой на эффект отскока прокрутки iOS и настраиваем параметры кривой Безье какcubic-bezier(.17, .89, .45, 1):

调整后的贝塞尔曲线

Эффект после настройки кривой намного лучше:

调整后的曲线效果

отскок

Затем смоделируйте ситуацию, когда отскок срабатывает при касании границы контейнера во время инерционной прокрутки.

Смоделируем такой сценарий на основе модели ползуна: левый конец ползуна соединен с пружиной, а другой конец пружины закреплен на стене, при скольжении ползуна вправо, когда ползунок достигает критического момент (пружина вот-вот деформируется) и скорость еще не упала до0m/sПри ползун будет продолжать скользить и тянуть пружину, деформируя ее, и в то же время ползун будет тормозиться обратным натяжением пружины (кинетическая энергия преобразуется во внутреннюю энергию); когда скорость ползуна падает до0m/sВ это время деформация пружины в это время наибольшая.Благодаря упругим характеристикам пружина вернется в исходное состояние (внутренняя энергия преобразуется в кинетическую энергию), и потянет ползунок для перемещения в обратном ( левое) направление.

Точно так же процесс пружинения также можно разделить на следующие две стадии:

  • Ползунок тянет пружину вправо, чтобы сделать движение с переменным замедлением;

    回弹第一阶段模型

На этом этапе ползунок подвергается трениюF摩и увеличивая натяжение пружиныF弹Вместе ускорение увеличивается, а скорость уменьшается до0m/sвремени будет очень мало.

  • Пружина возвращается в исходное состояние, а ползунок оттягивается влево для выполнения ускорения, а затем торможения;

    回弹第二阶段模型

    Сила трения на ползунке на этом этапеF摩и меньшее и меньшее натяжение пружиныF弹компенсировать друг друга, только началоF弹 > F摩, ползунок совершает все меньшее и меньшее ускорение, затемF弹 < F摩, ползунок выполняет движение замедления с увеличивающимся ускорением, пока, наконец, не остановится. Здесь, чтобы облегчить фактическое вычисление, мы могли бы также предположить идеальное состояние:Пружина просто восстанавливает свою деформацию, когда ползунок находится в состоянии покоя..

расстояние отскока

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

回弹第一阶段位移

вaЭто переменная ускорения, которая пока не будет здесь обсуждаться. Тогда, согласно упругой модели физики, расстояние отскока второй ступени равно

回弹第二阶段位移

Исчисление здесь, его почти невозможно вычислить...

Однако мы можем соответствующим образом упростить в соответствии с моделью движения.S回弹расчет стоимости. так как回弹第二阶段的加速度больше, чем非回弹惯性滚动阶段的加速度(F弹 + F摩 > F摩), мы могли бы также установить общее расстояние ступени инерционной прокатки без отскока какS滑,Так

回弹距离关系

Следовательно, мы можем установить более разумную константуB, так что он удовлетворяет следующему уравнению:

回弹距离等式

После долгих тренировок постоянныйBРазумное значение для 10.

Кривая скорости отскока

Всю траекторию инерционного качения, запускающую отскок, можно разделить на три фазы движения:

触发回弹的运动轨迹

Однако, если стадияaи сценаbТочная визуализация, поскольку анимация CSS сложна:

  • сценаbТрудно точно описать движение переменного замедления в
  • Хотя эти два этапа движутся в одном и том же направлении, кривая скорости анимации непоследовательна, что может легко вызвать ошибку в работе пользователя;

Для упрощения процесса поставим сценуaиbОбъединенная в один этап движения, упрощенная траектория становится:

简化后的回弹运动轨迹

Дано на сценеaОбратное ускорение в конце будет становиться все больше и больше, поэтому скорость ползуна на этом этапе падает быстрее, чем при инерционной качке без отскока, и соответствующий конец кривой Безье будет круче. Выбираем более разумную кривуюcubic-bezier(.25, .46, .45, .94):

回弹阶段 a 曲线

для сценыb, ползунок сначала ускоряется, а затем замедляется, иease-in-outКривая чем-то похожа, попробуйте на практике:

ease-in-out 曲线实践

Присмотревшись, мы находим сценуaи сценаbСоединение недостаточно плавное, это связано сease-in-outВызвано ослаблением первой половины кривой. Поэтому, чтобы подчеркнуть эффект, мы решили изобразить только стадию замедления движения.bпоследний абзац. Кривая Безье с поправкой наcubic-bezier(.165, .84, .44, 1)

调整后的贝塞尔曲线

Практический эффект:

调整后的贝塞尔曲线实践

Из-за некоторых пропаданий кадров, вызванных преобразованием gif, образец эффекта будет выглядеть немного застрявшим, рекомендуется испытать его непосредственно.demo

Продолжительность CSS-анимации

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

  • Отскок не сработал

    Разумная продолжительность инерционной качки составляет2500ms.

  • вызвать отскок

    для сценыa,когдаS回弹определяется как превышающий определенный критический порогСильный отскок, продолжительность движения400ms; в противном случае он определяется какСлабый отскок, продолжительность движения800ms.

    И для сценыb, продолжительность отскока500msболее разумно.

условие старт-стоп

Как было сказано выше, очень неразумно включать в расчет весь процесс прокрутки пользователем элементов страницы. Нетрудно представить, что когда пользователь прокручивает элемент на относительно большое расстояние с очень медленной скоростью, импульс элемента в этом случае очень мал, и инерционная прокрутка не должна срабатывать. Поэтому срабатывание инерционной прокрутки условно.

  • начальное условие

    Для инициации инерционной качки требуется достаточный импульс. Мы можем просто думать, что когда пользователь прокручивает страницу на достаточно большое расстояние (более15px) и достаточно короткое время (менее300ms), может быть сгенерирована инерционная прокрутка. Переход на язык программирования, в последний разtouchmoveвремя запуска события иtouchendИнтервал времени между срабатыванием события меньше300msИ расстояние между ними больше, чем15pxСчитается, что можно запускать инерционную прокрутку.

  • время сделать паузу

    Когда инерционная прокрутка не закончилась (в том числе в процессе отскока), когда пользователь снова коснется прокручиваемого элемента, мы должны приостановить прокрутку элемента. Что касается реализации, нам нужно пройтиgetComputedStyleиgetPropertyValueспособ получить текущийtransform: matrix()Значение матрицы, масштабированное после извлечения горизонтального смещения элемента по оси Ytranslateпозиция.

образец кода

На основе vuejs предоставляются некоторые коды клавиш, к которым также можно получить доступ напрямую.codepen demoИспытайте эффект (полный код).

<html>
  <body>
    <div id="app"></div>
    <template id="tpl">
      <div
        ref="wrapper"
        @touchstart.prevent="onStart"
        @touchmove.prevent="onMove"
        @touchend.prevent="onEnd"
        @touchcancel.prevent="onEnd"
        @transitionend="onTransitionEnd">
        <ul ref="scroller" :style="scrollerStyle">
          <li v-for="item in list">{{item}}</li>
        </ul>
      </div>
    </template>
    <script>
      new Vue({
        el: '#app',
        template: '#tpl',
        computed: {
          list() {},
          scrollerStyle() {
            return {
              'transform': `translate3d(0, ${this.offsetY}px, 0)`,
              'transition-duration': `${this.duration}ms`,
              'transition-timing-function': this.bezier,
            };
          },
        },
        data() {
          return {
            minY: 0,
            maxY: 0,
            wrapperHeight: 0,
            duration: 0,
            bezier: 'linear',
            pointY: 0,                    // touchStart 手势 y 坐标
            startY: 0,                    // touchStart 元素 y 偏移值
            offsetY: 0,                   // 元素实时 y 偏移值
            startTime: 0,                 // 惯性滑动范围内的 startTime
            momentumStartY: 0,            // 惯性滑动范围内的 startY
            momentumTimeThreshold: 300,   // 惯性滑动的启动 时间阈值
            momentumYThreshold: 15,       // 惯性滑动的启动 距离阈值
            isStarted: false,             // start锁
          };
        },
        mounted() {
          this.$nextTick(() => {
            this.wrapperHeight = this.$refs.wrapper.getBoundingClientRect().height;
            this.minY = this.wrapperHeight - this.$refs.scroller.getBoundingClientRect().height;
          });
        },
        methods: {
          onStart(e) {
            const point = e.touches ? e.touches[0] : e;
            this.isStarted = true;
            this.duration = 0;
            this.stop();
            this.pointY = point.pageY;
            this.momentumStartY = this.startY = this.offsetY;
            this.startTime = new Date().getTime();
          },
          onMove(e) {
            if (!this.isStarted) return;
            const point = e.touches ? e.touches[0] : e;
            const deltaY = point.pageY - this.pointY;
            this.offsetY = Math.round(this.startY + deltaY);
            const now = new Date().getTime();
            // 记录在触发惯性滑动条件下的偏移值和时间
            if (now - this.startTime > this.momentumTimeThreshold) {
              this.momentumStartY = this.offsetY;
              this.startTime = now;
            }
          },
          onEnd(e) {
            if (!this.isStarted) return;
            this.isStarted = false;
            if (this.isNeedReset()) return;
            const absDeltaY = Math.abs(this.offsetY - this.momentumStartY);
            const duration = new Date().getTime() - this.startTime;
            // 启动惯性滑动
            if (duration < this.momentumTimeThreshold && absDeltaY > this.momentumYThreshold) {
              const momentum = this.momentum(this.offsetY, this.momentumStartY, duration);
              this.offsetY = Math.round(momentum.destination);
              this.duration = momentum.duration;
              this.bezier = momentum.bezier;
            }
          },
          onTransitionEnd() {
            this.isNeedReset();
          },
          momentum(current, start, duration) {
            const durationMap = {
              'noBounce': 2500,
              'weekBounce': 800,
              'strongBounce': 400,
            };
            const bezierMap = {
              'noBounce': 'cubic-bezier(.17, .89, .45, 1)',
              'weekBounce': 'cubic-bezier(.25, .46, .45, .94)',
              'strongBounce': 'cubic-bezier(.25, .46, .45, .94)',
            };
            let type = 'noBounce';
            // 惯性滑动加速度
            const deceleration = 0.003;
            // 回弹阻力
            const bounceRate = 10;
            // 强弱回弹的分割值
            const bounceThreshold = 300;
            // 回弹的最大限度
            const maxOverflowY = this.wrapperHeight / 6;
            let overflowY;

            const distance = current - start;
            const speed = 2 * Math.abs(distance) / duration;
            let destination = current + speed / deceleration * (distance < 0 ? -1 : 1);
            if (destination < this.minY) {
              overflowY = this.minY - destination;
              type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
              destination = Math.max(this.minY - maxOverflowY, this.minY - overflowY / bounceRate);
            } else if (destination > this.maxY) {
              overflowY = destination - this.maxY;
              type = overflowY > bounceThreshold ? 'strongBounce' : 'weekBounce';
              destination = Math.min(this.maxY + maxOverflowY, this.maxY + overflowY / bounceRate);
            }

            return {
              destination,
              duration: durationMap[type],
              bezier: bezierMap[type],
            };
          },
          // 超出边界时需要重置位置
          isNeedReset() {
            let offsetY;
            if (this.offsetY < this.minY) {
              offsetY = this.minY;
            } else if (this.offsetY > this.maxY) {
              offsetY = this.maxY;
            }
            if (typeof offsetY !== 'undefined') {
              this.offsetY = offsetY;
              this.duration = 500;
              this.bezier = 'cubic-bezier(.165, .84, .44, 1)';
              return true;
            }
            return false;
          },
          // 停止滚动
          stop() {
            const matrix = window.getComputedStyle(this.$refs.scroller).getPropertyValue('transform');
            this.offsetY = Math.round(+matrix.split(')')[0].split(', ')[5]);
          },
        },
      });
    </script>
  </body>
</html>

использованная литература


Добро пожаловать в блог Bump Labs:aotu.io

Или обратите внимание на официальный аккаунт AOTULabs и время от времени публикуйте статьи:

image