Введение
В этом параграфе мы поговорим о лейауте во Flutter — это процесс при котором определяются размеры (Size
) и позиция виджетов на экране.
Также мы рассмотрим, что такое констрейнты (BoxConstraints
), посмотрим основные layout-виджеты, и разберёмся, с какими ошибками вы можете столкнуться во время вёрстки, почему они возникают, и как их решать.
Подход к лейауту во Flutter отличается от подходов в html/css
или нативной мобильной разработке. Сначала он может показаться странным и неудобным, но это дело привычки — если изучить основные принципы все становится понятным и легким.
Давайте рассмотрим небольшой пример:
class MyApp extends StatelessWidget {
static const appTitle = 'Flutter Handbook Demo';
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: Container(
color: Colors.red,
width: 100.0,
height: 100.0,
),
);
}
}
Мы добавляем виджет Container
, задаем ему красный фон и указываем размер 100 на 100.
Как вы думаете что будет выведено на экран?
Странно – почему-то виджет Container
получился размером не 100 на 100, а на весь экран.
Во Flutter есть специальный виджет SizedBox
– это виджет который задает размер для дочернего виджета (подробней о нем поговорим далее).
Давайте попробуем обернуть наш Container
в виджет SizedBox
размером 100 на 100 и посмотрим, что из этого получится:
class MyApp extends StatelessWidget {
static const appTitle = 'Flutter Handbook Demo';
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: SizedBox(
height: 100.0,
width: 100.0,
child: Container(
color: Colors.red,
width: 100.0,
height: 100.0,
),
),
);
}
}
Запускаем приложение и видим ту же самую проблему — виджет Container
вновь занимает весь экран.
А теперь давайте попробуем обернуть наш Container
в виджет Center
и посмотрим изменится ли что-нибудь:
class MyApp extends StatelessWidget {
static const appTitle = 'Flutter Handbook Demo';
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: Center(
child: Container(
color: Colors.red,
width: 100.0,
height: 100.0,
),
),
);
}
}
Как видим виджетContainer
наконец-то принял желаемый размер. Давайте разберёмся, почему так происходит.
Процесс лейаута во Flutter
Процесс лейаута во Flutter устроен следующим образом — при построении дерева виджетов от родителя к детям передаются констрейнты (BoxConstraints
) — это ограничения для размера (Size
) в границах которых ребенок может определить свой размер, родители в ответ получают размер (Size
) и позиционируют детей.
Таким образом для расчета лейаута производится два прохода — один проход вниз по дереву при котором передаются констрейнты (BoxConstraints
) и один проход вверх по дереву при котором возвращаются полученные размеры (Size
).
На самом деле есть исключения при которых происходит как меньше, так и больше проходов, но подробнее об этом поговорим далее
Что из себя представляют Size и BoxConstraints
Давайте разберемся что же из себя представляют Size
и BoxConstraints
.
Size
— класс который описывает размер, содержит два поля width
и height
.
BoxConstraints
— это класс который описывает возможные размеры виджета, а именно минимальную и максимальную ширину (параметры minWidth
и maxWidth
), а также минимальную и максимальную высоту (параметры minHeight
и maxHeight
).
Констрейнты можно разделить по строгости значений на:
- loose — когда минимальная ширина и высота равны нулю, а максимальная ширина и высота имеют конкретное значение, не равное нулю. Иными словами это гибкие констрейнты, которые предоставляют дочернему виджету выбор в определенных пределах (например при
BoxConstraints.loose(const Size(200.0, 300.0))
размеры виджета могут быть следующими0 <= width <= 200.0
и0 <= height <= 300.0
); - tight — когда минимальная ширина равна максимальной и минимальная высота равна максимальной высоте. Иными словами это негибкие констрейнты, которые заставляют виджет быть определенного размера (например при
BoxConstraints.tight(const Size(200.0, 300.0))
размеры виджета будут следующимиwidth = 200.0
иheight = 300.0
);
Стоит отметить, что констрейнты могут быть различными по ширине и высоте — например, loose по ширине и tight по высоте.
Также констрейнты можно разделить по пределу значений на:
- unbounded — когда максимальная ширина и/или максимальная высота равны бесконечности (
double.infinity
). Иными словами это гибкие констрейнты, которые определяют только минимальную ширину и/или высоту для дочернего виджета, не задавая при этом максимального предела (значенияSize
в таком случае будут находится в следующих пределах200.0 <= width <= ∞
и300.0 <= height <= ∞
). - bounded — когда максимальная ширина и максимальная высота не равны бесконечности(
double.infinity
).
А теперь давайте, рассмотрим что происходило в нашем примере с красным контейнером.
В первом примере Container
был растянут на весь экран — это произошло потому что виджет MaterialApp
отдает tight BoxConstraints
которые равны размеру экрана. Это особенность работы виджета MaterialApp
- он обязует виджет который мы передаем в качестве главного экрана home
быть на весь экран.
Во втором примере произошло то же самое — SizedBox
и вложенный в него Container
были растянуты на весь экран, а в примере с Center
наоборот сработало. Чтобы понять почему это произошло надо взглянуть на то какие значения BoxConstraints
были переданы контейнеру в каждом из случаев.
Главное правило лейаута — если какой-то из виджетов принимает неправильный размер, то первым делом стоит обратить внимание не на параметры этого виджета, а на родительский виджет и BoxConstraints
, которые он передаёт
Инспектируем BoxConstraints с помощью DevTools
Во Flutter есть инструмент, который позволяет инспектировать констрейнты виджетов приложения запущенного в debug
/ profile
режимах, — это DevTools.
Давайте рассмотрим наш пример с виджетом Center
и воспользуемся DevTools.
Запускаем DevTools, выбираем вкладку LayoutExplorer, затем в окне WidgetTree выбираем наш виджет Center
и видим следующее:
Виджет Center
получил от MaterialApp
tight-констрейнты размером 430 на 932 (констрейнты указаны в скобках w = 430.0
и h = 932.0
, ширина и высота определены строго, поэтому значения констрейнтов tight), сам Center
определил свой размер соответственно 430 на 932 (указано над скобками w = 430.0
и h = 932.0
).
Теперь давайте выберем виджет Container
и посмотрим какие у него констрейнты и размер:
Виджет Container
получил от Center
loose-констрейнты размером 0<=430 на 0<=932 (констрейнты указаны в скобках 0 <= w <= 430.0
и 0 <= h <= 932.0
).
Это означает, что Container
может быть любого размера в пределах данных значений: в результате размеры Container
определились как 100 на 100 (указано над скобками w = 100.0
и h = 100.0
), в соответствии с переданными width
и height
.
Рассмотрев пример с использованием DevTools, мы видим что действительно размер виджета определяется следующим образом в зависимости от констрейнтов:
- когда виджет получает tight-констрейнты, то его ширина и/или высота будут точно им соответствовать;
- когда виджет получает loose-констрейнты, то его ширина и/или высота будет в их диапазоне.
Определяем BoxConstraints с помощью виджета LayoutBuilder
Бывают случаи, когда нам необходимо знать констрейнты не во время дебага приложения, а в рантайме — например, в зависимости от текущих констрейнтов по-разному строить дерево виджетов. Для таких случаев нам не подойдет DevTools, и стоит воспользоваться LayoutBuilder
.
LayoutBuilder
— виджет, который вызывает специальный callback (builder
) на каждое изменение констрейнтов, при этом в качестве аргумента передается контекст и значение текущих констрейнтов. Результатом вызова builder
должен быть виджет который будет встроен в дерево виджетов.
Давайте взглянем на простой пример использования LayoutBuilder
– на каждый вызов выводим в консоль значение констрейнтов:
class MyApp extends StatelessWidget {
static const appTitle = 'Flutter Handbook Demo';
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: appTitle,
theme: ThemeData(
primarySwatch: Colors.blue,
),
debugShowCheckedModeBanner: false,
home: Center(
child: LayoutBuilder(builder: (context, constraints) {
print(constraints);
return Container(
color: Colors.red,
width: 100.0,
height: 100.0,
);
}),
),
);
}
}
При запуске приложения мы увидим, что на каждый build
в консоль будет выводиться следующее flutter: BoxConstraints(0.0<=w<=430.0, 0.0<=h<=932.0)
– это констрейнты которые мы получаем от родительского виджета Center
.
Знания о текущих констрейнтах обычно нам нужны для адаптивной верстки — когда в зависимости от констрейнтов мы хотим по-разному скомпоновать виджеты, например для portrait- и landscape-ориентации или для разных размеров экранов.
Для работы с portrait- и landscape-ориентациями есть специальный виджет OrientationBuilder
, который построен на базе LayoutBuilder
.
Основные layout-виджеты
Мы уже знаем что такое констрейнты и как работает расчет лейаута во Flutter, давайте рассмотрим как работают некоторые отдельные виджеты.
Align
Align
— виджет, который позволяет спозиционировать дочерний виджет относительно себя.
Если Align
имеет bounded-констрейнты, то он занимает максимально доступное место, если находится в unbounded-констрейнтах, то становится размером с дочерний виджет в тех направлениях в которых имеет unbounded значение констрейнта. У Align
есть два параметра widthFactor
и heightFactor
, которые позволяют задать размер Align
пропроционально размеру дочернего виджета, а также есть параметр alignment
(AlignmentGeometry
, Alignment
или AlignmentDirectional
), который определяет, где именно нужно спозиционировать дочерний виджет.
Расскажем о них подробнее.
AlignmentGeometry
— базовый абстрактный класс, у которого определены два поля x
и y
. Эти два поля задают положение дочернего виджета следующим образом
- (0.0, 0.0) — дочерний виджет будет по-центру;
- (-1.0, -1.0) — дочерний виджет будет в верхнем-левом углу;
- (1.0, 1.0) — дочерний виджет будет в нижнем-право углу.
Alignment
— класс, который позволяет задать основные позиции для дочернего виджета без учёта направления текста в приложении (в некоторых языках направление текста идет справа налево). Вот все возможные значения Alignment
(сперва выравнивание по вертикали, потом — по горизонтали) - bottomCenter
, bottomLeft
, bottomRight
, center
, centerLeft
, centerRight
, topCenter
, topLeft
, topRight
.
AlignmentDirectional
— класс, который позволяет задать основные позиции для дочернего виджета с учетом направления текста которое используется в приложении. Вот все возможные значения AlignmentDirectional
(сперва выравнивание по вертикали, потом — по горизонтали) - bottomCenter
, bottomEnd
, bottomStart
, center
, centerEnd
, centerStart
, topCenter
, topEnd
, topStart
.
Пример использования Align
— размещаем виджет сверху-справа:
home: Align(
alignment: Alignment.topRight,
child: Container(
color: Colors.red,
width: 100.0,
height: 100.0,
),
),
Center
Center
— виджет-обертка над Align
. Работает точно так же как Align
, но позиционирует дочерний виджет всегда по центру.
ConstrainedBox
ConstrainedBox
— виджет, который позволяет переопределять констрейнты для дочернего виджета, но только в рамках тех, что приходят от родителя.
При этом мы можем «сузить» констрейнты, т.е. не можем выходить за пределы внешних констрейнтов, например констрейнт 100≤размер≤200 мы можем переопределить как 125≤размер≤175, но не можем переопределить как 0≤размер≤300.
Пример использования ConstrainedBox
за пределами текущих констрейнтов — пытаемся переопределить констрейнты для Container
:
home: ConstrainedBox(
constraints: BoxConstraints.tight(Size(100, 100.0)),
child: Container(
color: Colors.red,
width: 100.0,
height: 100.0,
),
),
Как мы видим Container
все так же растягивается на весь экран т.к. мы не можем переопределить констрейнты которые не вписываются в текущие.
Пример использования ConstrainedBox
в пределах текущих констрейнтов — пытаемся переопределить констрейнты для Container
:
home: Center(
child: ConstrainedBox(
constraints: BoxConstraints.tight(Size(50.0, 50.0)),
child: Container(
color: Colors.red,
width: 100.0,
height: 100.0,
),
),
),
Как видим, Container
принимает размер 50х50 в соответствии с констрейнтами из ConstrainedBox
, несмотря на переданные в него параметры width
и height
.
UnconstrainedBox
UnconstrainedBox
— виджет, который убирает текущие констрейнты для дочернего виджета и позволяет дочернему виджету быть любого размера.
У UnconstrainedBox
есть уже знакомый нам параметр alignment
(AlignmentGeometry
, Alignment или AlignmentDirectional
), а также особый параметр constrainedAxis
(Axis
). С помощью constrainedAxis
можно задать конкретную ось, по которой будут игнорироваться констрейнты. По-умолчанию констрейнты будут игнорироваться по обоим осям.
UnconstrainedBox
используется довольно редко, об одном из примеров использования команда Flutter рассказала в следующем ролике:
Пример использования UnconstrainedBox
— убираем констрейнты для Container
:
home: UnconstrainedBox(
child: Container(
color: Colors.red,
width: 100.0,
height: 100.0,
),
),
Как мы видим Container
принимает желаемый размер, однако использование данного виджета может привести к ошибке — мы можем выйти за пределы доступных границ.
Пример использования UnconstrainedBox
— выход за пределы доступных границ:
home: UnconstrainedBox(
child: Container(
color: Colors.red,
width: 1000.0,
height: 1000.0,
),
),
В данном примере Container
также принимает желаемый размер, но при этом возникает ошибка т.к. виджет не умещается.
OverflowBox
OverflowBox
— виджет, который работает точно так же, как и UnconstrainedBox
, но не вызывает ошибку если дочерний виджет не умещается.
Большинство ошибок в верстке возникают именно при работе с unbounded-констрейнтами стоит особое внимание уделять виджетам которые отдают unbounded-констрейнты, а также изучить как работать с виджетами которые помогают работать с unbounded констрейнтами
FittedBox
FittedBox
— виджет, который позволяет «вписать» и расположить дочерний виджет в текущих границах.
FittedBox
часто используется с «негибкими» виджетами (например картинки или подобные виджеты), размер которых неизвестен заранее и не зависит от текущих констрейнтов, и мы хотим встроить их с учетом текущих границ.
FittedBox
стоит использовать, когда дочерний виджет больше или меньше границ в которых мы хотим его расположить. После стадии лейаута FittedBox
производит трансформацию чтобы вписать дочерний виджет особым образом.
Мы можем определить каким образом вписать дочерний виджет с помощью трех параметров alignment
(AlignmentGeometry
, Alignment
или AlignmentDirectional
), clipBehavior
(Clip
) и fit
(BoxFit
).
BoxFit
— может принимать одно из следующих значений (наглядные примеры можно увидеть в документации):
BoxFit.fill
— дочерний виджет полностью заполнит текущие границы, при этом пропорции при необходимости будут изменены;
BoxFit.contain
(дефолтное значение) — дочерний виджет полностью впишется в текущие границы, примет максимально возможный размер и при этом пропорции не будут изменены, по краям будет пустота если пропорции отличаются и передан соответствующий clipBehavior
;
BoxFit.cover
— дочерний виджет полностью заполнит текущие границы, примет минимально возможный размер и при этом пропорции не будут изменены, дочерний виджет будет “обрезан” по краям если пропорции отличаются и передан соответствующий clipBehavior
;
BoxFit.fitWidth
— дочерний виджет полностью заполнит текущие границы по ширине, при этом пропорции не будут изменены, дочерний виджет будет “обрезан” по краям если пропорции отличаются и передан соответствующий clipBehavior
;
BoxFit.fitHeight
— дочерний виджет полностью заполнит текущие границы по высоте, при этом пропорции не будут изменены, дочерний виджет будет “обрезан” по краям если пропорции отличаются и передан соответствующий clipBehavior
;
BoxFit.none
— не трансформирует размер дочернего виджета, при этом виджет может быть обрезан «по краям», если не умещается в текущих границах и передан соответствующий clipBehavior
;
BoxFit.scaleDown
— работает по сути так же, как и BoxFit.contain
, но допускает только уменьшение дочернего виджета.
Например, вместо использования OverflowBox
, чтобы избежать ошибки и полностью отобразить контент, мы можем воспользоваться виджетом FittedBox
.
home: FittedBox(
child: UnconstrainedBox(
child: Container(
color: Colors.red,
width: 1000.0,
height: 1000.0,
),
),
),
Как мы видим, контейнер полностью был вписан в текущие границы, никаких ошибок нет и его пропорции сохранены.
Flex, Column и Row
Flex
, Column
и Row
— виджеты, которые принимают список дочерних виджетов и размещают их в ряд один за другим в определённом направлении. Flex
— это базовый виджет, который может работать и как Column
, и как Row
. Column
и Row
— это на самом деле обёртки над Flex
.
Давайте рассмотрим работу с Flex
-виджетами на примере работы с Row
.
Row
отдаёт дочерним виджетам констрейнты, которые по высоте равны полученным от родителя, а по ширине unbounded.
Пример использования Row
:
home: Material(
color: Colors.white,
child: Center(
child: Row(
children: [
Container(
color: Colors.red,
child: const Text(
'Hello',
style: TextStyle(fontSize: 56),
),
),
Container(
color: Colors.blue,
child: const Text(
'Flutter',
style: TextStyle(fontSize: 56),
),
),
],
),
),
),
В примере выше нет никаких проблем, давайте попробуем увеличить количество текста:
home: Material(
color: Colors.white,
child: Center(
child: Row(
children: [
Container(
color: Colors.red,
child: const Text(
'Hello',
style: TextStyle(fontSize: 56),
),
),
Container(
color: Colors.blue,
child: const Text(
'Lorem ipsum is placeholder text commonly used in the graphic',
style: TextStyle(fontSize: 56),
),
),
],
),
),
),
Как мы видим, текст не уместился, и мы получили ошибку A RenderFlex overflowed by 3352 pixels on the right...
Дело в том, что Row
— это особый виджет с особой логикой размещения виджетов и разработчиками фреймворка Flutter было принято решение оставить ответственность за констрейнты и размеры (Size
) на разработчике.
Такое решение было принято потому, что фреймворк не может знать как именно вы хотите расположить виджеты и кому из них сколько выделить места. Например, если первому виджету передать всю доступную ширину, то он, возможно, займёт её целиком, и для остальных уже не останется места.
Таким образом все виджеты получают unbounded-констрейнты по ширине. Ответственность за то, что все виджеты уместятся — лежит на разработчике.
Чтобы разделить доступную ширину среди детей, есть три виджета — Flexible
, Expanded
и Spacer
.
const Flexible({
super.key,
this.flex = 1,
this.fit = FlexFit.loose,
required super.child,
});
flex
определяет вес среди всех детейfit
определяет констрейнты (loose или tight).
const Expanded({
super.key,
super.flex,
required super.child,
}) : super(fit: FlexFit.tight);
Expanded
наследуется от Flexible
и всегда имеет FlexFit.tight
const Spacer({super.key, this.flex = 1})
: assert(flex != null),
assert(flex > 0);
Spacer
это Expanded
у которого нет child
, позволяет заполнить пустотой оставшееся место
Чтобы исправить ошибку в нашем последнем примере, мы можем обернуть не уместившийся виджет в Expanded
, тогда первый виджет займёт место, равное своему размеру, а второй — всё оставшееся место.
Пример использования Row
c Expanded
:
home: Material(
color: Colors.white,
child: Center(
child: Row(
children: [
Container(
color: Colors.red,
child: const Text(
'Hello',
style: TextStyle(fontSize: 56),
),
),
Expanded(
child: Container(
color: Colors.blue,
child: const Text(
'Lorem ipsum is placeholder text commonly used in the graphic',
style: TextStyle(fontSize: 56),
),
),
),
],
),
),
),
Первый виджет Text
получил unbounded-констрейнты и занял столько места сколько ему нужно, а второй виджет Text
получил bounded-констрейнты равные оставшемуся свободному месту и занял его, при этом текст переносится на новую строку..
Если мы обернём оба виджета в Expanded
, то они оба получат bounded-констрейнты равные половине доступного места. Если мы хотим распределить место в других пропорциях, стоит воспользоваться параметром flex
, так если первому виджету передать значение flex
1, а второму 3 - то первый займет 25% места, а второй 75% (т.е. свободное место будет определяться как доля flex
текущего виджета от суммы всех flex
).
IntrinsicWidth и IntrinsicHeight
IntrinsicWidth
и IntrinsicHeight
— это специальные виджеты которые могут быть использованы в unbounded- и loose-констрейнтах, чтобы предотвратить «растягивание» виджета и определить максимальную ширину/высоту в зависимости от содержания виджетов.
Данные виджеты часто используются в Column
и Row
чтобы задать ширину/высоту для виджета равную максимальной из детей.
Пример использования Column
без IntrinsicWidth
:
Material(
color: Colors.white,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
color: Colors.red,
child: const Text(
'Hello',
style: TextStyle(fontSize: 56),
),
),
Container(
color: Colors.blue,
child: const Text(
'Flutter',
style: TextStyle(fontSize: 56),
),
),
Container(
color: Colors.green,
child: const Text(
'Handbook',
style: TextStyle(fontSize: 56),
),
),
],
),
),
),
В данном примере мы видим, что каждый Container
принимает ширину текста
Если нам необходимо выровнять всех детей Column
по самому широкому элементу, мы можем воспользоваться виджетом IntrinsicWidth
. Давайте обернём Column
в IntrinsicWidth
и добавим crossAxisAlignment: CrossAxisAlignment.stretch
.
CrossAxisAlignment.stretch
заставит всех детей принять ширину самого Column
, а IntrinsicWidth
предотвратит растягивание Column
на всю ширину экрана и заставит его принять ширину по максимальной ширине контента.
Пример использования Column
c IntrinsicWidth
:
home: Material(
color: Colors.white,
child: Center(
child: IntrinsicWidth(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Container(
color: Colors.red,
child: const Text(
'Hello',
style: TextStyle(fontSize: 56),
),
),
Container(
color: Colors.blue,
child: const Text(
'Flutter',
style: TextStyle(fontSize: 56),
),
),
Container(
color: Colors.green,
child: const Text(
'Handbook',
style: TextStyle(fontSize: 56),
),
),
],
),
),
),
),
Важное замечание! Не стоит использовать intrinsic-виджеты если можно обойтись без них. Данный виджет добавляет спекулятивную фазу расчета лейаута перед финальной, что делает расчет лейаута дороже (в худшем случае сложность может быть O(N²) от размера пересчитываемого поддерева)
Виджеты со скроллом и списки
К виджетам со скроллом и спискам относятся SingleChildScrollView
, ListView
и многие другие (более подробно работа со списками будет рассмотрена в следующих параграфах).
У этих виджетов особым образом определяется их размер и констрейнты для дочерних виджетов:
- они растягиваются по ширине и высоте на всё доступное место, поэтому их нельзя размещать в unbounded-констрейнтах;
- по направлению скрола такие виджеты отдают unbounded-констрейнты;
- ортогонально направлению скрола предоставляют tight-констрейнты.
Пример использования SingleChildScrollView
:
home: Material(
color: Colors.white,
child: SingleChildScrollView(
child: Column(
children: Colors.primaries
.map(
(color) => Container(
height: 200.0,
color: color,
),
)
.toList(),
),
),
),
Как мы видим SingleChildScrollView
занял весь экран, список из Container
скроллится и несмотря на то, что мы указали только height
, в Container
по ширине виджеты растянулись сами.
Давайте запустим LayoutExplorer выберем Column
и посмотрим какие в данном случае будут констрейнты.
Column
— получил от SingleChildScrollView
tight-констрейнты по ширине (w = 390.0
) и unbounded по высоте.
Container
— получил от Column
loose-констрейнты по ширине (0.0 <= w <= 390.0
) и unbounded по высоте.
По высоте виджет Container
определил свой размер согласно переданному значению height
, а по ширине растянулся на все доступное место - это стандартное поведение виджета Container
у которого нет дочернего виджета, в bounded-констрейнтах он занимает все свободное место, а в unbounded-констрейнтах становится минимально возможного размера.
- под констрейнтом понимается констрейнт по конкретной оси, по ширине или высоте;
- ширина или высота — это, соответсвенно, значение
width
иheight
переданное вContainer
.
Пример использования SingleChildScrollView
в Column
, в unbounded-констрейнтах
home: Material(
color: Colors.white,
child: Column(
children: [
SingleChildScrollView(
child: Column(
children: Colors.primaries
.map(
(color) => Container(
height: 200.0,
color: color,
),
)
.toList(),
),
),
Container(
height: 100.0,
color: Colors.grey,
),
],
),
),
В данном примере мы получили ошибку A RenderFlex overflowed by 2856 pixels on the bottom...
. Это происходит потому, что SingleChildScrollView
пытается занять всё свободное место, в то время когда мы его помещаем в unbounded-констрейнты — эту ошибку можно исправить если обернуть SingleChildScrollView
в Expanded
, в таком случае мы получим следующий результат:
LimitedBox
LimitedBox
— виджет, который позволяет ограничить размер дочернего виджета в случае, если текущие констрейнты — unbounded.
Это может быть полезным, когда мы хотим сделать адаптивный виджет, который может быть размещен и в bounded- и в unbounded-констрейнтах.
Например, мы могли воспользоваться LimitedBox
вместо Expanded
чтобы исправить ошибку и определить максимальный размер для SingleChildScrollView
если он расположен в unbounded-констрейнтах
home: Material(
color: Colors.white,
child: Column(
children: [
LimitedBox(
maxHeight: 500.0,
child: SingleChildScrollView(
child: Column(
children: Colors.primaries
.map(
(color) => Container(
height: 200.0,
color: color,
),
)
.toList(),
),
),
),
Container(
height: 200.0,
color: Colors.grey,
),
],
),
),
Stack
Stack
— виджет, который размещает дочерние виджеты относительно своих границ, то есть позволяет располагать виджеты один поверх другого. Stack
аналогично Flex
-виджетам обладает особой логикой размещения и констрейнты в Stack
работают по-разному для спозиционированных и неспоциционированных виджетов.
Чтобы спозиционировать виджет внутри Stack
используется виджет Positioned
(данный виджет обязательно должен использоваться внутри Stack
).
const Positioned({
super.key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
required super.child,
})
Positioned
— это виджет который управляет, где относительно самого Stack
будет спозиционирован child
.
left
, top
, right
, bottom
— отступы от краёв Stack
width
— ширина child
(одновременно могут быть заданы не более 2 значений из left
, right
, width
)
height
— высота child
(одновременно могут быть заданы не более 2 значений из top
, bottom
, height
)
Параметр с помощью которого управляются констрейнты называется fit
(StackFit
)
StackFit.loose
— всем виджетам передаются loose констрейнты с максимальной шириной и выстой равными ширине и высоте самого Stack
StackFit.expand
— неспозиционнированные виджеты получают tight-констрейнты равные максимально возможным, спозиционнированые получают unbounded- loose-констрейнты
StackFit.passthrough
- неспозиционнированные дети получают констрейнты которые Stack
получил от родителя, спозиционнированые получают loose констрейнты
StackFit.passthrough
часто используется, когда Stack
находится в Row
/Column
- например, если Stack
находится в Row
и обернут в Expanded
, он будет иметь tight-констрейнты по горизонтали и loose-констрейнты по вертикали.
Пример использования Stack
с StackFit.loose
home: Material(
color: Colors.white,
child: Center(
child: SizedBox(
width: 350.0,
height: 350.0,
child: Container(
color: Colors.black12,
child: Stack(
alignment: Alignment.center,
fit: StackFit.loose,
children: [
Container(
color: Colors.red,
height: 100.0,
width: 100.0,
),
Container(
color: Colors.blue,
height: 50.0,
width: 50.0,
),
Positioned(
bottom: 25.0,
child: Container(
color: Colors.green,
height: 100.0,
width: 100.0,
),
),
Positioned(
bottom: 50.0,
child: Container(
color: Colors.yellow,
height: 50.0,
width: 50.0,
),
),
],
),
),
),
),
),
Пример использования Stack
с StackFit.expand
Заключение
Итак, в этом параграфе мы ознакомились с основными принципами лейаута во Flutter и рассмотрели базовые layout-виджеты.
Конечно, это далеко не все виджеты, которые нам предоставляет фреймворк, но разобравшись с основными принципами и базовыми виджетами будет довольно просто разобраться с остальными.
Каждый раз, когда сталкиваетесь с новым виджетом, рекомендуем начинать с документации — команда Flutter проводит отличную работу по документированию, а также снимает короткие видеоролики в которых наглядно демонстрируется поведение и примеры использования различных компонентов Flutter.