Layout class components will contain one or more sub-components. Layout class components are all directly or indirectly inherited SingleChildRenderObjectWidget
and MultiChildRenderObjectWidget
Widgets. They generally have an child
or children
property for receiving sub-Widgets.
Different layout components arrange (layout) subcomponents in different ways, as shown in the following table:
Widget | illustrate | use |
---|---|---|
LeafRenderObjectWidget |
Base class for non-container components | The leaf node of the Widget tree is used for widgets without child nodes, and usually the basic components belong to this category, such as Image . |
SingleChildRenderObjectWidget |
Single child component base class | Contains a child Widget, such as: ConstrainedBox , DecoratedBox , etc. |
MultiChildRenderObjectWidget |
Multi-child component base class | Contains multiple sub-Widgets, and generally has a children parameter that accepts an array of Widgets. Such as Row, Column, Stack, etc. |
Let's look at the inheritance relationship Widget > RenderObjectWidget > (Leaf/SingleChild/MultiChild) RenderObjectWidget
.
RenderObjectWidget
The creation and update methods are defined in the class RenderObject
, and subclasses must implement them. RenderObject
We only need to know that it is the object of the final layout and rendering UI interface. That is to say, for layout components, the layout algorithms are all It is RenderObject
realized through the corresponding object, so if you are interested in the principle of a layout class component, you can check its corresponding RenderObject
implementation, for example Stack
(cascade layout) the corresponding RenderObject
object is RenderStack
, and the implementation of the cascade layout is in RenderStack
.
Dimensional Constraint Class Layout
The size limit container is used to limit the size of the container. Flutter provides a variety of such containers, such as ConstrainedBox
, SizedBox
, UnconstrainedBox
, AspectRatio
and so on.
Box layout model
There are two layout models in Flutter:
- Layout based on
RenderBox
the box model. - Load list layouts on demand based on
Sliver
( ).RenderSliver
The details of the two layout methods are slightly different, but the general process is the same. The layout process is as follows:
- Upper-level components pass constraints (constraints) conditions to lower-level components.
- The lower component determines its own size and then tells the upper component. Note that the size of the underlying component must conform to the constraints of the parent component.
- The upper-level component determines the offset of the lower-level component relative to itself and determines its own size (in most cases, it determines its own size according to the size of the child component).
For example, the constraint passed from the parent component to the child component is " 最大宽高不能超过100,最小宽高为0
", if we set the width and height of the child component to be 200
, the final size of the child component is 100*100
, because at any time the child component must first abide by the constraints of the parent component, on this basis Then apply the sub-component constraints (equivalent to finding an intersection between the constraints of the parent component and its own size).
The box model layout component has two characteristics:
- The rendering objects corresponding to the components all inherit from the
RenderBox
class. - Constraint information passed from parent to child during layout is
BoxConstraints
described by .
BoxConstraints
BoxConstraints
It is the constraint information passed from the parent component to the child component during the layout of the box model. It is used to describe the available space range of the child component. It contains the minimum and maximum width and height information. The size of the child component needs to be within the constraint range. The default BoxConstraints
constructor as follows:
const BoxConstraints({
this.minWidth = 0.0, //最小宽度
this.maxWidth = double.infinity, //最大宽度
this.minHeight = 0.0, //最小高度
this.maxHeight = double.infinity //最大高度
})
It contains 4 attributes, BoxConstraints
and also defines some convenient constructors for quickly generating specific restriction rules, BoxConstraints
for example BoxConstraints.tight(Size size)
, it can generate a fixed width and height restriction; BoxConstraints.expand()
it can generate a container as large as possible to fill another container BoxConstraints
.
Convention: For the convenience of description, if we say that a component does not constrain its subcomponents or cancels the constraints on subcomponents, it means that the maximum width and height of the subcomponent constraints are infinite, while the minimum width and height are 0, which is equivalent to that subcomponents are completely ok Determine your own size based on the space you need.
ConstrainedBox
ConstrainedBox
Used to add additional constraints to child components. For example, if you want a child component to have a minimum height of 80 pixels, you can use const BoxConstraints(minHeight: 80.0)
constraints as a child component.
example
Let's define one first redBox
, which is a box with a red background color, without specifying its width and height:
Widget redBox = DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
);
We implement a red container with a minimum height of 50 and a width as large as possible .
ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity, //宽度尽可能大
minHeight: 50.0 //最小高度为50像素
),
child: Container(
height: 5.0,
child: redBox ,
),
)
Effect:
It can be seen that although we Container
set the height of _ to 5
pixels, it ends up being 50
pixels, which is why ConstrainedBox
the minimum height restriction of _ is in effect. If you Container
set the height to 80
pixels, then the final height of the red area will also be 80
pixels, because in this example, only the minimum heightConstrainedBox
is restricted , not the maximum height .
SizedBox
SizedBox
Used to specify a fixed width and height for child elements, such as:
SizedBox(
width: 80.0,
height: 80.0,
child: redBox
)
Effect:
In fact, SizedBox
it is just ConstrainedBox
a customization, the above code is equivalent to:
ConstrainedBox(
constraints: BoxConstraints.tightFor(width: 80.0,height: 80.0),
child: redBox,
)
which is BoxConstraints.tightFor(width: 80.0,height: 80.0)
equivalent to:
BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0)
In fact ConstrainedBox
, both and SizedBox
are RenderConstrainedBox
rendered through, we can see ConstrainedBox
that the methods SizedBox
of and createRenderObject()
return an RenderConstrainedBox
object:
RenderConstrainedBox createRenderObject(BuildContext context) {
return RenderConstrainedBox(
additionalConstraints: ...,
);
}
multiple restrictions
If a component has multiple parent ConstrainedBox
restrictions, which one will take effect? Let's look at an example:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0), //父
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
child: redBox,
),
)
Above we have two fathers and sons ConstrainedBox
, their constraints are different, and the running effect:
the final display effect is 90 in width and 60 in height, that is to say, the child ConstrainedBox
takes minWidth
effect, minHeight
but the parent ConstrainedBox
takes effect. Based on this example alone, we can't conclude any rules. Let's change the parent-child constraint in the above example:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 60.0),
child: redBox,
)
)
running result:
The final display effect is still 90, and the height is 60. The effect is the same, but the meaning is different, because minWidth
it is the parent that takes effect at this time ConstrainedBox
, minHeight
but the child ConstrainedBox
.
Through the above example, we found that when there are multiple restrictions , for minWidth
and minHeight
, the corresponding larger value in the parent and child is taken . In fact, this is the only way to ensure that parent constraints do not conflict with child constraints.
UnconstrainedBox
Although subcomponents must abide by the constraints of their parent components at any time, the prerequisite is that they must be in a parent-child relationship. If there is a component A
whose subcomponents are B
and B
whose subcomponents are C
, then the constraints C
must be obeyed B
, and B
must be A
obeyed Constraints, but A
the constraints of will not be directly constrained C
unless B
passedA
through to its own constraints C
. Using this principle, you can implement a B
component like this:
B
C
There are no constraints in the layout of the componentC
(it can be infinite).C
Determine its own size according to its real space occupation.B
A
Determine its own size in combination with the size of the subcomponents under the premise of observing the constraints.
And this B
component is UnconstrainedBox
the component, which means that UnconstrainedBox
the subcomponents of will no longer be constrained, and the size depends entirely on itself . In general, we will rarely use this component directly, but it may be helpful when "removing" multiple restrictions . Let's look at the following code:
ConstrainedBox(
constraints: BoxConstraints(minWidth: 60.0, minHeight: 100.0), //父
child: UnconstrainedBox( //“去除”父级限制
child: ConstrainedBox(
constraints: BoxConstraints(minWidth: 90.0, minHeight: 20.0),//子
child: redBox,
),
)
)
In the above code, if there is no middle one UnconstrainedBox
, then according to the multiple restriction rules described above, a 90×100
red box will eventually be displayed. But since the parent's restriction is UnconstrainedBox
" removed " ConstrainedBox
, it will eventually be ConstrainedBox
drawn according to the child's restriction redBox
, that is 90×20
, as shown in the figure:
However, please note that UnconstrainedBox
the "removal" of the parent component's restrictions is not a real removal: in the above example, although the size of the red area is 90×20
, 80
there is still an . That is to say, the restriction of the parent minHeight(100.0)
is still in effect, but it does not affect redBox
the size of the final child element, but it still occupies the corresponding space. It can be considered that the parent at this time ConstrainedBox
acts on the child UnconstrainedBox
and redBox
is only ConstrainedBox
limited by the child. This point Please pay attention.
So is there any way to completely remove the parent ConstrainedBox
restriction? the answer is negative! Please keep in mind that at any time a child component must abide by the constraints of its parent component , so when defining a generic component, if you want to specify constraints on the child component, you must pay attention, because once the constraints are specified, the child component itself cannot violate constraint.
In actual development, when we find that we have used SizedBox
or ConstrainedBox
specified a fixed width and height for the child element, but it still has no effect, we can almost conclude that the constraint has been specified by the parent component !
For example, AppBar
in the right menu of the (navigation bar) in the Material component library, we use SizedBox
the specified size of the loading button, the code is as follows:
AppBar(
title: Text(title),
actions: <Widget>[
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
)
],
)
running result:
We will find that the size of the loading button on the right has not changed! This is precisely because the constraints of the button AppBar
have been specified in actions
, so if we want to customize the size of the loading button, we must UnconstrainedBox
" remove " the constraints of the parent element, the code is as follows:
AppBar(
title: Text(title),
actions: <Widget>[
UnconstrainedBox(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation(Colors.white70),
),
),
)
],
)
running result:
You can see it works! In fact, it is also possible to UnconstrainedBox
replace Center
with or .Align
In addition, it should be noted that UnconstrainedBox
although the constraints can be canceled during the layout of its subcomponents (subcomponents can be infinite), but UnconstrainedBox
itself is constrained by its parent component, so when UnconstrainedBox
its subcomponents become larger, if UnconstrainedBox
the size exceeds it When the parent component is constrained, it will also cause an overflow error, such as:
Column(
children: <Widget>[
UnconstrainedBox(
alignment: Alignment.topLeft,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(children: [Text('xx' * 30)]),
),
),
]
running result:
The text has exceeded the width of the screen, overflowing.
AspectRatio
AspectRatio
The function is to adjust the aspect ratio of the child element child .
AspectRatio
First, it will expand as much as possible within the scope allowed by the layout constraints. The height of the widget is determined by the width and ratio, similar to the one in the BoxFit
middle contain
, and try to occupy the area as much as possible according to the fixed ratio.
If a feasible size cannot be found after satisfying all constraints, AspectRatio
it will eventually try to fit the layout constraints first, regardless of the set ratio.
Attributes | illustrate |
---|---|
aspectRatio |
The aspect ratio may not be laid out according to this value in the end. It depends on comprehensive factors. Whether the outer layer is allowed to be laid out according to this ratio is just a reference value |
child |
Subassembly |
Example:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key}) : super(key: key);
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(title: const Text("Flutter App")),
body: const LayoutDemo(),
),
);
}
}
// 需求:页面上显示一个容器,宽度是屏幕的宽度,高度是容器宽度的一半
class LayoutDemo extends StatelessWidget {
const LayoutDemo({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 2/1,
child: Container(
color: Colors.red,
),
);
}
}
LimitedBox
LimitedBox
It is used to specify child
the maximum width and height. It can be child
limited to the maximum width and height set by it, but this limitation is conditional. When LimitedBox
it is not constrained by the parent component, its size is limited. What is not constrained by the parent component? Most components will constrain the subcomponents, and the parent components without constraints include ListView
, Row
, and Column
so on. If LimitedBox
the parent component is constrained, LimitedBox
nothing will be done at this time, we can think that there is no such component.
For example the following code:
Widget build(BuildContext context) {
return Center(
child: LimitedBox(
maxWidth: 100,
maxHeight: 50,
child: Container(color: Colors.red, child: Text("ss"*50)),
),
);
}
Text
Components are not constrained 100x50
within the region as we would like, because Center
components impose LimitedBox
constraints on them, so we can use the UnconstrainedBox
removed LimitedBox
parent constraints:
Widget build(BuildContext context) {
return Center(
child: UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
maxHeight: 50,
child: Container(color: Colors.red, child: Text("ss"*50)),
)
),
);
}
At this time, the effect is as follows:
If we ListView
add the component directly in Container
, as follows:
ListView(
children: <Widget>[
Container(color: Colors.green, ),
Container(color: Colors.red, ),
],
)
At this point you'll find nothing, because the size will be set when the container is unconstrained, 0
just wrap Container
the in LimitedBox
a :
ListView(
children: <Widget>[
LimitedBox(
maxHeight: 100,
child: Container(color: Colors.green),
),
LimitedBox(
maxHeight: 100,
child: Container(color: Colors.red),
),
],
)
Effect:
FractionallySizedBox
FractionallySizedBox
The function is to make the width and height of the child component can be set according to the percentage of the width and height of the parent container .
Example:
class FractionallySizedBoxWidget extends StatelessWidget {
const FractionallySizedBoxWidget({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("FractionallySizedBox"),),
body: Center(
//child宽高分别是父组件宽高的80%
child: FractionallySizedBox(
widthFactor: 0.8,
heightFactor: 0.8,
child: Container(
color: Colors.red,
),
),
),
);
}
}
Effect:
Linear layout (Row and Column)
The so-called linear layout refers to arranging subcomponents along the horizontal or vertical direction. Linear layouts are implemented in Flutter through Row
and , similar to controls Column
in Android . and both inherit from .LinearLayout
Row
Column
Flex
Main axis and vertical axis
For linear layout, there are main axis and vertical axis. If the layout is along the horizontal direction, then the main axis refers to the horizontal direction, and the vertical axis refers to the vertical direction; if the layout is along the vertical direction, then the main axis refers to the vertical direction, and the vertical axis is horizontal direction. In fact, the vertical axis is the cross axis relative to the main axis direction.
MainAxisAlignment
In linear layout, there are two enumeration classes and which define the alignment CrossAxisAlignment
, which represent main axis alignment and cross axis alignment respectively.
Row
Row can arrange its child widgets horizontally. Common attributes:
Attributes | illustrate |
---|---|
textDirection |
Indicates the layout order of subcomponents in the horizontal direction (from left to right or from right to left), and defaults to the text direction of the current Locale environment of the system (for example, Chinese and English are from left to right, while Arabic is from right to left ) |
mainAxisSize |
Indicates the space occupied in the main axis (horizontal) direction. By default MainAxisSize.max , it means occupying as much space as possible in the horizontal direction. At this time, no matter how much widgets horizontal space the child actually occupies, Row the width of the sub is always equal to the maximum width in the horizontal direction; while it MainAxisSize.min means as little as possible The horizontal space occupied by , when the subcomponent does not occupy the remaining horizontal space, the Row actual width of is equal to the horizontal space occupied by all subcomponents |
mainAxisAlignment |
Indicates the alignment of subcomponents in the horizontal direction of Row. If mainAxisSize the value is MainAxisSize.min , this property is meaningless, because the width of subcomponents is equal to Row the width of . This property is only meaningful when mainAxisSize the value ofMainAxisSize.max |
verticalDirection |
Indicates the alignment direction of the Row cross axis (vertical), the default is VerticalDirection.down , it means from top to bottom |
crossAxisAlignment |
Indicates the alignment of subcomponents in the direction of the cross axis. The height of Row is equal to the height of the highest subelement in the subcomponent. Its value is the same as ( MainAxisAlignment including start、end、 center three values). The difference is that crossAxisAlignment the reference system is verticalDirection that verticalDirection the value refers VerticalDirection.down to crossAxisAlignment.start Top alignment, when verticalDirection the value is bottom alignmentVerticalDirection.up crossAxisAlignment.start ; |
children |
array of child components |
The values of mainAxisAlignment
are as follows:
MainAxisAlignment | illustrate |
---|---|
MainAxisAlignment.start |
Indicates textDirection the alignment along the initial direction. If textDirection the value TextDirection.ltr is set , it MainAxisAlignment.start means left-aligned, and when textDirection the value is TextDirection.rt l, it means right-aligned |
MainAxisAlignment.end |
and MainAxisAlignment.start exactly the opposite |
MainAxisAlignment.center |
Indicates center alignment |
MainAxisAlignment.spaceBetween |
Distribute free space evenly between subcomponents |
MainAxisAlignment.spaceAround |
Distributes free space evenly between child components and shows half of the space before the first child and after the last child |
MainAxisAlignment.spaceEvenly |
Distributes the free space evenly between the child components and also shows the evenly spaced space before the first child and after the last child |
It can be understood in this way: textDirection
Yes is mainAxisAlignment
the frame of reference.
Example:
Column(
//测试Row对齐方式,排除Column默认居中对齐的干扰
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(" hello world "),
Text(" I am Jack "),
],
),
Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(" hello world "),
Text(" I am Jack "),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
textDirection: TextDirection.rtl,
children: <Widget>[
Text(" hello world "),
Text(" I am Jack "),
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
verticalDirection: VerticalDirection.up,
children: <Widget>[
Text(" hello world ", style: TextStyle(fontSize: 30.0),),
Text(" I am Jack "),
],
),
],
);
Effect:
explain:
- The first one
Row
is very simple, and the default is center alignment; - The second one
Row
, since the width of the value is equal to the sum of the two widthsmainAxisSize
, the alignment is meaningless, so it will be displayed from left to right;MainAxisSize.min
Row
Text
- The third
Row
settextDirection
the valueTextDirection.rtl
, so the subcomponents will be arranged in order from right to left, and at this timeMainAxisAlignment.end
it means left alignment, so the final display result is like the third row in the figure; - The fourth
Row
test is the alignment of the vertical axis. Since the two subText
-fonts are different, their heights are also different. We specifyverticalDirection
a valueVerticalDirection.up
, that is, arrange from bottom to top, andcrossAxisAlignment
the value at this timeCrossAxisAlignment.start
means bottom alignment.
Column
Column
Its subcomponents can be arranged vertically. The parameters are Row
the same as, except that the layout direction is vertical.
Example:
import 'package:flutter/material.dart';
class CenterColumnRoute extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("hi"),
Text("world"),
],
);
}
}
Effect:
explain:
- Since we didn't specify
Column
onemainAxisSize
, using the default willMainAxisSize.max
take up as much space as possible vertically, in this case the full screen height.Column
- Since we specified
crossAxisAlignment
the attribute asCrossAxisAlignment.center
, the child itemsColumn
will be centered in the cross axis direction (horizontal direction). Note that alignment in the horizontal direction is bounded, the total width is the actual width of the occupied space, and the actual width depends on the largest widthColumn
among the children . In this example, there are two children , and the display "world" has the largest width, so the actual width of is the width of , so it will be displayed in the middle of the center after alignment .Widget
Column
Widget
Text
Column
Text("world")
Text("hi")
Text("world")
In fact, Row
and Column
both will only take up as much space as possible in the main axis direction, and the length of the cross axis depends on the length of their largest child element .
If we want the two text controls in this example to be aligned in the middle of the entire phone screen, we have two methods:
-
Specify
Column
the width of the as the screen width; this is simple, we can force the width limit to be changed byConstrainedBox
or .SizedBox
For example: set to
ConstrainedBox
, to make the width take up as much space as possible.minWidth
double.infinity
ConstrainedBox(
constraints: BoxConstraints(minWidth: double.infinity),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("hi"),
Text("world"),
],
),
);
Center
Components can be used
Nesting
If Row
it is nested inside Row
, or Column
nested inside again Column
, only the outermost one Row
will Column
take up as much space as possible, and the space occupied by the inside Row
or inside is the actual size. The following is an example:Column
Column
Container(
color: Colors.green,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max, //有效,外层Colum高度为整个屏幕
children: <Widget>[
Container(
color: Colors.red,
child: Column(
mainAxisSize: MainAxisSize.max,//无效,内层Colum高度为实际高度
children: <Widget>[
Text("hello world "),
Text("I am Jack "),
],
),
)
],
),
),
);
running result:
If you want the inside Column
to fill the outside Column
, you can use Expanded
the component:
Expanded(
child: Container(
color: Colors.red,
child: Column(
mainAxisAlignment: MainAxisAlignment.center, //垂直方向居中对齐
children: <Widget>[
Text("hello world "),
Text("I am Jack "),
],
),
),
)
running result:
Elastic layout (Flex, Expanded)
Flexible layout allows child components to allocate parent container space according to a certain ratio. The concept of elastic layout also exists in other UI systems, such as the elastic box layout in H5 , AndroidFlexboxLayout
, etc. The flexible layout in Flutter is mainly realized through cooperation with Flex
and Expanded
.
Flex
Flex
Components can arrange subcomponents along the horizontal or vertical direction. If you know the direction of the main axis , you can use it directly Row
or Column
it will be more convenient, because they all inherit from Flex
, and the parameters are basically the same, so Flex
basically you can use Row
or Column
.
Flex
The function itself is very powerful, and it can also Expanded
cooperate with components to achieve flexible layout. Next, we only discuss Flex
properties related to flexible layout (other properties have been introduced in the introduction Row
and ).Column
Flex({
...
required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
List<Widget> children = const <Widget>[],
})
Flex
Inherited from MultiChildRenderObjectWidget
, corresponding RenderObject
to RenderFlex
, RenderFlex
implements its layout algorithm in.
Flexible
Flexible
It can only be used as Row、Column、Flex
a child of (otherwise an error will be reported, because both inherit from Row
and , so can also be used as their child), the component can control the child controls of to fill up the parent control, and the remaining space can be used to extend the occupied space.Column
Flex
Flexible
Flexible
Row、Column、Flex
For example, Row
there are 3 sub-controls in , with fixed width on both sides, and the middle one takes up the remaining space, the code is as follows:
Row(
children: <Widget>[
Container(color: Colors.blue, height: 50, width: 100, ),
Flexible(child: Container(color: Colors.red, height: 50) ),
Container(color: Colors.blue, height: 50, width: 100, ),
],
)
The effect is as shown in the figure:
There are still 3 sub-controls. I hope the first one will take up 1/6, the second one will take up 2/6, and the third one will take up 3/6. The code is as follows:
Column(
children: <Widget>[
Flexible(
flex: 1,
child: Container(
color: Colors.blue,
alignment: Alignment.center,
child: const Text('1 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
),
),
Flexible(
flex: 2,
child: Container(
color: Colors.red,
alignment: Alignment.center,
child: const Text('2 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
),
),
Flexible(
flex: 3,
child: Container(
color: Colors.green,
alignment: Alignment.center,
child: const Text('3 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
),
),
],
)
The effect is as shown in the figure:
Sub-control ratio = current sub-control flex / sum of all sub-control flex.
Flexible
The middle fit
parameter indicates how to fill up the remaining space:
tight
: Must (mandatory) fill the remaining space.loose
: Fill the remaining space as large as possible, but it is not necessary to fill it up.
These two seem not very easy to understand, what is to fill the remaining space as large as possible? When will it fill up? See the example below:
Row(
children: <Widget>[
Container(color: Colors.blue, height: 50, width: 100,),
Flexible(
child: Container(
color: Colors.red,
height: 50,
child: const Text('Container',style: TextStyle(color: Colors.white),),
)
),
Container(color: Colors.blue, height: 50, width: 100, ),
],
)
Effect:
This code is based on the top code to Container
add Text
sub-controls to the red in the middle.
At this time, the red Container
is no longer filling the space, and then Container
add alignment. The code is as follows:
Row(
children: <Widget>[
Container(color: Colors.blue, height: 50, width: 100,),
Flexible(
child: Container(
color: Colors.red,
height: 50,
alignment: Alignment.center,
child: const Text('Container',style: TextStyle(color: Colors.white),),
)
),
Container(color: Colors.blue, height: 50, width: 100, ),
],
)
Effect:
This again fills the remaining space. Container
The default is to adapt the size of the child control, but when the alignment is set, it will fill the parent control (refer to hereContainer
for details ), so whether to fill the remaining space depends on whether the child control needs to fill the parent control.
If you change Flexible
the neutron control from to , the code is as follows:Container
OutlineButton
Row(
children: <Widget>[
Container(color: Colors.blue, height: 50, width: 100,),
Flexible(child: OutlineButton(child: Text('OutlineButton'), ),),
Container(color: Colors.blue, height: 50, width: 100, ),
],
)
OutlineButton
Under normal circumstances, the parent control is not filled, so the final effect should not fill the remaining space, as shown in the figure:
Expanded
Expanded
is Flexible
a special case of , which inherits from Flexible
:
class Expanded extends Flexible {
/// Creates a widget that expands a child of a [Row], [Column], or [Flex]
/// so that the child fills the available space along the flex widget's
/// main axis.
const Expanded({
super.key,
super.flex,
required super.child,
}) : super(fit: FlexFit.tight);
}
So Expanded
can only be used as Row、Column、Flex
a child of , it can "expand" Flex
the space occupied by the child components in proportion. Note that the parameter Expanded
of fit
is fixed FlexFit.tight
, which means Expanded
must (force) to fill the remaining space.
flex
: Elasticity coefficient, if it is0
ornull
,child
it is inelastic, that is, the space that will not be occupied by expansion. If greater0
, all free spaces of the main axis are dividedExpanded
according to its proportion.flex
The above OutlineButton
want to fill the remaining space can use Expanded
:
Row(
children: <Widget>[
Container(color: Colors.blue, height: 50, width: 100,),
Expanded(child: OutlineButton(child: Text('OutlineButton'), ),),
Container(color: Colors.blue, height: 50, width: 100, ),
],
)
Effect:
Let's look at another example:
class FlexLayoutTestRoute extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: <Widget>[
//Flex的两个子widget按1:2来占据水平空间
Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
flex: 1,
child: Container(
height: 30.0,
color: Colors.red,
),
),
Expanded(
flex: 2,
child: Container(
height: 30.0,
color: Colors.green,
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: SizedBox(
height: 100.0,
//Flex的三个子widget,在垂直方向按2:1:1来占用100像素的空间
child: Flex(
direction: Axis.vertical,
children: <Widget>[
Expanded(
flex: 2,
child: Container(
height: 30.0,
color: Colors.red,
),
),
Spacer(
flex: 1,
),
Expanded(
flex: 1,
child: Container(
height: 30.0,
color: Colors.green,
),
),
],
),
),
),
],
);
}
}
running result:
Spacer
The function in the example is to occupy a specified proportion of space. In fact, it is just Expanded
a wrapper class. Spacer
The source code is as follows:
class Spacer extends StatelessWidget {
const Spacer({
Key? key, this.flex = 1})
: assert(flex != null),
assert(flex > 0),
super(key: key);
final int flex;
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: const SizedBox.shrink(),
);
}
}
Flex layout in action
In daily use, you often encounter such a UI: there is a title in front, and several labels in the back. No matter how long the title is, the labels should be fully displayed, and the title is truncated according to the remaining space. Specifically, several typical situations shown in the figure below should be met.
The figure above shows several layouts of the target UI style: when both the title and the label can be fully displayed, they are displayed sequentially, as shown in line 1 in the figure; when the title is too long, the label is fully displayed, and the title is truncated by itself. As shown in lines 2 and 3 in the figure; the maximum length of the label cannot exceed half of the screen, as shown in line 4 in the figure.
It should be noted that the above UI cannot Flexible
be realized through multiple components. From the above description, it can be found that the label part must be laid out first, and the maximum width is limited to half of the screen; while the title part must be laid out later, and the maximum width is The space left after the label layout ends. Combined with the analysis in Chapter 6, the label should be set as a non- Flex
node, and the title should be set as Flex
a node, so that the above-mentioned expected layout order can be achieved. The specific implementation code is as follows:
class Tile extends StatelessWidget {
final String title;
final String tag;
const Tile({
Key key, this.title, this.tag}) : super(key: key);
Widget build(BuildContext context) {
return Row(
children: [
Flexible( // 填充剩余宽度
child: Text(title,
maxLines: 1, overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 20, color: Colors.black87),
),
), // Flexible
ConstrainedBox( // 约束最大宽度
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width/2),
child: Text(tag, maxLines: 1,
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
), // ConstrainedBox
],
); // Row
}
} // StatelessWidget
In the above implementation, firstly, ConstrainedBox
the maximum width of the label part is constrained, and then Flexible
the title can fully fill the remaining space with the help of components.
Flow layout (Wrap, Flow)
When introducing Row
and Colum
, if the child widget
exceeds the screen range, an overflow error will be reported, such as:
Row(
children: <Widget>[
Text("xxx"*100)
],
);
As you can see, the overflow part on the right reports an error. This is because Row
there is only one line by default, and it will not wrap when it exceeds the screen. We call a layout that automatically wraps beyond the screen display range as a fluid layout. In Flutter, Wrap
and are Flow
used to support flow layout. If the above example is Row
replaced Wrap
with the overflow part, the line will be automatically folded. Next, we will introduce Wrap
and respectively Flow
.
Wrap
Wrap
Row\Column
Similar to most of the attributes of and are both single-row Row
and Column
single-column, Wrap
which breaks through this limitation. When mainAxis
the upper space is insufficient, crossAxis
the display will be expanded upwards.
The following are Wrap
some commonly used properties:
Attributes | illustrate |
---|---|
direction |
The direction of the main axis, the default is horizontalAxis.horizontal |
alignment |
The alignment method of the main axis, the defaultWrapAlignment.start |
textDirection |
text direction |
verticalDirection |
Defines the placement order of children, the default is VerticalDirection.down , the same as Flex related attributes |
spacing |
The spacing of sub-widgets in the main axis direction |
runSpacing |
Spacing in the direction of the cross axis |
runAlignment |
Alignment in the direction of the cross axis |
Example:
Wrap(
spacing: 8.0, // 主轴(水平)方向间距
runSpacing: 4.0, // 交叉轴(垂直)方向间距
alignment: WrapAlignment.center, //沿主轴方向居中
children: <Widget>[
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('A')),
label: Text('Hamilton'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('M')),
label: Text('Lafayette'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('H')),
label: Text('Mulligan'),
),
Chip(
avatar: CircleAvatar(backgroundColor: Colors.blue, child: Text('J')),
label: Text('Laurens'),
),
],
)
Effect:
Flow
We generally seldom use it Flow
, because it is too complicated and needs to realize widget
the position conversion of the sub by itself. In many scenarios, the first thing to consider is Wrap
whether it meets the requirements. Flow
It is mainly used in scenes that require custom layout strategies or high performance requirements (such as in animation). Flow
It has the following advantages:
- Good performance;
Flow
it is a very efficient control for adjusting the size and position of subcomponents. ItFlow
uses the transformation matrix to optimize the position adjustment of subcomponents: afterFlow
positioning, if the size or position of subcomponents changes, inFlowDelegate
the ThepaintChildren()
method callscontext.paintChild
to redraw, andcontext.paintChild
the transformation matrix is used during redrawing, and the component position is not actually adjusted. - Flexible; since we need to implement the method ourselves
FlowDelegate
,paintChildren()
we need to calculate the position of each component by ourselves, so we can customize the layout strategy.
shortcoming:
- Complicated to use.
Flow
It cannot adapt to the size of the child component, and must return a fixed size by specifying the size of the parent container or implementing itTestFlowDelegate
.getSize
Example: We make a custom flow layout for six color blocks:
Flow(
delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
children: <Widget>[
Container(width: 80.0, height:80.0, color: Colors.red,),
Container(width: 80.0, height:80.0, color: Colors.green,),
Container(width: 80.0, height:80.0, color: Colors.blue,),
Container(width: 80.0, height:80.0, color: Colors.yellow,),
Container(width: 80.0, height:80.0, color: Colors.brown,),
Container(width: 80.0, height:80.0, color: Colors.purple,),
],
)
achieve TestFlowDelegate
:
class TestFlowDelegate extends FlowDelegate {
EdgeInsets margin;
TestFlowDelegate({
this.margin = EdgeInsets.zero});
double width = 0;
double height = 0;
void paintChildren(FlowPaintingContext context) {
var x = margin.left;
var y = margin.top;
//计算每一个子widget的位置
for (int i = 0; i < context.childCount; i++) {
var w = context.getChildSize(i)!.width + x + margin.right;
if (w < context.size.width) {
context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
x = w + margin.left;
} else {
x = margin.left;
y += context.getChildSize(i)!.height + margin.top + margin.bottom;
//绘制子widget(有优化)
context.paintChild(i, transform: Matrix4.translationValues(x, y, 0.0));
x += context.getChildSize(i)!.width + margin.left + margin.right;
}
}
}
Size getSize(BoxConstraints constraints) {
// 指定Flow的大小,简单起见我们让宽度竟可能大,但高度指定为200,
// 实际开发中我们需要根据子元素所占用的具体宽高来设置Flow大小
return Size(double.infinity, 200.0);
}
bool shouldRepaint(FlowDelegate oldDelegate) {
return oldDelegate != this;
}
}
running result:
可以看到我们主要的任务就是实现paintChildren
,它的主要任务是确定每个子widget
位置。由于Flow
不能自适应子widget
的大小,我们通过在getSize
返回一个固定大小来指定Flow
的大小。
注意,如果我们需要自定义布局策略,一般首选的方式是通过直接继承RenderObject
,然后通过重写 performLayout
的方式实现。
层叠布局(Stack、Positioned)
层叠布局和 Web 中的绝对定位、Android 中的 FrameLayout
相似,子组件可以根据距父容器四个角的位置来确定自身的位置。子组件是按照代码中声明的顺序堆叠起来。Flutter中使用Stack结合Positioned、Align这两个组件来配合实现定位。Stack
允许子组件堆叠,而Positioned
用于根据Stack
的四个角来确定子组件的位置。
Stack
常用属性:
属性 | 说明 |
---|---|
alignment |
此参数决定如何去对齐没有定位(没有使用Positioned )或部分定位的子组件。所谓部分定位,在这里特指没有在某一个轴上定位: left 、right 为横轴,top 、bottom 为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。默认值是AlignmentDirectional.topStart |
textDirection |
和Row 、Wrap 的textDirection 功能一样,都用于确定alignment 对齐的参考系,即: textDirection 的值为TextDirection.ltr ,则alignment 的start 代表左,end 代表右,即从左往右的顺序;textDirection 的值为TextDirection.rtl ,则alignment 的start 代表右,end 代表左,即从右往左的顺序 |
fit |
此参数用于确定没有定位的子组件如何去适应Stack 的大小。StackFit.loose 表示使用子组件的大小,StackFit.expand 表示扩伸到Stack 的大小,默认是StackFit.loose |
clipBehavior |
此属性决定对超出Stack 显示空间的部分如何剪裁,Clip 枚举类中定义了剪裁的方式,默认是Clip.hardEdge 表示直接剪裁,不应用抗锯齿 |
Positioned
常用属性:
属性 | 说明 |
---|---|
left |
子元素距离左侧距离 |
top |
子元素距离顶部的距离 |
right |
子元素距离右侧距离 |
bottom |
子元素距离底部的距离 |
child |
子组件 |
width |
**子组件的高度 ** |
height |
子组件的高度 |
注意,Positioned
的width
、height
和其他地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom
来定位组件,举个例子,在水平方向时,你只能指定left、right、width
三个属性中的两个,如指定left
和width
后,right
会自动算出(left+width
),如果同时指定三个属性则会报错,垂直方向同理。另外,宽度和高度必须是固定值,没法使用double.infinity
。
示例:下面代码通过对几个Text
组件的定位来演示Stack
和Positioned
的特性
//通过ConstrainedBox来确保Stack占满屏幕
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: Stack(
alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式
//注意:相对于外部容器进行定位,如果没有外部容器就相对于整个屏幕进行定位
children: <Widget>[
Container(
child: Text("Hello world",style: TextStyle(color: Colors.white)),
color: Colors.red,
),
Positioned(
left: 18.0,
child: Text("I am Jack"),
),
Positioned(
top: 18.0,
child: Text("Your friend"),
)
],
),
);
效果:
- 由于第一个子文本组件
Text("Hello world")
没有指定定位,并且alignment
值为Alignment.center
,所以它会居中显示。 - 第二个子文本组件
Text("I am Jack")
只指定了水平方向的定位(left
),所以属于部分定位,即垂直方向上没有定位,那么它在垂直方向的对齐方式则会按照alignment
指定的对齐方式对齐,即垂直方向居中。 - 对于第三个子文本组件
Text("Your friend")
,和第二个Text
原理一样,只不过是水平方向没有定位,则水平方向居中。
我们给上例中的Stack
指定一个fit
属性,然后将三个子文本组件的顺序调整一下:
Stack(
alignment:Alignment.center ,
fit: StackFit.expand, //未定位widget占满Stack整个空间
children: <Widget>[
Positioned(
left: 18.0,
child: Text("I am Jack"),
),
Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
color: Colors.red,
),
Positioned(
top: 18.0,
child: Text("Your friend"),
)
],
),
效果:
可以看到,由于第二个子文本组件没有定位,所以fit
属性会对它起作用,就会占满Stack
。由于Stack
子元素是堆叠的,所以第一个子文本组件被第二个遮住了,而第三个在最上层,所以可以正常显示。
Stack
和 Positioned
实现固定导航案例
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text("你好Flutter")), body: const HomePage()),
));
}
class HomePage extends StatelessWidget {
const HomePage({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
//获取设备的宽度和高度
final size = MediaQuery.of(context).size;
return Stack(
children: [
ListView(
padding: const EdgeInsets.only(top: 50),
children: List.generate(
40, (index) => ListTile(title: Text("我是一个列表$index"))),
),
Positioned(
left: 0,
top: 0,
// bottom: 0, // 改为bottom即底部固定
width: size.width, //配置子元素的宽度和高度 没法使用double.infinity
height: 44, //配置子元素的宽度和高度
child: Container(
alignment: Alignment.center,
color: Colors.red,
child: const Text(
"二级导航",
style: TextStyle(color: Colors.white),
),
))
],
);
}
}
效果:
上面代码中MediaQuery
可以到获取屏幕宽度和高度,可以在build
方法中调用:
Widget build(BuildContext context) {
final size =MediaQuery.of(context).size;
final width =size.width;
final height =size.height;
...
}
对齐与相对定位(Align)
通过Stack
和Positioned
,我们可以指定多个子元素相对于父元素各个边的精确偏移,并且可以重叠。但如果我们只想简单的调整一个子元素在父元素中的位置的话,使用 Align
组件会更简单一些。
属性 | 说明 |
---|---|
alignment |
需要一个AlignmentGeometry 类型的值,表示子组件在父组件中的起始位置。AlignmentGeometry 是一个抽象类,它有两个常用的子类:Alignment 和 FractionalOffset |
widthFactor 和heightFactor |
用于确定 Align 组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是 Align 组件的宽高。如果值为null ,则组件的宽高将会占用尽可能多的空间。 |
简单示例:
Container(
height: 120.0,
width: 120.0,
color: Colors.blue.shade50,
child: Align(
alignment: Alignment.topRight,
child: FlutterLogo(size: 60),
),
)
效果:
FlutterLogo
是 Flutter SDK 提供的一个组件,内容就是 Flutter 的 logo 。在上面的例子中,我们显式指定了Container
的宽、高都为 120
。如果我们不显式指定宽高,而通过同时指定widthFactor
和heightFactor
为 2
也是可以达到同样的效果:
Align(
widthFactor: 2,
heightFactor: 2,
alignment: Alignment.topRight,
child: FlutterLogo(size: 60),
),
因为FlutterLogo的宽高为 60
,则Align
的最终宽高都为2*60=120
。
另外,我们还通过Alignment.topRight
将FlutterLogo定位在Container
的右上角。那Alignment.topRight
是什么呢?通过源码我们可以看到其定义如下:
static const Alignment topRight = Alignment(1.0, -1.0);
可以看到它只是Alignment
的一个实例,下面我们介绍一下Alignment
。
Alignment
Alignment
继承自AlignmentGeometry
,表示矩形内的一个点,他有两个属性x、y
,分别表示在水平和垂直方向的偏移,Alignment
定义如下:
Alignment(this.x, this.y)
Alignment
Widget会以矩形的中心点作为坐标原点,即Alignment(0.0, 0.0)
。x、y
的值从-1
到1
分别代表矩形左边到右边的距离和顶部到底边的距离,因此2
个水平(或垂直)单位则等于矩形的宽(或高),如Alignment(-1.0, -1.0)
代表矩形的左侧顶点,而Alignment(1.0, 1.0)
代表右侧底部终点,而Alignment(1.0, -1.0)
则正是右侧顶点,即Alignment.topRight
。为了使用方便,矩形的原点、四个顶点,以及四条边的终点在Alignment
类中都已经定义为了静态常量。
Alignment
可以通过其坐标转换公式将其坐标转为子元素的具体偏移坐标:
(Alignment.x * childWidth / 2 + childWidth / 2, Alignment.y * childHeight / 2 + childHeight / 2)
其中childWidth
为子元素的宽度,childHeight
为子元素高度。
现在我们再看看上面的示例,我们将Alignment(1.0, -1.0)
带入上面公式,可得FlutterLogo的实际偏移坐标正是(60,0)
。
下面再看一个例子:
Align(
widthFactor: 2,
heightFactor: 2,
alignment: Alignment(2,0.0),
child: FlutterLogo(size: 60),
)
我们可以先想象一下运行效果:将Alignment(2,0.0)
带入上述坐标转换公式,可以得到FlutterLogo的实际偏移坐标为(90,30)
。实际运行如图所示:
FractionalOffset
FractionalOffset
继承自 Alignment
,它和 Alignment
唯一的区别就是坐标原点不同!FractionalOffset
的坐标原点为矩形的左侧顶点,这和布局系统的一致,所以理解起来会比较容易。FractionalOffset
的坐标转换公式为:
实际偏移 = (FractionalOffse.x * childWidth, FractionalOffse.y * childHeight)
简单示例:
Container(
height: 120.0,
width: 120.0,
color: Colors.blue[50],
child: Align(
alignment: FractionalOffset(0.2, 0.6),
child: FlutterLogo(size: 60),
),
)
效果:
我们将FractionalOffset(0.2, 0.6)
带入坐标转换公式得FlutterLogo实际偏移为(12,36)
,和实际运行效果吻合。
建议在需要制定一些精确的偏移时应优先使用FractionalOffset
,因为它的坐标原点和布局系统相同,能更容易算出实际偏移。
Align和Stack对比
可以看到,Align
和Stack/Positioned
都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:
- 定位参考系统不同:
Stack/Positioned
定位的的参考系可以是父容器矩形的四个顶点;而Align
则需要先通过alignment
参数来确定坐标原点,不同的alignment
会对应不同原点,最终的偏移是需要通过alignment
的转换公式来计算出。 Stack
可以有多个子元素,并且子元素可以堆叠,而Align
只能有一个子元素,不存在堆叠。
Align
结合Stack
使用:
class HomePage extends StatelessWidget {
const HomePage({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Center(
child: Container(
height: 400,
width: 300,
color: Colors.red,
child: const Stack(
// alignment: Alignment.center,
children: <Widget>[
Align(
alignment: Alignment(1,-0.2),
child: Icon(Icons.home,size: 40,color: Colors.white),
),
Align(
alignment: Alignment.center,
child: Icon(Icons.search,size: 30,color: Colors.white),
),
Align(
alignment: Alignment.bottomRight,
child: Icon(Icons.settings_applications,size: 30,color:
Colors.white),
)
],
),
),
);
}
}
Center组件
Center
组件的源码定义如下:
class Center extends Align {
const Center({
Key? key, double widthFactor, double heightFactor, Widget? child })
: super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
可以看到Center
继承自Align
,它比Align
只少了一个alignment
参数;由于Align
的构造函数中alignment
默认值为Alignment.center
,所以,我们可以认为Center
组件其实是对齐方式确定(Alignment.center
)了的Align
。
上面我们讲过当widthFactor
或heightFactor
为null
时组件的宽高将会占用尽可能多的空间,这一点需要特别注意,我们通过一个示例说明:
...//省略无关代码
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Center(
child: Text("xxx"),
),
),
DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
child: Center(
widthFactor: 1,
heightFactor: 1,
child: Text("xxx"),
),
)
效果:
熟悉Web开发的同学可能会发现Align
组件的特性和Web开发中相对定位(position: relative
)非常像,是的!在大多数时候,我们可以直接使用Align
组件来实现Web中相对定位的效果。
Card 组件
Card
是卡片组件块,内容可以由大多数类型的Widget构成,Card
具有圆角和阴影,这让它看起来有立体感。
属性 | 说明 |
---|---|
margin |
外边距 |
child |
子组件 |
elevation |
阴影值的深度 |
color |
背景颜色 |
shadowColor |
阴影颜色 |
margin |
外边距 |
clipBehavior |
内容溢出的剪切方式 Clip.none 不剪切 Clip.hardEdge 裁剪但不应用抗锯齿 Clip.antiAlias 裁剪而且抗锯齿 Clip.antiAliasWithSaveLayer 带有抗锯齿的剪辑,并在剪辑之后立即保存saveLayer |
Shape |
Card的阴影效果,默认的阴影效果为圆角的长方形边。 shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10)) ), |
用 Card
实现一个通讯录的卡片效果:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key}) : super(key: key);
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(title: const Text("Flutter App")),
body: const LayoutDemo(),
),
);
}
}
class LayoutDemo extends StatelessWidget {
const LayoutDemo({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return ListView(
children: [
Card(
shape: RoundedRectangleBorder(
//Card的阴影效果
borderRadius: BorderRadius.circular(10)),
elevation: 20, //阴影值的深度
margin: const EdgeInsets.all(10),
child: Column(
children: const [
ListTile(
title: Text("张三", style: TextStyle(fontSize: 28)),
subtitle: Text("高级软件工程师"),
),
Divider(),
ListTile(
title: Text("电话:152222222"),
),
ListTile(
title: Text("地址:北京市海淀区 xxx"),
),
],
),
),
Card(
shape: RoundedRectangleBorder(
//Card的阴影效果
borderRadius: BorderRadius.circular(10)),
elevation: 20,
margin: const EdgeInsets.all(10),
// color:Colors.black12, //背景颜色
child: Column(
children: const [
ListTile(
title: Text("李四", style: TextStyle(fontSize: 28)),
subtitle: Text("Flutter高级软件工程师"),
),
Divider(),
ListTile(
title: Text("电话:152222222"),
),
ListTile(
title: Text("地址:北京市海淀区 xxx"),
),
],
),
),
Card(
shape: RoundedRectangleBorder(
//Card的阴影效果
borderRadius: BorderRadius.circular(10)),
elevation: 20, //阴影值的深度
margin: const EdgeInsets.all(10),
child: Column(
children: const [
ListTile(
title: Text("张三", style: TextStyle(fontSize: 28)),
subtitle: Text("高级软件工程师"),
),
Divider(),
ListTile(
title: Text("电话:152222222"),
),
ListTile(
title: Text("地址:北京市海淀区 xxx"),
),
],
),
),
],
);
}
}
用 Card
实现一个图文列表卡片效果:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({
Key? key}) : super(key: key);
// This widget is the root of your application.
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(title: const Text("Flutter App")),
body: const LayoutDemo(),
),
);
}
}
class LayoutDemo extends StatelessWidget {
const LayoutDemo({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
return ListView(
children: [
Card(
shape: RoundedRectangleBorder(
borderRadius:BorderRadius.circular(10)
),
elevation: 20,
margin: const EdgeInsets.all(10),
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
"https://www.itying.com/images/flutter/3.png",
fit: BoxFit.cover),
),
ListTile(
leading: ClipOval(
child:Image.network(
"https://www.itying.com/images/flutter/3.png",
fit: BoxFit.cover,
height: 40,
width: 40,
),
),
title: const Text("xxxxxxxxx"),
subtitle: const Text("xxxxxxxxx"),
)
],
),
),
Card(
shape: RoundedRectangleBorder(
borderRadius:BorderRadius.circular(10)
),
elevation: 20,
margin: const EdgeInsets.all(10),
child: Column(
children: [
AspectRatio(
aspectRatio: 16 / 9,
child: Image.network(
"https://www.itying.com/images/flutter/3.png",
fit: BoxFit.cover),
),
const ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage("https://www.itying.com/images/flutter/4.png"),
),
title: Text("xxxxxxxxx"),
subtitle: Text("xxxxxxxxx"),
)
],
),
)
],
);
}
}
LayoutBuilder
通过 LayoutBuilder
,我们可以在布局过程中拿到父组件传递的约束信息,然后我们可以根据约束信息动态的构建不同的布局。
比如我们实现一个响应式的 Column
组件 ResponsiveColumn
,它的功能是当当前可用的宽度小于 200
时,将子组件显示为一列,否则显示为两列。简单来实现一下:
class ResponsiveColumn extends StatelessWidget {
const ResponsiveColumn({
Key? key, required this.children}) : super(key: key);
final List<Widget> children;
Widget build(BuildContext context) {
// 通过 LayoutBuilder 拿到父组件传递的约束,然后判断 maxWidth 是否小于200
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth < 200) {
// 最大宽度小于200,显示单列
return Column(children: children, mainAxisSize: MainAxisSize.min);
} else {
// 大于200,显示双列
var _children = <Widget>[];
for (var i = 0; i < children.length; i += 2) {
if (i + 1 < children.length) {
_children.add(Row(
children: [children[i], children[i + 1]],
mainAxisSize: MainAxisSize.min,
));
} else {
_children.add(children[i]);
}
}
return Column(children: _children, mainAxisSize: MainAxisSize.min);
}
},
);
}
}
class LayoutBuilderRoute extends StatelessWidget {
const LayoutBuilderRoute({
Key? key}) : super(key: key);
Widget build(BuildContext context) {
var _children = List.filled(6, Text("A"));
// Column在本示例中在水平方向的最大宽度为屏幕的宽度
return Column(
children: [
// 限制宽度为190,小于 200
SizedBox(width: 190, child: ResponsiveColumn(children: _children)),
ResponsiveColumn(children: _children),
LayoutLogPrint(child:Text("xx")) // 下面介绍
],
);
}
}
可以发现 LayoutBuilder
的使用很简单,但是不要小看它,因为它非常实用且重要,它主要有两个使用场景:
- 可以使用
LayoutBuilder
来根据设备的尺寸来实现响应式布局。 LayoutBuilder
可以帮我们高效排查问题。比如我们在遇到布局问题或者想调试组件树中某一个节点布局的约束时LayoutBuilder
就很有用。
打印布局时的约束信息
为了便于排错,我们封装一个能打印父组件传递给子组件约束的组件:
class LayoutLogPrint<T> extends StatelessWidget {
const LayoutLogPrint({
Key? key,
this.tag,
required this.child,
}) : super(key: key);
final Widget child;
final T? tag; //指定日志tag
Widget build(BuildContext context) {
return LayoutBuilder(builder: (_, constraints) {
// assert在编译release版本时会被去除
assert(() {
print('${
tag ?? key ?? child}: $constraints');
return true;
}());
return child;
});
}
}
这样,我们就可以使用 LayoutLogPrint
组件树中任意位置的约束信息,比如:
LayoutLogPrint(child:Text("xx"))
控制台输出:
flutter: Text("xx"): BoxConstraints(0.0<=w<=428.0, 0.0<=h<=823.0)
可以看到 Text("xx")
的显示空间最大宽度为 428,最大高度为 823 。
注意!我们的大前提是盒模型布局,如果是
Sliver
布局,可以使用SliverLayoutBuiler
来打印。
运行效果:
Flutter 的 build 和 layout
通过观察 LayoutBuilder
的示例,我们还可以发现一个关于 Flutter 构建(build)和 布局(layout)的结论:Flutter 的 build 和 layout 是可以交错执行的,并不是严格的按照先 build 再 layout 的顺序。比如在上例中 ,在build过程中遇到了 LayoutBuilder
组件,而 LayoutBuilder
的 builder
是在 layout 阶段执行的(layout阶段才能取到布局过程的约束信息),在 builder
中新新建了一个 widget
后,Flutter 框架随后会调用该 widget
的 build
方法,又进入了 build 阶段。
AfterLayout
1. 获取组件大小和相对于屏幕的坐标
Flutter 是响应式UI框架,而命令式UI框架最大的不同就是:大多数情况下开发者只需要关注数据的变化,数据变化后框架会自动重新构建UI而不需要开发者手动去操作每一个组件,所以我们会发现 Widget 会被定义为不可变的(immutable),并且没有提供任何操作组件的 API,因此如果我们想在 Flutter 中获取某个组件的大小和位置就会很困难,当然大多数情况下不会有这个需求,但总有一些场景会需要,而在命令式UI框架中是不会存在这个问题的。
我们知道,只有当布局完成时,每个组件的大小和位置才能确定,所以获取的时机肯定是布局完成后,那布局完成的时机如何获取呢?至少事件分发肯定是在布局完成之后的,比如:
Builder(
builder: (context) {
return GestureDetector(
child: Text('flutter'),
onTap: () => print(context.size), //打印 text 的大小
);
},
),
context.size
可以获取当前上下文 RenderObject
的大小,对于Builder
、StatelessWidget
以及 StatefulWidget
这样没有对应 RenderObject
的组件(这些组件只是用于组合和代理组件,本身并没有布局和绘制逻辑),获取的是子代中第一个拥有 RenderObject
组件的 RenderObject
对象。
虽然事件点击时可以拿到组件大小,但有两个问题,第一是需要用户手动触发,第二是时机较晚,更多的时候我们更希望在布局一结束就去获取大小和位置信息,为了解决这个问题,我们可以自己封装一个 AfterLayout
组件,它可以在子组件布局完成后执行一个回调,并同时将 RenderObject
对象作为参数传递。
以下是 AfterLayout
实现源码:
typedef AfterLayoutCallback = Function(RenderAfterLayout ral);
/// A widget can retrieve its render object after layout.
///
/// Sometimes we need to do something after the build phase is complete,
/// for example, most of [RenderObject] methods and attributes, such as
/// `renderObject.size`、`renderObject.localToGlobal(...)` only can be used
/// after build.
///
/// Call `setState` in callback is **allowed**, it is safe!
class AfterLayout extends SingleChildRenderObjectWidget {
const AfterLayout({
Key? key, required this.callback, Widget? child,}) : super(key: key, child: child);
RenderObject createRenderObject(BuildContext context) {
return RenderAfterLayout(callback);
}
void updateRenderObject(context, RenderAfterLayout renderObject) {
renderObject.callback = callback;
}
/// 组件树布局结束后会被触发,注意,并不是当前组件布局结束后触发
/// [callback] will be triggered after the layout phase ends.
final AfterLayoutCallback callback;
}
class RenderAfterLayout extends RenderProxyBox {
RenderAfterLayout(this.callback);
ValueSetter<RenderAfterLayout> callback;
void performLayout() {
super.performLayout();
// 不能直接回调callback,在 frame 结束的时候再去触发回调。
SchedulerBinding.instance.addPostFrameCallback((timeStamp) => callback(this));
}
/// 组件在在屏幕坐标中的起始偏移坐标
Offset get offset => localToGlobal(Offset.zero);
/// 组件在屏幕上占有的矩形空间区域
Rect get rect => offset & size;
}
AfterLayout
可以在布局结束后拿到子组件的代理渲染对象 (RenderAfterLayout
), RenderAfterLayout
对象会代理子组件渲染对象 ,因此,通过RenderAfterLayout
对象也就可以获取到子组件渲染对象上的属性,比如件大小、位置等。
AfterLayout
使用示例:
AfterLayout(
callback: (RenderAfterLayout ral) {
print(ral.size); //子组件的大小
print(ral.offset); // 子组件在屏幕中坐标
},
child: Text('flutter'),
),
运行后控制台输出:
flutter: Size(105.0, 17.0)
flutter: Offset(42.5, 290.0)
可以看到 Text
文本的实际长度是 105,高度是 17,它的起始位置坐标是(42.5, 290.0)。
2. 获取组件相对于某个父组件的坐标
RenderAfterLayout
类继承自 RenderBox
,RenderBox
有一个 localToGlobal
方法,它可以将坐标转化为相对与指定的祖先节点的坐标,比如下面代码可以打印出 Text('A')
在 父 Container
中的坐标:
Builder(builder: (context) {
return Container(
color: Colors.grey.shade200,
alignment: Alignment.center,
width: 100,
height: 100,
child: AfterLayout(
callback: (RenderAfterLayout ral) {
Offset offset = ral.localToGlobal(
Offset.zero,
// 传一个父元素 Container 对应的 RenderObject 对象
ancestor: context.findRenderObject(),
);
print('A 在 Container 中占用的空间范围为:${
offset & ral.size}');
},
child: Text('A'),
),
);
}),
下面是一个 AfterLayout
的完整测试示例:
class AfterLayoutRoute extends StatefulWidget {
const AfterLayoutRoute({
Key? key}) : super(key: key);
_AfterLayoutRouteState createState() => _AfterLayoutRouteState();
}
class _AfterLayoutRouteState extends State<AfterLayoutRoute> {
String _text = 'flutter 实战 ';
Size _size = Size.zero;
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Builder(
builder: (context) {
return GestureDetector(
child: Text(
'Text1: 点我获取我的大小',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.blue),
),
onTap: () => print('Text1: ${
context.size}'),
);
},
),
),
AfterLayout(
callback: (RenderAfterLayout ral) {
print('Text2: ${
ral.size}, ${
ral.offset}');
},
child: Text('Text2:flutter@wendux'),
),
Builder(builder: (context) {
return Container(
color: Colors.grey.shade200,
alignment: Alignment.center,
width: 100,
height: 100,
child: AfterLayout(
callback: (RenderAfterLayout ral) {
Offset offset = ral.localToGlobal(
Offset.zero,
ancestor: context.findRenderObject(),
);
print('A 在 Container 中占用的空间范围为:${
offset & ral.size}');
},
child: Text('A'),
),
);
}),
Divider(),
AfterLayout(
child: Text(_text),
callback: (RenderAfterLayout value) {
setState(() {
//更新尺寸信息
_size = value.size;
});
},
),
//显示上面 Text 的尺寸
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
'Text size: $_size ',
style: TextStyle(color: Colors.blue),
),
),
ElevatedButton(
onPressed: () {
setState(() {
_text += 'flutter 实战 ';
});
},
child: Text('追加字符串'),
),
],
);
}
}
运行效果:
After running, click Text1 to see its size in the log panel. Click the "Append String" button, after the size of the string changes, the changed size of the text area will also be displayed on the screen (next to the button above).
Reference: "Flutter Combat Second Edition"