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

OSCHINA-MIRROR/CarGuo-GSYFlutterBook

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

Этот материал предназначен для анализа и понимания состава списков и прокрутки в Flutter, используя простой и доступный язык. Мы рассмотрим внутреннее устройство от обычного ListView до сложного NestedScrollView, чтобы помочь вам лучше понять и использовать прокручиваемые списки в Flutter.

Эта статья не обучает вас использованию API, а затрагивает важные темы, с которыми вы можете не сталкиваться каждый день при разработке.

Прокручиваемые списки в Flutter

Обычно прокручиваемый список в Flutter состоит из трёх основных частей:

  • Viewport: Это компонент типа MultiChildRenderObjectWidget. Он предоставляет "окно просмотра", то есть размер видимого списка;
  • Scrollable: Основная задача этого компонента — обработка жестов пользователя для реализации эффекта прокрутки, такие как VerticalDragGestureRecognizer и HorizontalDragGestureRecognizer.
  • Sliver: В точности это RenderSliver, который используется для размещения и рендера содержимого внутри Viewport.

На примере ListView:

Как показано на рисунке выше, процесс прокрутки ListView включает следующие части:

  • Зелёная область Viewport представляет собой окно просмотра списка;
  • Фиолетовая часть — это Scrollable, который обрабатывает жесты пользователя, позволяя желтому участку SliverList прокручиваться внутри Viewport;
  • Жёлтая область — это SliverList, когда мы прокручиваем список, фактически меняется его положение внутри Viewport.Поняв эту базовую концепцию, можно сказать, что обычно Viewport и Scrollable имеют универсальное применение. Поэтому, чтобы создать различные прокручиваемые списки в Flutter, используются различные Sliver для создания уникальной логики отображения.

Более точно говоря, это процесс выполнения метода performLayout для различных RenderSliver, где через SliverConstraints получается соответствующий SliverGeometry.

Поэтому в Flutter:

  • ListView использует SliverFixedExtentList или SliverList;
  • GridView использует SliverGrid;
  • PageView использует SliverFillViewport;

Единственным исключением является SingleChildScrollView, так как он представляет собой прокручиваемый компонент с одним потомком. Он не использует RenderSliver, а вместо этого имеет свой собственный RenderObject (RenderBox) и в процессе performLayout непосредственно изменяет offset своего потомка для достижения эффекта прокрутки.

Отрисовка Sliver

Как известно, общая схема отрисовки в Flutter выглядит следующим образом: WidgetElementRenderObjectLayer. А логика размещения и отрисовки в Flutter реализуется внутри RenderObject.

На самом деле, RenderObject также можно разделить на две основные базовые категории:

  • RenderBox: основные компоненты размещения используются на основе RenderBox;
  • RenderSliver: используется главным образом для размещения внутри Viewport, прямые потомки Viewport должны быть типа RenderSliver.И вот здесь может возникнуть вопрос: если в SingleChildScrollView используется RenderBox, а не RenderSliver, то почему всё же требуется использовать метод Viewport + RenderSliver для реализации скроллинга списка?

RenderBox

Внутри SingleChildScrollView используется RenderBox. В процессе размещения каждый раз будет производиться размещение и расчёт всего потомства child, при отрисовке используются такие параметры как offset и clip для выполнения эффекта перемещения. Такой подход становится менее эффективным, когда child имеет сложную структуру или слишком большой размер.

RenderSliver

Реализация RenderSliver более сложна по сравнению с RenderBox. Как уже упоминалось ранее, RenderSliver использует SliverConstraints для получения SliverGeometry. В частности:

  • В SliverConstraints есть параметр remainingPaintExtent, который указывает на оставшуюся область для отрисовки;

  • В SliverGeometry содержится множество параметров, таких как scrollExtent (расстояние для прокрутки), paintExtent (размер области для отрисовки), layoutExtent (диапазон размещения), visible (необходимость отображения).

С помощью этих параметров в Viewport можно динамически управлять ресурсами, определяя, какой участок экрана следует отрисовать, какие части ещё могут быть отрисованы и какие размещения требуются для загрузки.

Кратко говоря, это позволяет реализовать "ленивую" загрузку, отрисовывать только необходимое, что обеспечивает более плавный опыт прокрутки.image

Примером является ListView. На приведенной выше图为高度为701的ListView的实际布局渲染结果,对于SliverList输出的SliverGeometry而言:

image

Для SliverList выводимого SliverGeometry:

  • Высота каждого элемента в списке установлена в 114;
  • scrollExtent равен 2353, то есть общее расстояние прокрутки равно 2353;
  • paintExtent составляет 701, так как viewport списка ListView равен 701, поэтому значение remainingPaintExtent, полученное из SliverConstraints, также равно 701, поэтому по умолчанию требуется отрисовать и разместить только ту часть, высота которой составляет 701; (так как по умолчанию paintExtent равен layoutExtent)
  • Для дополнительной синей области элементов 8-9 это связано с тем, что внутри SliverConstraints существует параметр remainingCacheExtent, который представляет собой область, которую следует заранее закэшировать, то есть "предварительно размещаемую" область. По умолчанию размер этой области составляет defaultCacheExtent = 250.0;

ListView высота равна 701, defaultCacheExtent равен значению по умолчанию 250, то есть расстояние до нижней границы при первом отображении составляет 951. При высоте каждого элемента в 114 пикселей получается, что это примерно 8,3 элемента, округляемое до целого числа — 9 элементов. В результате общее размерное пространство, которое требуется обработать, равно 114 * 9 = 1026. Внутри SliverList это параметр endScrollOffset.Поэтому, исходя из вышеописанной ситуации, ListView будет выводить объект SliverGeometry, где paintExtent равен 701, а cacheExtent равен 1026.

Из этого примера можно заключить, что RenderSliver при реализации затрат и логики для прокручиваемых списков оказывается намного лучше и гибче, чем использование RenderBox, и именно поэтому в Viewport используется RenderSliver, а не RenderBox.

⚠️ Обратите внимание, здесь легко возникнуть заблуждение, что ListView состоит из Viewport + Scrollable и одного RenderSliver. Поэтому в ListView будет всего один RenderSliver, а не несколько. Чтобы использовать несколько RenderSliver, следует использовать CustomScrollView.

Наконец, немного о CustomScrollView: фактически это просто контроллер прокрутки с возможностью настройки массива RenderSliver, например:

  • Используя SliverList + SliverGrid, можно создать различные виды прокручиваемых списков;
  • Используя CupertinoSliverRefreshControl + SliverList, можно реализовать список с эффектом "погружения" как в iOS;

Другие доступные встроенные Sliver включают: SliverPadding, SliverFillRemaining, SliverFillViewport, SliverPersistentHeader, SliverAppBar и так далее.

NestedScrollView

Почему NestedScrollView рассматривается отдельно? Это связано с тем, что его реализация отличается от ранее рассмотренных прокручиваемых списков.

Внутреннее устройство

imageКак показано на вышестоящем рисунке, внутри NestedScrollView основное внимание уделяется наследованию CustomScrollView и использованию пользовательского NestedScrollViewViewport для достижения эффекта взаимодействия.

А что особенного в этом? Как показано ниже, это типичный способ использования NestedScrollView. Вы заметили какие-то особенности?

image

Код NestedScrollView имеет body, который представляет собой вложенный ListView. Мы уже говорили, что ListView сам является сочетанием Viewport + Scrollable + SliverList, а NestedScrollView также имеет свой NestedScrollViewViewport.Суть реализации NestedScrollView состоит из вложенного Viewport, где присутствуют два Scrollable. Вложенный ListView располагается внутри Sliver компонента NestedScrollView, что примерно можно представить следующей схемой.

В этом контексте есть несколько ключевых объектов:

  • SliverFillRemaining: используется для заполнения оставшегося пространства Viewport, то есть заполняет пространство за пределами header в NestedScrollView;
  • NestedScrollViewViewport: представляет собой расширенный Viewport с параметром SliverOverlapAbsorberHandle, который является ChangeNotifier и используется для отправки уведомлений при вызове метода markNeedsLayout.

Итак, NestedScrollView по своей сути представляет собой вложенность двух Viewport, но как они взаимодействуют друг с другом? Для этого существует объект _NestedScrollCoordinator.### _NestedScrollCoordinator

Реализация _NestedScrollCoordinator достаточно сложна. Кратко говоря, он создаёт два _NestedScrollController:

  • _outerController: контроллер для _NestedScrollViewCustomScrollView, то есть для самого себя;
  • _innerController: контроллер для body.

По умолчанию в родительском классе ScrollView для ListView используется PrimaryScrollController.of(context), так как PrimaryScrollController является InheritedWidget.

Процесс синхронизации скроллинга основывается на _NestedScrollCoordinator и созданных им двух _NestedScrollController:

  • _NestedScrollController использует _NestedScrollPosition вместо ScrollPosition;
  • _NestedScrollCoordinator объединяет _outer и _inner _NestedScrollController (_outer и _inner применяются соответственно к NestedScrollView и body);
  • _NestedScrollPosition передаёт события типа Drag обратно в _NestedScrollCoordinator.
  • Наконец, методы _NestedScrollCoordinator, такие как drag и applyUserOffset, используются для управления внутренним и внешним скроллингом.изображение

SliverPersistentHeader

После того как мы рассмотрели размещение и реализацию взаимосвязей в NestedScrollView, давайте кратко поговорим о SliverPersistentHeader. Этот компонент часто используется внутри NestedScrollView, а его основной компонент — SliverAppBar — на самом деле основан на SliverPersistentHeader.SliverPersistentHeader имеет два ключевых свойства: floating и pinned. Различие между ними заключается в использовании различных реализаций RenderSliver, но основное различие заключается в том, что они выдают различные значения SliverGeometry.изображение

Для примера можно сравнить первый _SliverFloatingPinnedPersistentHeader с последним _SliverScrollingPersistentHeader. В коде видно, что при использовании floating и pinned слотов, paintExtent и layoutExtent имеют минимальные значения.

изображение

Поэтому принцип фиксации Sliver заключается в том, чтобы Viewport получил положительные значения paintExtent и layoutExtent, что позволяет продолжать отрисовку содержимого этого Sliver.

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

изображение

Как показано на приведённом выше видео, элемент item0 не прекращает движение в orange области, а продолжает двигаться вверх, потому что список, являющийся body, не знает о наличии фиксированной области сверху.

Для решения этой проблемы можно использовать сочетание SliverOverlapAbsorber и SliverOverlapInjector:

  • Обернуть SliverPersistentHeader в SliverOverlapAbsorber, чтобы он мог абсорбировать высоту SliverPersistentHeader;

  • Использовать SliverOverlapInjector, чтобы передать эту высоту списку body, делая его осведомлённым о существовании фиксированной области сверху.изображение

Пример кода доступен здесь: https://github.com/CarGuo/gsy_flutter_demo/blob/master/lib/widget/sliver_list_demo_page.dart

Завершение описания реализации прокрутки списка в Flutter. Если у вас есть вопросы или замечания, пожалуйста, оставьте свои комментарии ниже.

Опубликовать ( 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