[Перевод] Правила компоновки во Flutter, которые должен знать каждый
Когда новичок во Flutter спрашивает, почему какой-то виджет с width: 100
не ширины 100 пикселей, обычно ему отвечают, что надо обернуть этот виджет в Center
, верно?
Не надо так делать
Если так отвечать, то к вам будут возвращаться снова и снова, спрашивая, почему какой-то FittedBox
не работает, почему этот Column
переполнен или как работает IntrinsicWidth
.
Сначала объясните, что Flutter компоновка очень отличается от HTML компоновки (особенно, если говорите с веб-разработчиком), а затем скажите, что необходимо запомнить следующее правило:
Ограничения для виджетов объявляются в родителях. Размеры (желаемые) задаются в самом виджете. Позиция виджета на экране устанавливается родителем
На мой взгляд, это правило нужно изучить, как можно раньше, так как без него по-настоящему понять компоновку во Flutter нельзя.
Более детально:
- Виджет получает свои ограничения от своего родителя. «Ограничение» — это всего 4 значения: минимальная и максимальная ширина, минимальная и максимальная высота.
- Затем виджет проходит по своему списку детей. Виджет сообщает своим дочерним элементам, каковы их ограничения (которые могут быть разными для каждого ребенка), а затем спрашивает каждого ребенка, какого размера он «хочет» быть.
- Затем виджет размещает свои дочерние элементы (горизонтально по оси x и вертикально по оси y) один за другим.
- И, наконец, виджет сообщает своему родителю о собственном размере (в пределах исходных ограничений, конечно).
Например, виджет, похожий на столбец с некоторыми отступами, хочет расположить два дочерних элемента:
Виджет: Родитель, каковы мои ограничения?Родитель: Ты должен быть от
90
до300
пикселей в ширину и от30
до85
в высоту.Виджет: Хм, так как я хочу иметь 5 пикселей отступа, то мои дети могут иметь не более
290
пикселей ширины и75
пикселей высоты.Виджет: Первый ребенок, вы должны быть от
0
до290
пикселей в ширину и от0
до75
в высоту.Первый ребенок: Хорошо, тогда я хочу быть
290
пикселей в ширину и20
пикселей в высоту.Виджет: Хм, поскольку я хочу поместить своего второго ребенка ниже первого, это оставляет только
55
пикселей высоты для моего второго ребенка.Виджет: Эй, второй ребенок, ты должен быть от
0
до290
в ширину и от0
до55
в высоту.Второй ребенок: Хорошо, я хочу быть
140
пикселей в ширину и30
пикселей в высоту.Виджет: Очень хорошо. Я расположу своего первого ребенка в точке
x: 5
иy: 5
, а второго — вx: 80
иy: 25
.Виджет: Родитель, я решил, что мой размер будет
300
пикселей в ширину и60
пикселей в высоту.
Ограничения
В результате вышеописанного правила механизм компоновки Flutter имеет несколько важных ограничений:
- Виджет может получить свой собственный размер только в пределах ограничений, заданных ему его родителем. Это означает, что виджет обычно не может иметь любой размер, какой он хочет.
- Виджет не может знать о своем положении на экране и не определяет его, так как это решает его родитель.
- Поскольку размер и положение родителя, в свою очередь, также зависит от его собственного родителя, невозможно точно определить размер и положение любого виджета, не принимая во внимание дерево в целом.
Примеры
Чтобы посмотреть, как работают примеры, можно:
Пример 1
Container(color: Colors.red)
«Экран» является родителем для Container
. Это заставляет красный Container
быть точно такого же размера, что и «экран».
Итак, Container
полностью заполняет видимую область, и та становится красной.
Примечание переводчика. У автора говорится, что «screen» определяет ограничения, но самом деле родителем виджета-примера является
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: double.infinity, height: double.infinity),
child: example
)
который задает минимально возможные ширину и высоту равными размерам экрана. Поэтому здесь и далее я пишу"экран"
, говоря о родительском виджетеConstrainedBox
.
Более подробная информация проConstrainedBox
будет ниже.
Примечание к примечанию. Если проверять приведенные примеры следующим образом
import 'package:flutter/material.dart';
void main() => runApp(ExampleTest());
class ExampleTest extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
color: Colors.red
);
}
}
то поведение соответствуют описанному автором.
Пример 2
Container(width: 100, height: 100, color: Colors.red)
Красный Container
хочет быть 100 × 100, но он не может, потому что «экран» приводит его к своим размерам.
Таким образом, Container
заполняет экран.
Пример 3
Center(
child: Container(width: 100, height: 100, color: Colors.red)
)
«Экран» приводит Center
к своему размеру. Таким образом, Center
заполняет «экран».Center
сообщает Container
, что тот может быть любого размера, но не больше «экрана». Теперь Container
действительно может быть 100 × 100.
Пример 4
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red),
)
В отличие от предыдущего примера здесь используется использует Align
вместо Center
.
Align
также говорит Container
, что он может быть любого размера, но если есть пустое место, оно не будет центрировать Container
, а вместо этого поместит его в правом нижнем углу доступного пространства.
Пример 5
Center(
child: Container(
color: Colors.red,
width: double.infinity,
height: double.infinity,
)
)
«Экран» приводит Center
к своему размеру. Таким образом, Center
заполняет всю видимую область.Center
сообщает Container
, что он может быть любого размера, но не больше «экрана». Container
хочет быть бесконечного размера, но так как он не может быть больше «экрана», он просто заполнит «экран».
Пример 6
Center(child: Container(color: Colors.red))
«Экран» приводит Center
к своему размеру. Таким образом, Center
заполняет всю видимую область.Center
сообщает Container
, что тот может быть любого размера, но не больше «экрана». Поскольку у Container
нет дочернего элемента и нет фиксированного размера, он решает, что хочет быть как можно больше, поэтому он заполняет весь «экран».
Но почему Container
так решил? Просто потому, что это дизайнерское решение тех, кто создал виджет Container
. Он мог бы быть создан по-другому, и вам действительно нужно прочитать документацию к Container
, чтобы понять, что он будет делать в зависимости от обстоятельств.
Пример 7
Center(
child: Container(
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
)
)
«Экран» приводит Center
к своему размеру. Таким образом, Center
заполняет весь «экран».Center
сообщает Container
, что тот может быть любого размера, но не больше «экрана». Поскольку у красного Container
нет размера, но есть дочерний виджет, то он решает, что хочет быть того же размера, что и его ребенок.
Красный Container
говорит своему дочернему виджету, что тот может быть любого размера, но не больше «экрана».
Ребенок оказывается зеленым Container
, который хочет быть размером 30 × 30. Как уже было сказано, красный Container
сам по себе будет соответствовать размеру его детей, поэтому он также будет 30 × 30. Красный цвет не будет виден, так как зеленый Container
будет занимать собой весь красный Container
.
Пример 8
Center(
child: Container(
color: Colors.red,
padding: const EdgeInsets.all(20.0),
child: Container(color: Colors.green, width: 30, height: 30),
)
)
Размер красного Container
будет соответствовать размеру своих детей, но в этот раз необходимо учесть собственные отступы (padding). Так что итоговый размер красного Container
будет 30 × 30 плюс отступы. Благодаря отступам красный цвет будет виден, а зеленый Container
будет того же размера, что и в предыдущем примере.
Пример 9
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
Вы могли бы предположить, что размер Container
должен быть от 70
до 150
пикселей, но это не так. ConstrainedBox
только накладывает дополнительные ограничения к тем, что она получила от своего родителя.
В данном случае «экран» приводит ConstrainedBox
к своему размеру, поэтому дочерний Container
также будет получит размеры «экрана», игнорируя полученные ограничения.
Пример 10
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
)
В данном случае Center
позволяет ConstrainedBox
быть любого размера, но не больше «экрана». Дочерний виджет ConstrainedBox
получит дополнительные ограничения от параметра constraints
.
Таким образом, размер Container
должен составлять от 70
до 150
пикселей. Он хочет иметь 10
пикселей в ширину и высоту, поэтому в конечном итоге у него будет (минимум) 70
.
Пример 11
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 1000, height: 1000),
)
)
В данном варианте Center
позволяет ConstrainedBox
быть любого размера, но не больше «экрана». Дочерний виджет ConstrainedBox
получит дополнительные ограничения от параметра constraints
.
Таким образом, размер Container
должен составлять от 70
до 150
пикселей. Он хочет иметь 1000
пикселей в ширину и высоту, поэтому в конечном итоге у него будет (максимум) 150
.
Пример 12
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 100, height: 100),
)
)
В таком варианте Center
позволяет ConstrainedBox
быть любого размера, но не больше «экрана». Дочерний виджет ConstrainedBox
получит дополнительные ограничения от параметра constraints
.
Таким образом, размер Container
должен составлять от 70
до 150
пикселей. Он хочет иметь 100
пикселей в ширину и высоту, и именно таким он будет, так как это значение между 70
и 150
.
Пример 13
UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
«Экран» приводит UnconstrainedBox
к своему размеру. Как бы то ни было, UnconstrainedBox
позволяет дочернему Container
быть желаемого размера, в данном случае 20 × 50.
Пример 14
UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
)
«Экран» приводит UnconstrainedBox
к своему размеру. Как бы то ни было, UnconstrainedBox
позволяет дочернему Container
быть желаемого размера (4000 × 50).
К сожалению, в таком случае у Container
ширина 4000
пикселей и он слишком велик, чтобы поместиться в UnconstrainedBox
, поэтому UnconstrainedBox
будет показывать очень страшное «предупреждение о переполнении» (... OVERFLOWED BY ... PIXELS
).
Пример 15
OverflowBox(
child: Center(child: Container(color: Colors.red, width: 4000, height: 50)),
)
«Экран» приводит OverflowBox
к своему размеру. Как бы то ни было, Center
внутри OverflowBox
позволяет дочернему Container
быть желаемого размера (4000 × 50).
OverflowBox
похож на UnconstrainedBox
, но разница в том, что он не будет отображать никаких предупреждений, если ребенок не помещается.
Container
имеет ширину 4000
пикселей и слишком велик, чтобы поместиться в OverflowBox
, но OverflowBox
просто покажет, что может, без каких-либо предупреждений.
Пример 16
UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
На экране ничего не отобразистя, и вы получите ошибку в консоли.
UnconstrainedBox
позволяет своему ребенку иметь любой размер, который он хочет, однако, у его дочернего виджета Container
ширина бесконечна (width: double.infinity
).
Flutter не может визуализировать бесконечные размеры, поэтому он выдаст ошибку со следующим сообщением: Box Constraints forces an infinite width
.
Пример 17
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
)
Здесь вы больше не получите ошибку. Да, LimitedBox
получает возможность быть сколько угодно большим от UnconstrainedBox
, но он передает своему потомку максимальную ширину 100
.
Обратите внимание, что если вы измените UnconstrainedBox
на Center
, то LimitedBox
больше не будет применять свои ограничения (так как они применяются только тогда, когда сам LimitedBox
получает «бесконечные» ограничения), и ширина Container
сможет быть больше 100
.
Это должно помочь понять разницу между LimitedBox
и ConstrainedBox
.
Пример 18
FittedBox(
child: Text('Some Example Text.', textDirection: TextDirection.ltr),
)
Примечание переводчика. Чтобы данный пример воспроизводился в качестве первого виджета в приложении, добавлено свойствоtextDirection: TextDirection.ltr
вText
, так как оно необходимо, если в дереве доText
не было виджета, определяющего направление текста. Если вы используетеMaterialApp
, тоText
не требует явного указанияtextDirection
.
«Экран» приводит FittedBox
к своему размеру. Text
будет иметь некоторую естественную ширину (также называемую его intrinsic width), которая зависит от количества текста, его размера шрифта и т.д.
FittedBox
позволит Text
иметь любой размер, который он хочет, но после того, как Text
сообщит свой размер FittedBox
, FittedBox будет масштабировать его до тех пор, пока не заполнит всю доступную ширину.
Пример 19
Center(
child: FittedBox(
child: Text('Some Example Text.', textDirection: TextDirection.ltr,),
)
)
Но что случится, если мы поместим FittedBox
в Center
? Center
позволит FittedBox
иметь любой размер, который она хочет, вплоть до размера экрана.
В итоге размер FittedBox
будет в соответствии с Text
, и Text
может быть любого размера, которого захочет. Поскольку и FittedBox
, и Text
имеют одинаковый размер, никакого масштабирования не произойдет.
Пример 20
Center(
child: FittedBox(
child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.', textDirection: TextDirection.ltr,),
)
)
Однако, что произойдет, если FittedBox
в Center
, но Text
слишком большой, чтобы поместиться на экране?
FittedBox
попытается стать таким же, как и Text
, но он не может быть больше «экрана». Поэтому он примет размер «экрана» и изменит размер Text
так, чтобы тот тоже влез в «экран».
Пример 21
Center(
child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.', textDirection: TextDirection.ltr,),
)
Однако, если убрать FittedBox
, то Text
получит максимальную ширину от «экрана» и затем перенесет текст на новую строку, чтобы влезть в «экран».
Пример 22
FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
)
)
Примечание. FittedBox
может масштабировать только виджет, который ограничен (имеет не бесконечную ширину и высоту). В противном случае он ничего не будет рендерить, и вы получите ошибку в консоли: RenderConstrainedBox object was given an infinite size during layout.
.
Пример 23
Row(textDirection: TextDirection.ltr, children: [
Container(
color: Colors.red,
child: Text(
'Hello!',
textDirection: TextDirection.ltr,
)),
Container(
color: Colors.green,
child: Text(
'Goodbye!',
textDirection: TextDirection.ltr,
)),
]);
«Экран» приводит Row
к своему размеру.
Точно так же, как UnconstrainedBox
, Row
не будет накладывать никаких ограничений на своих потомков, а вместо этого позволит им иметь любой размер, который захотят. Затем Row
поставит их рядом, а любое дополнительное пространство останется пустым.
Пример 24
Row(textDirection: TextDirection.ltr, children: [
Container(
color: Colors.red,
child: Text(
'This is a very long text that won’t fit the line.',
textDirection: TextDirection.ltr,
)),
Container(
color: Colors.green,
child: Text(
'Goodbye!',
textDirection: TextDirection.ltr,
)),
]);
Поскольку Row
не накладывает никаких ограничений на своих детей, вполне возможно, что дочерние элементы будут слишком большими, чтобы соответствовать доступной ширине строки. В этом случае, как и в случае с UnconstrainedBox
, Row
будет отображать «предупреждение о переполнении».
Пример 25
Row(textDirection: TextDirection.ltr, children: [
Expanded(
child: Container(
color: Colors.red,
child: Text(
'This is a very long text that won’t fit the line.',
textDirection: TextDirection.ltr,
))),
Container(
color: Colors.green,
child: Text(
'Goodbye!',
textDirection: TextDirection.ltr,
)),
])
Когда у Row
дочерний элемент обернут в Expanded
виджет, то Row
больше не позволяет этому дочернему элементу определять свою ширину.
Вместо этого он будет определять ширину Expanded
в соответствии с другими дочерними элементами, и затем Expanded
приводит ширину исходного дочернего элемента к своей.
Другими словами, как только вы используете Expanded
, исходная дочерняя ширина становится неважной и будет игнорироваться.
Пример 26
Row(textDirection: TextDirection.ltr, children: [
Expanded(
child: Container(
color: Colors.red,
child: Text(
'This is a very long text that won’t fit the line.',
textDirection: TextDirection.ltr,
))),
Expanded(
child: Container(
color: Colors.green,
child: Text(
'Goodbye!',
textDirection: TextDirection.ltr,
))),
])
Если у Row
все дочерние элементы обернуты в Expanded
, то каждый Expanded
будет иметь размер, пропорциональный его параметру flex
, и затем каждый Expanded
виджет приведет ширину дочернего элемента к своей.
Пример 27
Row(textDirection: TextDirection.ltr, children: [
Flexible(
child: Container(
color: Colors.red,
child: Text(
'This is a very long text that won’t fit the line.',
textDirection: TextDirection.ltr,
))),
Flexible(
child: Container(
color: Colors.green,
child: Text(
'Goodbye!',
textDirection: TextDirection.ltr,
))),
])
Единственное отличие, если вы используете Flexible
вместо Expanded
, заключается в том, что Flexible
позволяет своему дочернему элементу иметь ту же или меньшую ширину, чем у самого Flexible
, в то время как Expanded
заставляет дочерний элемент иметь точно такую же ширину, что и Expanded
.
Но и Expanded
, и Flexible
игнорируют ширину своих детей при определении своих размеров.
Обратите внимание. Выше сказанное означает, что невозможно получить размеры Flexible
/Expanded
пропорционально размерам соответствующих дочерних элементов у Row
. Row
будет либо использовать точные значения длины дочерних элементов, либо полностью игнорировать их при использовании Flexible
/Expanded
.
Пример 28
MaterialApp(
home: Scaffold(
body: Container(
color: Colors.blue,
child: Column(children: [
Text('Hello!'),
Text('Goodbye!'),
]))))
«Экран» приводит Scaffold
к своему размеру. Так что Scaffold
заполняет весь «экран».
Scaffold
говорит Container
, что он может быть любого размера, но не больше «экрана».
Примечание: когда виджет сообщает своему дочернему элементу, что он может быть меньше определенного размера, мы говорим, что виджет предоставляет «loose» (пер. свободные) ограничения своему дочернему элементу. Но об этом чуть позже.
Пример 29
MaterialApp(
home: Scaffold(
body: SizedBox.expand(
child: Container(
color: Colors.blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
)))))
Если мы хотим, чтобы ребенок у Scaffold
был точно такого же размера, как и сам Scaffold
, мы можем обернуть этот дочерний виджет в SizedBox.expand
.
Примечание: когда виджет сообщает своему дочернему элементу, что он должен иметь определенный размер, мы говорим, что виджет предоставляет «tight» (пер. жесткие) ограничения своему дочернему элементу.
Tight × Loose ограничения
Очень часто можно услышать, что некоторые ограничения являются «tight» или «loose», поэтому стоит знать, что это значит.
tight ограничение предполагает только точный размер. Другими словами, tight
ограничение определяет свою максимальную ширину, равную его минимальной ширине, и определяет свою максимальную высоту, равную его минимальной высоте.
Если вы перейдете к box.dart в репозитории Flutter и поищете конструктуры BoxConstraints
, то найдете следующее:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
Если вы вернетесь к Примеру 2, то увидите, что «экран» заставляет красный Container
быть точно такого же размера, как и «экран». «Экран» делает это, передавая tight
ограничения в Container
.
loose
ограничение, с другой стороны, устанавливает максимальную ширину/высоту, но позволяет виджету быть таким маленьким, каким он хочет. Другими словами, loose
ограничение определяет минимальную ширину/высоту равной нулю:
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
Если вы вернетесь к Примеру 3, то увидите, что Center
позволяет красному Container
быть меньше, но не больше «экрана». Center
делает так, передавая loose
ограничения контейнеру. В конечном счете, сама цель центра Center
в том, чтобы преобразовать tight
ограничения, которые он получил от своего родителя («экрана»), в loose
ограничения для своего потомка (Container
).
Изучение правил компоновки для конкретных виджетов
Знание общего правила компоновки необходимо, но его недостаточно.
Каждый виджет имеет большую свободу в рамках общего правила, поэтому нет никакого способа узнать, что будет делать виджет, просто прочитав его имя.
Если вы попытаетесь угадать, то, скорее всего, ошибетесь. Вы не можете точно знать, как будет вести себя виджет, если не прочитали его документацию или не изучили исходный код.
Исходный код обычно сложен, поэтому, вероятно, лучше просто прочитать документацию. Однако, если вы решили изучить код, то его легко найти, используя навигационные возможности ваших IDE.
Пример:
- Найдите какой-нибудь
Column
в своем коде и перейдите к его исходному коду (Ctrl-B
в IntelliJ). Откроетсяbasic.dart
файл. ПосколькуColumn
наследуетFlex
, то перейдите к исходному кодуFlex
(также вbasic.dart
). - Теперь прокрутите вниз, пока не найдете метод под названием
createRenderObject
. Как вы можете видеть, этот метод возвращаетRenderFlex
. Это соответствующий объект визуализации дляColumn
. Перейдите к исходному кодуRenderFlex
. Откроетсяflex.dart
. - Прокрутите вниз, пока не найдете метод
performLayout
. Это метод, который отвечает за компоновку дляColumn
.
Спасибо Simon Lightfoot за помощь в написании данной статьи.
Пакеты для компоновки за моим авторством
Еще мои Flutter пакеты
Обо мне
PS Всю критику, вопросы и предложения по переводу буду рад услышать в (личных) сообщениях.
PSS Ещё раз ссылка на оригинальную статью Flutter: The Advanced Layout Rule Even Beginners Must Know от Marcelo Glasberg