Автор: Bumpman-Акридин
В нашем обычном программировании и разработке, помимо внимания к таким факторам, как техническая реализация, алгоритмы и эффективность кода, мы должны гибко применять полученные предметные знания (такие как физика, теоретическая математика и т. д.). теория и практика дополняют друг друга, они неразделимы, что очень помогает нам в выборе программы и понимании технической практики. Сегодня давайте повторим физические знания средней школы и гибко применим их к реализации динамического эффекта инерционной прокрутки.
惯性滚动
(Также известен как滚动回弹
,momentum-based scrolling
) впервые появился в системе iOS, ссылаясь наКогда пользователь прокручивает страницу на терминале, а затем убирает палец, страница не останавливается сразу, а продолжает прокручиваться в течение определенного периода времени, а скорость и продолжительность прокрутки пропорциональны интенсивности скользящего жеста.. В абстрактном понимании это похоже на скоростной поезд, который после торможения все равно пройдет определенное расстояние, прежде чем окончательно остановиться. А в системе iOS при прокрутке страницы вверх/вниз также можно вызвать эффект «отскока». Вот запись эффекта инерционной прокрутки собственного селектора времени iOS на странице WeChat APP [Billing]:
Студенты, знакомые с разработкой CSS, могут знать, что в браузере Safari есть такое правило CSS:
-webkit-overflow-scrolling: touch;
Когда его значение стиля равноtouch
, браузер будет использовать эффект прокрутки с эффектом отскока, то есть «когда палец убирается с сенсорного экрана, содержимое будет продолжать прокручиваться в течение определенного периода времени». Кроме того, в богатой и красочной интерфейсной веб-экосистеме взаимодействие многих классических компонентов в определенной степени использует эффект инерционной прокрутки, как, например, в нескольких популярных библиотеках компонентов H5, упомянутых ниже.
Эффекты популярной библиотеки пользовательского интерфейса
Для удобства сравнения давайте сначала посмотрим на производительность прокрутки обычного длинного списка H5 в системе iOS (с включенным отскоком прокрутки):
-
weuiкомпонент выбора
Очевидно, что инерционный эффект прокрутки селектора weui очень слабый, в основном прокрутка быстро останавливается после того, как рука убрана с экрана, и опыт не очень хороший.
-
vantкомпонент выбора
Напротив, эффект инерционной прокрутки селектора ванта намного четче, но, поскольку отскок вверх/вниз по-прежнему поддерживает коэффициент или продолжительность нормальной прокрутки, общий эффект отскока немного разъединен.
Прикладные физические модели
惯性
Этот термин происходит от закона инерции в физике (т.е.первый закон Ньютона): Когда на все объекты не действует сила, состояние движения не изменится, и это свойство, которым обладают объекты, называется инерцией. Можно предположить, что сущностью инерционной качки является инерционное явление в физике, поэтому мы можем правильно использовать滑块模型
Описать весь процесс инерционной прокрутки.
Для удобства описания мы имитируем цель прокрутки в браузере с эффектом инерционной прокрутки (например, элементы страницы в браузере) как модель слайдера.滑块
. Более того, анализ показывает, что весь процесс инерционной качки можно смоделировать как процесс скольжения (людьми) ползунка на определенное расстояние и последующего его отпускания, тогда весь процесс можно разобрать на следующие два этапа:
-
Первый этап,Сдвиньте ползунок, чтобы ускориться с места;
На этом этапе ползунок подвергается
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)
), кривая Безье
график изРисовать кривые Безье онлайн сайт.
Среди них вертикальная ось на диаграмме относится кПрогресс анимации, абсцисса относится квремя, координаты начала координат(0, 0)
, координаты конечной точки(1, 1)
, предполагая, что продолжительность анимации составляет 2 секунды,(1, 1)
Координатная точка означает, что анимация завершена (100%) через 2 секунды после начала анимации. По диаграмме можно сделать вывод, что скорость продвижения процесса анимации замедляется с течением времени, что соответствует характеристикам равномерного замедления движения.
Попробуем практическое применение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)
:
для сценыb
, ползунок сначала ускоряется, а затем замедляется, и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 и время от времени публикуйте статьи: