Корень страницы: скроллер, шапка, накладки
Правило структурное, а не про адаптивность: оно о том, что стоит на СТВОЛЕ страницы. Адаптивная вёрстка — в responsive.md .
Правило
Страница объявляет скроллер
одним
CustomScrollView
на стволе. Что липнет к потоку — сливер внутри него. Что лежит поверх —
Positioned
рядом со скроллером в том же
Stack
. Что меряет страницу (
ScrollData
) — стоит НАД скроллером.
ScrollData progress:@p // читает скроллер под собой
Stack clipBehavior:none // окно — только если есть накладки
CustomScrollView // скроллер страницы: ровно один, на стволе
PinnedHeaderSliver // шапка в потоке
$Header
Column crossAxisAlignment:stretch size:min
…секции… // бокс прямым ребёнком законен
Positioned top:0 left:0 right:0 height:3 // накладка
$Progress
Почему на сайте это не «лишний» элемент
На сайте документ прокручивается сам, поэтому страница без скроллера выглядит рабочей и живёт годами. В приложении этого нет: превью отдаёт корню жёсткие ограничения, и страница уходит в сплошной overflow — борд правки её не открывает вовсе. Одна и та же разметка обязана работать в обоих рендерах, и именно скроллер делает её таковой. Анализатор говорит об этом заранее — советом
page_without_scroller
.
Накладка — форма, а не параметр
Positioned
, лежащий в
Stack
рядом со скроллером страницы, прибивается к окну. Никакого слова для этого не нужно: во Flutter
Stack
берёт размер непозиционированного ребёнка — скроллвью, то есть окна, — поэтому позиционированный сосед стоит, пока содержимое едет. Веб читает ту же форму и эмитит
position: fixed
.
Positioned
в любом ДРУГОМ
Stack
остаётся со своим
Stack
и уезжает вместе с ним — в обоих рендерах одинаково. Это и есть разница, которую рисует форма: кладите слой туда, где имеете в виду.
Три ловушки
Сливер вне скроллвью роняет экран целиком
(
_RenderPinnedHeaderSliver is not a subtype of RenderBox
) — не «рисуется не так», а не открывается. Обратное безопасно: бокс прямым ребёнком
CustomScrollView
законен, скроллвью сам заворачивает его в
SliverToBoxAdapter
.
Клип на стволе прячет скроллер страницы.
У
Stack
по умолчанию
clipBehavior: hardEdge
, то есть
overflow: hidden
. Компилятор ищет скроллер спуском по стволу и снимает с него
overflow
, чтобы крутился документ, — но клип он не трогает намеренно: его поставил автор, и снятый клип вернул бы на страницу то, что прятали. Спуск на клипе прекращается, крутится вложенный элемент, и липкая шапка уезжает вместе со страницей. Отсюда
clipBehavior:none
у корневого
Stack
. Бокс с заданной
height
на стволе останавливает спуск так же.
Шапка в потоке занимает своё место.
Плавающая не занимала — значит, верхний отступ первой секции надо уменьшить ровно на её высоту. Высота обычно дробная (измеренные 81,5625 и 65,9375 px), поэтому совпадение будет с точностью до доли пикселя; подгонять распорку под метрику шрифта не стоит. И задавать её надо элементом (
SizedBox height:{…}
), а не
#padding
: объект отступов не читает переменную дерева.
ScrollData
стоит НАД скроллером
ScrollData
заводит собственный контроллер и раздаёт его поддереву, а
CustomScrollView
под ним этот контроллер берёт. Поставленный СОСЕДОМ скроллвью, он не видит ничего, и
progress
остаётся нулём — молча.