Механика замедления в UIScrollView

Ilya Lobanov
Яндекс.Карты Mobile
5 min readMay 21, 2019
803 Designing Fluid Interfaces / 65 / WWDC18

Привет! В этой статье я расскажу о том, как устроен скролл в UIScrollView, какие формулы для этого используются и как самостоятельно реализовать подобную механику.

Знание того, как работает скролл, может пригодиться при реализации подобной анимации не для UIScrollView, а, например, для произвольной UIView с UIPanGestureRecognizer. Также эта механика использовалась в реализации скролла схемы в мультиплатформенной части Яндекс.Метро для iOS и Android.

Чтобы понять, как работает скролл, нам необходимо найти уравнение движения. Как только мы его найдем, мы сможем найти остальные формулы: для времени скролла и для поиска конечной позиции (проекции) скролла.

В статье про пейджинацию уже была упомянута формула для поиска конечной позиции скролла, показанная на презентации 803 Designing Fluid Interfaces / WWDC18.

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

Формула скорости

Для начала выведем все необходимые формулы, а затем опишем их в виде функций на языке Swift.

Для конфигурации скролла UIScrollView используется параметр .decelerationRate (коэффициент замедления). Он показывает, как сильно уменьшается скорость скролла с течением времени. При работе с UIScrollView важно иметь в виду, что параметры заданы не в секундах (как, например, в UIGestureRecognizer), а в миллисекундах. Поэтому .decelerationRate указывает то, во сколько раз изменится скорость за одну миллисекунду.

Поэтому если мы хотим использовать UIScrollView.DecelerationRate, то формула для нахождения скорости будет выглядеть так:

𝑣 — скорость,

𝑣ₒ — начальная скорость в pt/s (поинтов в секунду),

d — коэффициент замедления (0 < d < 1),

t — время.

Уравнение движения

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

x — положение точки,

xₒ — начальное положение точки.

В результате мы получим такую формулу:

log — натуральный логарифм.

Формула для нахождения конечной точки

Эта функция при t → ∞ стремится к своему конечному положению, так как d < 1:

X — конечное положение точки.

Итак, мы получили формулу для нахождения конечной точки, но она не совпадает с той, которая была предложена на WWDC. Если представить ее в наших обозначениях, учитывая начальное положение, то она будет выглядеть так:

Наши функции различаются в правых частях:

Для начала подставим в них значения .decelerationRate по умолчанию, которые даны в SDK:

Если подставить .normal = 0.998, то получим 499.5 и 499.0, а если подставить .fast = 0.99, то 99.5 и 99.0 соответственно.

Разница не слишком большая. Все потому, что правая часть является следствием разложения натурального логарифма в ряд Тейлора с использованием преобразования Эйлера. Если обратиться к WolframAlpha, то мы увидим, что функции ведут себя похожим образом при приближении к единице.

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

Формула для нахождения времени движения

В результате мы нашли уравнение движения и формулу для нахождения конечной точки. Но для создания анимации нам необходимо еще и время движения.

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

Теперь нам необходимо найти такое время T, при котором

Из этого уравнения получаем необходимое T:

В качестве значения ε можно использовать, например, 0.1 (0.1 поинта).

В результате мы вывели все необходимые формулы. Теперь осталось реализовать для них функции.

Реализация функций

Мы будем предполагать, что наши точки и скорость имеют тип CGPoint. Для удобства объединим все наши исходные параметры в структуру ScrollTimingParameters, переименовав ε в threshold:

В формулах мы использовали арифметические операции над векторами: сложение, вычитание и умножение и деление на число. Также мы использовали длину вектора. Всех этих функций для CGPoint в SDK нет, поэтому нам нужно добавить их самостоятельно:

Теперь у нас есть все необходимое, чтобы реализовать функции. Начнем с функции для нахождения конечной точки. В качестве примера будем использовать более точную и менее оптимальную формулу с использованием логарифма. Если вы будете уверены, что в вашем проекте значения decelerationRate будут всегда близки к единице, то можете заменить log(d) на (d — 1) / d, как было предложено на WWDC.

Затем реализуем функцию для времени движения:

Ну и наконец, функцию для вычисления промежуточных значений:

Итак, мы реализовали все необходимые функции для реализации скролла. И в итоге мы можем создать анимацию скролла с помощью TimerAnimation, которая вызывает переданный блок с текущим прогрессом:

Данные функции, реализациию TimerAnimation и пример использования в Playground’е можно найти на GitHub: https://github.com/super-ultra/ScrollTimingParameters

Однако в UIScrollView есть не только затухающий скролл, но и bounce: отскок от границ скролла. То, как он работает, мы рассмотрим в одной из следующих статей.

--

--