Этот материал предназначен для анализа и понимания состава списков и прокрутки в Flutter, используя простой и доступный язык. Мы рассмотрим внутреннее устройство от обычного ListView
до сложного NestedScrollView
, чтобы помочь вам лучше понять и использовать прокручиваемые списки в Flutter.
Эта статья не обучает вас использованию API, а затрагивает важные темы, с которыми вы можете не сталкиваться каждый день при разработке.
Обычно прокручиваемый список в 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
своего потомка для достижения эффекта прокрутки.
Как известно, общая схема отрисовки в Flutter выглядит следующим образом: Widget → Element → RenderObject → Layer. А логика размещения и отрисовки в Flutter реализуется внутри RenderObject
.
На самом деле, RenderObject
также можно разделить на две основные базовые категории:
RenderBox
: основные компоненты размещения используются на основе RenderBox
;RenderSliver
: используется главным образом для размещения внутри Viewport
, прямые потомки Viewport
должны быть типа RenderSliver
.И вот здесь может возникнуть вопрос: если в SingleChildScrollView
используется RenderBox
, а не RenderSliver
, то почему всё же требуется использовать метод Viewport + RenderSliver
для реализации скроллинга списка?Внутри SingleChildScrollView
используется RenderBox
. В процессе размещения каждый раз будет производиться размещение и расчёт всего потомства child
, при отрисовке используются такие параметры как offset
и clip
для выполнения эффекта перемещения. Такой подход становится менее эффективным, когда child
имеет сложную структуру или слишком большой размер.
Реализация RenderSliver
более сложна по сравнению с RenderBox
. Как уже упоминалось ранее, RenderSliver
использует SliverConstraints
для получения SliverGeometry
. В частности:
В SliverConstraints
есть параметр remainingPaintExtent
, который указывает на оставшуюся область для отрисовки;
В SliverGeometry
содержится множество параметров, таких как scrollExtent
(расстояние для прокрутки), paintExtent
(размер области для отрисовки), layoutExtent
(диапазон размещения), visible
(необходимость отображения).
С помощью этих параметров в Viewport
можно динамически управлять ресурсами, определяя, какой участок экрана следует отрисовать, какие части ещё могут быть отрисованы и какие размещения требуются для загрузки.
Кратко говоря, это позволяет реализовать "ленивую" загрузку, отрисовывать только необходимое, что обеспечивает более плавный опыт прокрутки.
Примером является ListView
. На приведенной выше图为高度为701的ListView的实际布局渲染结果,对于SliverList输出的SliverGeometry而言:
Для SliverList выводимого SliverGeometry:
scrollExtent
равен 2353, то есть общее расстояние прокрутки равно 2353;paintExtent
составляет 701, так как viewport
списка ListView
равен 701, поэтому значение remainingPaintExtent
, полученное из SliverConstraints
, также равно 701, поэтому по умолчанию требуется отрисовать и разместить только ту часть, высота которой составляет 701; (так как по умолчанию paintExtent
равен layoutExtent
)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
основное внимание уделяется наследованию CustomScrollView
и использованию пользовательского NestedScrollViewViewport
для достижения эффекта взаимодействия.
А что особенного в этом? Как показано ниже, это типичный способ использования NestedScrollView
. Вы заметили какие-то особенности?
Код 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
, используются для управления внутренним и внешним скроллингом.После того как мы рассмотрели размещение и реализацию взаимосвязей в 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 )