Как восемнадцатую статью серии, эта статья погрузит вас в мир Flutter с помощью ScrollPhysics и Simulation, открыв новое окно для управления скроллом в Flutter.
На следующем рисунке показана стандартная возможность прокрутки Widget
в Flutter, которая отличается между платформами Android и iOS различной скоростью прокрутки и эффектом при оттягивании краёв. Это связано с тем, что на разных платформах используются различные значения по умолчанию для ScrollPhysics
и Simulation
. Мы постепенно рассмотрим реализацию этих двух ключевых компонентов, чтобы вы смогли достичь уровня мастерства в управлении прокруткой в мире Flutter.
Дальше следует полезная информация, поэтому рекомендуется иметь при себе чай или кофе.
Первоначально рассмотрим ScrollPhysics
, который, согласно официальной документации Flutter, определяет физические характеристики прокручиваемого контента. Вот основные типы:
Каждый из этих типов имеет свои особенности и может быть выбран в зависимости от требований вашего приложения.* BouncingScrollPhysics
: позволяет прокрутке выходить за границы, после чего содержимое возвращается обратно благодаря эффекту отскока.
ClampingScrollPhysics
: ограничивает прокрутку так, чтобы она не выходила за границы.AlwaysScrollableScrollPhysics
: всегда реагирует на действия пользователя для прокрутки.NeverScrollableScrollPhysics
: никогда не реагирует на действия пользователя для прокрутки.В процессе разработки обычно используется следующий код для установки:CustomScrollView(physics: const BouncingScrollPhysics())
ListView.builder(physics: const AlwaysScrollableScrollPhysics())
GridView.count(physics: NeverScrollableScrollPhysics())
Однако обычно мы не явно задаём свойство physics
. Почему же тогда в ListView
, CustomScrollView
и других Scrollable
контролах в Flutter появляются различия в поведении прокрутки и оттягивания краёв между платформами Android и iOS?
Ключевой момент здесь заключается в использовании ScrollConfiguration
и ScrollBehavior
.
Как известно, все элементы прокрутки реагируют на касания через Scrollable
, обеспечивая движение. Как показано ниже в методе _updatePosition
класса Scrollable
, когда widget.physics == null
, значение _physics
по умолчанию получается с помощью метода getScrollPhysics(context)
объекта ScrollConfiguration.of(context)
, который возвращает объект типа ScrollBehavior
.
// Только вызывайте этот метод оттуда, где точно будет вызван rebuild.
void _updatePosition() {
_configuration = ScrollConfiguration.of(context);
_physics = _configuration.getScrollPhysics(context);
if (widget.physics != null)
_physics = widget.physics.applyTo(_physics);
final ScrollController controller = widget.controller;
final ScrollPosition oldPosition = position;
if (oldPosition != null) {
controller?.detach(oldPosition);
scheduleMicrotask(() => oldPosition.dispose());
}
_position = controller?.createScrollPosition(_physics, this, oldPosition)
?? ScrollPositionWithSingleContext(
physics: _physics,
context: this,
oldPosition: oldPosition);
assert(position != null);
controller?.attach(position);
}
```**Поэтому по умолчанию значение `ScrollPhysics` связано с `ScrollConfiguration` и `ScrollBehavior`.**
А как работает **`ScrollBehavior`**?
Изучив исходный код класса **`ScrollBehavior`**, можно заметить, что его метод `getScrollPhysics` по умолчанию реализует различные значения `ScrollPhysics` для разных платформ, поэтому по умолчанию поведение прокрутки и пограничной прокрутки может различаться на различных платформах:
```dart
ScrollPhysics getScrollPhysics(BuildContext context) {
switch (getPlatform(context)) {
case TargetPlatform.iOS:
return const BouncingScrollPhysics();
case TargetPlatform.android:
case TargetPlatform.fuchsia:
return const ClampingScrollPhysics();
}
return null;
}
Ранее было сказано, что ScrollPhysics
определяет физические характеристики скроллируемых компонентов. Как указано выше, на платформе Android эффект прокрутки за границы экрана (синий полукруг) возникает благодаря значению ScrollPhysics
. А когда и как ScrollConfiguration
и её ScrollBehavior
устанавливаются?
Просматривая исходный код класса ScrollConfiguration
, можно понять, что он является также типом InheritedWidget
, как и Theme
и Localizations
, следовательно, он передаётся с вершины вниз. Просматривая исходный код MaterialApp
, мы видим следующий код. Можно заметить, что ScrollConfiguration
встроен по умолчанию внутри MaterialApp
и через _MaterialScrollBehavior
установлено поведение ScrollBehavior
. Метод buildViewportChrome
, переопределённый в нём, реализует эффект полумесяца при прокрутке за границы экрана на платформе Android, где GlowingOverscrollIndicator
отвечает за отрисовку этого эффекта.```dart
@override
Widget build(BuildContext context) {
...
return ScrollConfiguration(
behavior: _MaterialScrollBehavior(),
child: result,
);
}
class _MaterialScrollBehavior extends ScrollBehavior { @override TargetPlatform getPlatform(BuildContext context) { return Theme.of(context).platform; }
@override Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) { switch (getPlatform(context)) { case TargetPlatform.iOS: return child; case TargetPlatform.android: case TargetPlatform.fuchsia: return GlowingOverscrollIndicator( child: child, axisDirection: axisDirection, color: Theme.of(context).accentColor, ); } return null; } }
Таким образом, мы можем понять, как конфигурируются `ScrollPhysics` для скроллируемых компонентов по умолчанию:
- 1. **`ScrollConfiguration` является `InheritedWidget`.**
- 2. **Внутри `MaterialApp` используется `ScrollConfiguration`, чтобы поделиться экземпляром подкласса `ScrollBehavior` — `_MaterialScrollBehavior`.**
- 3. **`ScrollBehavior` по умолчанию возвращает специальное поведение `BouncingScrollPhysics` и `ClampingScrollPhysics` в зависимости от платформы.**
- 4. **В `_MaterialScrollBehavior` реализован метод `buildViewportChrome`, который обеспечивает синий полумесяц при прокрутке за границы экрана на платформе Android.**
> Примечание: Мы можем создать свое собственное поведение `ScrollBehavior`, чтобы реализовать пользовательское поведение при прокрутке за границы экрана.
## Три. Как работает `ScrollPhysics`
Как **`ScrollPhysics` реализуют прокрутку и эффект прокрутки за границы экрана?** По умолчанию `ScrollPhysics` не имеют логики, но они предоставляют основные методы, такие как:```
/// [position] current position, [offset] distance the user scrolls
/// Converts the user's scroll distance offset into the number of pixels to move
double applyPhysicsToUserOffset(ScrollMetrics position, double offset)
##### Returns the overscroll value if it returns 0, then overscroll will always be equal to 0
##### Returns boundary conditions
`double applyBoundaryConditions(ScrollMetrics position, double value)`
##### Creates a simulation of ballistic scrolling
`Simulation createBallisticSimulation(ScrollMetrics position, double velocity)`
##### Minimum fling velocity
`double get minFlingVelocity`
##### Carries momentum, returns the velocity for subsequent scrolling
`double carriedMomentum(double existingVelocity)`
##### Minimum distance threshold for initiating dragging motion
`double get dragStartDistanceMotionThreshold`
##### Scroll simulation accuracy
##### Specifies a structure that defines what certain distances, durations of time and differences in velocities should be considered equivalent differences.
`Tolerance get tolerance`
Вышеуказанный код представляет описание методов класса ScrollPhysics
. В статье "Тринадцать. Полное понимание принципов прикосновения и прокрутки" мы подробно рассмотрели принципы прикосновения и прокрутки, начиная от момента прикосновения до выполнения прокрутки:
Работа
ScrollPhysics
также входит в этот процесс, и её основная логика заключена в трёх методах, выделенных красным цветом:
applyPhysicsToUserOffset
: преобразует отклонение пользователя offset
, используя физику, в увеличение setPixels
(прокрутка).applyBoundaryConditions
: вычисляет текущие граничные условия прокрутки, используя физику.createBallisticSimulation
: создаёт симулятор автоматической прокрутки.Эти три метода вызываются в моментах _handleDragUpdate
, _handleDragCancel
и _handleDragEnd
, то есть во время движения и его завершения:
applyPhysicsToUserOffset
и applyBoundaryConditions
вызываются в _handleDragUpdate
.createBallisticSimulation
вызывается в _handleDragCancel
и _handleDragEnd
.Поэтому основное различие между дефолтными реализациями BouncingScrollPhysics
и ClampingScrollPhysics
заключено именно в этих трёх методах.
Класс ClampingScrollPhysics
по умолчанию не переопределяет метод applyPhysicsToUserOffset
. Когда parent == null
, возвращаемое значение равно значению offset
пользователя:
double applyPhysicsToUserOffset(ScrollMetrics position, double offset) {
if (parent == null)
return offset;
return parent.applyPhysicsToUserOffset(position, offset);
}
Класс BouncingScrollPhysics
переопределяет метод applyPhysicsToUserOffset
, где пока пользователь не достигнет границы, возвращается стандартное значение offset
. Когда же пользователь достигает границы, применяется алгоритм для моделирования эффекта затухания при прокрутке за границу.```
/// Коэффициент трения double frictionFactor(double overscrollFraction) => 0.52 * math.pow(1 - overscrollFraction, 2);
@Override double applyPhysicsToUserOffset(ScrollMetrics position, double offset) { assert(offset != 0.0); assert(position.minScrollExtent <= position.maxScrollExtent);
if (!position.outOfRange) return offset;
final double overscrollPastStart = math.max(position.minScrollExtent - position.pixels, 0.0); final double overscrollPastEnd = math.max(position.pixels - position.maxScrollExtent, 0.0); final double overscrollPast = math.max(overscrollPastStart, overscrollPastEnd); final bool easing = (overscrollPastStart > 0.0 && offset < 0.0) || (overscrollPastEnd > 0.0 && offset > 0.0);
final double friction = easing // Применение меньшего сопротивления при плавной прокрутке за границу по сравнению с натяжением. ? frictionFactor((overscrollPast - offset.abs()) / position.viewportDimension) : frictionFactor(overscrollPast / position.viewportDimension); final double direction = offset.sign;
return direction * _applyFriction(overscrollPast, offset.abs(), friction); }
### 3.2. applyBoundaryConditions
Метод `applyBoundaryConditions` в классе `ClampingScrollPhysics` вычисляет значения условий границы таким образом, что **значение прокрутки вычитается из значений границ, чтобы получить противоположное значение, что позволяет сделать границы прокрутки относительно неподвижными, обеспечивая "заклинивание" прокрутки**, то есть **динамическое ограничение прокрутки**. Поэтому по умолчанию прокрутка на Android прекращается, когда она достигает границ.
@Override double applyBoundaryConditions(ScrollMetrics position, double value) { if (value < position.pixels && position.pixels <= position.minScrollExtent) // прокрутка меньше минимального значения return value - position.pixels; if (position.maxScrollExtent <= position.pixels && position.pixels < value) // прокрутка больше максимального значения return value - position.pixels; if (value < position.minScrollExtent && position.minScrollExtent < position.pixels) // достижение верхней границы return value - position.minScrollExtent; if (position.pixels < position.maxScrollExtent && position.maxScrollExtent < value) // достижение нижней границы return value - position.maxScrollExtent; return 0.0; }
> **Примечание:** Как уже было указано ранее, синий полукруг представляет значение по умолчанию метода `buildViewportChrome`, который реализован внутри `ScrollBehavior`.
Метод `applyBoundaryConditions` в классе `BouncingScrollPhysics` сразу возвращает 0, то есть значение 0 указывает на границу, а значения меньше 0 относятся к эффекту прокрутки за пределами границ.
```dart
@override
double applyBoundaryConditions(ScrollMetrics position, double value) => 0.0;
createBallisticSimulation
)Функция createBallisticSimulation
вызывается при событиях _handleDragCancel
и _handleDragEnd
, что происходит при прекращении взаимодействия пользователя. Когда createBallisticSimulation
возвращает null
, состояние Scrollable
переходит в IdleScrollActivity
, то есть в состояние остановки прокрутки.
На следующей диаграмме показан список прокрутки без использования модели движения (Simulation
). В этом случае прокрутка не будет продолжаться автоматически.
В методе createBallisticSimulation
класса ClampingScrollPhysics
используются две модели движения — ClampingScrollSimulation
(фиксированное) и ScrollSpringSimulation
(упругое), как показано ниже:
// Код метода createBallisticSimulation
Теоретически, упругое восстановление должно происходить только тогда, когда position.outOfRange
. Однако, поскольку ScrollPhysics
использует модель двойного представления родителей, её родительская модель может активировать position.outOfRange
, поэтому здесь используется ScrollSpringSimulation
для дополнительной проверки.Из следующего кода видно, что только если скорость (velocity
) больше чем дефолтное ускорение и находится в диапазоне скролла, возвращается модель движения ClampingScrollPhysics
. В противном случае возвращается null
, что приводит к состоянию Idle
остановки скролла. Именно поэтому медленное движение мыши не запускает автоматическую прокрутку.
@Override
Simulation createBallisticSimulation(
ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance;
if (position.outOfRange) {
double end;
if (position.pixels > position.maxScrollExtent)
end = position.maxScrollExtent;
if (position.pixels < position.minScrollExtent)
end = position.minScrollExtent;
assert(end != null);
return ScrollSpringSimulation(
spring,
position.pixels,
end,
Math.min(0.0, velocity),
tolerance: tolerance,
);
}
if (Math.abs(velocity) < tolerance.velocity) return null;
if (velocity > 0.0 && position.pixels >= position.maxScrollExtent)
return null;
if (velocity < 0.0 && position.pixels <= position.minScrollExtent)
return null;
return ClampingScrollSimulation(
position: position.pixels,
velocity: velocity,
tolerance: tolerance,
);
}
Метод createBallisticSimulation
класса BouncingScrollPhysics
проще, возвращает BouncingScrollSimulation
для моделирования скролла только в том случае, если начальная скорость при окончании касания больше по модулю, чем допустимое ускорение (tolerance.velocity
) или область прокрутки выходит за пределы допустимого диапазона. В противном случае состояние переходит в режим Idle
, когда скролл прекращается.```
@override
Simulation createBallisticSimulation(ScrollMetrics position, double velocity) {
final Tolerance tolerance = this.tolerance;
if (velocity.abs() >= tolerance.velocity || position.outOfRange) {
return BouncingScrollSimulation(
spring: spring,
position: position.pixels,
velocity: velocity * 0.91, // TODO(abarth): We should move this constant closer to the drag end.
leadingExtent: position.minScrollExtent,
trailingExtent: position.maxScrollExtent,
tolerance: tolerance,
);
}
return null;
}
Как видно, **при завершении касания, продолжение моделирования скролла зависит от скорости (`velocity`) и допустимого ускорения (`tolerance.velocity`). То есть скролл будет продолжаться только тогда, когда скорость превышает заданное ускорение**, а также эффекты `ClampingScrollSimulation` и `BouncingScrollSimulation` различаются в рамках допустимой области прокрутки.
На следующем рисунке показано, что **на первом экране `ScrollSpringSimulation` имеет замедление перед тем как остановиться; во втором же случае `ClampingScrollSimulation` сразу быстро прокручивает до границы.**

> **Фактически, выбор или настройка `Simulation` позволяет гибко настраивать скорость, демпфирование и эффекты отскока списка.**
## Четвертый раздел. Simulation
Как было сказано ранее, использование `Simulation` позволяет реализовать обработку скролла, демпфирования и отскока списка. Но как работает `Simulation`?

Как показано на рисунке выше, **метод `goBallistic` класса `ScrollPositionWithSingleContext` вызывает создание `Simulation`. Затем через `BallisticScrollActivity` происходит его выполнение.**
@override
void goBallistic(double velocity) {
assert(pixels != null);
final Simulation simulation = physics.createBallisticSimulation(this, velocity);
if (simulation != null) {
beginActivity(BallisticScrollActivity(this, simulation, context.vsync));
} else {
goIdle();
}
}
```В состоянии BallisticScrollActivity
`Simulation` используется для управления значением `value` контроллера анимации `AnimationController`, после чего в обратном вызове анимации значение `value`, полученное после вычисления `Simulation`, передается методом `setPixels(value)` для реализации прокрутки.
Здесь снова затрагиваются механизмы отрисовки анимации и сам механизм анимации, подробнее о которых будет рассказано в следующих разделах. Вкратце можно сказать, что когда приходит сигнал
vsync
системы при вызове методаdrawFrame
, выполняется метод_tick
внутри контроллера анимацииAnimationController
, что приводит к изменению значения_value = _simulation.x(elapsedInSeconds).clamp(lowerBound, upperBound);
и отправке уведомления о необходимости обновления с помощью методаnotifyListeners();
.
Детали внутренних вычислений Simulation
здесь не рассматриваются, но известно, что в случае ClampingScrollSimulation
коэффициент трения является постоянным, тогда как в случае BouncingScrollSimulation
коэффициент трения и вычисления зависят от передаваемого положения.
Здесь следует особое внимание уделить тому, почему BouncingScrollPhysics
автоматически возвращается к начальному состоянию?
На самом деле это заслуга BouncingScrollSimulation
: при создании BouncingScrollSimulation
передаются два параметра — leadingExtent: position.minScrollExtent
и trailingExtent: position.maxScrollExtent
. При недостаточной (underscroll
) и избыточной (overscroll
) прокрутке используются вычисления ScrollSpringSimulation
для создания анимированного эффекта возврата к leadingExtent
и trailingExtent
, что обеспечивает следующий результат:
Таким образом, анализ Flutter ScrollPhysics
и Simulation
завершен. Строго говоря, Simulation
относится к области анимации, однако здесь он рассматривается вместе с ScrollPhysics
.
Итог заключается в том, что ScrollPhysics
управляет преобразованием пользовательского взаимодействия и условиями границ, а также использует Simulation
для реализации автоматической прокрутки и анимированного эффекта возврата при прокрутке за пределы экрана.
Таким образом, восемнадцатый раздел наконец завершен! (///▽///)
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )