1 В избранное 0 Ответвления 0

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

Присоединиться к Gitlife
Откройте для себя и примите участие в публичных проектах с открытым исходным кодом с участием более 10 миллионов разработчиков. Приватные репозитории также полностью бесплатны :)
Присоединиться бесплатно
Клонировать/Скачать
Flutter-18.md 29 КБ
Копировать Редактировать Web IDE Исходные данные Просмотреть построчно История
Отправлено 10.03.2025 00:06 5767d61

Как восемнадцатую статью серии, эта статья погрузит вас в мир Flutter с помощью ScrollPhysics и Simulation, открыв новое окно для управления скроллом в Flutter.

Ссылка на статью:

Полный набор статей по Flutter

Серия статей "Мир Flutter"

1. Введение

На следующем рисунке показана стандартная возможность прокрутки Widget в Flutter, которая отличается между платформами Android и iOS различной скоростью прокрутки и эффектом при оттягивании краёв. Это связано с тем, что на разных платформах используются различные значения по умолчанию для ScrollPhysics и Simulation. Мы постепенно рассмотрим реализацию этих двух ключевых компонентов, чтобы вы смогли достичь уровня мастерства в управлении прокруткой в мире Flutter.

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

2. ScrollPhysics

Первоначально рассмотрим ScrollPhysics, который, согласно официальной документации Flutter, определяет физические характеристики прокручиваемого контента. Вот основные типы:

  • AlwaysScrollableScrollPhysics: всегда позволяет прокрутку независимо от текущего состояния.
  • ClampingScrollPhysics: ограничивает прокрутку до границ содержимого.
  • BouncingScrollPhysics: обеспечивает эффект отскока при достижении границ прокрутки.
  • FixedExtentScrollPhysics: используется для прокрутки с фиксированной шириной шага.

Каждый из этих типов имеет свои особенности и может быть выбран в зависимости от требований вашего приложения.* 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.

2.1. 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: создаёт симулятор автоматической прокрутки.

Схема работы ScrollPhysics

Эти три метода вызываются в моментах _handleDragUpdate, _handleDragCancel и _handleDragEnd, то есть во время движения и его завершения:

  • applyPhysicsToUserOffset и applyBoundaryConditions вызываются в _handleDragUpdate.
  • createBallisticSimulation вызывается в _handleDragCancel и _handleDragEnd.

Поэтому основное различие между дефолтными реализациями BouncingScrollPhysics и ClampingScrollPhysics заключено именно в этих трёх методах.

3.1 applyPhysicsToUserOffset

Класс 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;

3.3 Создание модели движения (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` сразу быстро прокручивает до границы.**

![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image6)


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

## Четвертый раздел. Simulation

Как было сказано ранее, использование `Simulation` позволяет реализовать обработку скролла, демпфирования и отскока списка. Но как работает `Simulation`?

![](http://img.cdn.guoshuyu.cn/20190929_Flutter-18/image7)

Как показано на рисунке выше, **метод `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 )

Вы можете оставить комментарий после Вход в систему

1
https://api.gitlife.ru/oschina-mirror/CarGuo-GSYFlutterBook.git
git@api.gitlife.ru:oschina-mirror/CarGuo-GSYFlutterBook.git
oschina-mirror
CarGuo-GSYFlutterBook
CarGuo-GSYFlutterBook
master