Flutter Notes | Flutter Layout Components

Layout class components will contain one or more sub-components. Layout class components are all directly or indirectly inherited SingleChildRenderObjectWidgetand MultiChildRenderObjectWidgetWidgets. They generally have an childor childrenproperty 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.

RenderObjectWidgetThe creation and update methods are defined in the class RenderObject, and subclasses must implement them. RenderObjectWe 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 RenderObjectrealized through the corresponding object, so if you are interested in the principle of a layout class component, you can check its corresponding RenderObjectimplementation, for example Stack(cascade layout) the corresponding RenderObjectobject 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, AspectRatioand so on.

Box layout model

There are two layout models in Flutter:

  1. Layout based on RenderBoxthe box model.
  2. 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:

  1. Upper-level components pass constraints (constraints) conditions to lower-level components.
  2. 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.
  3. 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:

  1. The rendering objects corresponding to the components all inherit from the RenderBoxclass.
  2. Constraint information passed from parent to child during layout is BoxConstraintsdescribed by .

BoxConstraints

BoxConstraintsIt 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 BoxConstraintsconstructor as follows:

const BoxConstraints({
    
    
  this.minWidth = 0.0, //最小宽度
  this.maxWidth = double.infinity, //最大宽度
  this.minHeight = 0.0, //最小高度
  this.maxHeight = double.infinity //最大高度
})

It contains 4 attributes, BoxConstraintsand also defines some convenient constructors for quickly generating specific restriction rules, BoxConstraintsfor 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

ConstrainedBoxUsed 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:

insert image description here
It can be seen that although we Containerset the height of _ to 5pixels, it ends up being 50pixels, which is why ConstrainedBoxthe minimum height restriction of _ is in effect. If you Containerset the height to 80pixels, then the final height of the red area will also be 80pixels, because in this example, only the minimum heightConstrainedBox is restricted , not the maximum height .

SizedBox

SizedBoxUsed to specify a fixed width and height for child elements, such as:

SizedBox(
  width: 80.0,
  height: 80.0,
  child: redBox
)

Effect:
insert image description here

In fact, SizedBoxit is just ConstrainedBoxa 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 SizedBoxare RenderConstrainedBoxrendered through, we can see ConstrainedBoxthat the methods SizedBoxof and createRenderObject()return an RenderConstrainedBoxobject:


RenderConstrainedBox createRenderObject(BuildContext context) {
    
    
  return RenderConstrainedBox(
    additionalConstraints: ...,
  );
}

multiple restrictions

If a component has multiple parent ConstrainedBoxrestrictions, 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:
insert image description here
the final display effect is 90 in width and 60 in height, that is to say, the child ConstrainedBoxtakes minWidtheffect, minHeightbut the parent ConstrainedBoxtakes 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:

insert image description here

The final display effect is still 90, and the height is 60. The effect is the same, but the meaning is different, because minWidthit is the parent that takes effect at this time ConstrainedBox, minHeightbut the child ConstrainedBox.

Through the above example, we found that when there are multiple restrictions , for minWidthand 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 Awhose subcomponents are Band Bwhose subcomponents are C, then the constraints Cmust be obeyed B, and Bmust be Aobeyed Constraints, but Athe constraints of will not be directly constrained Cunless BpassedA through to its own constraints C. Using this principle, you can implement a Bcomponent like this:

  1. BCThere are no constraints in the layout of the component C(it can be infinite).
  2. CDetermine its own size according to its real space occupation.
  3. BADetermine its own size in combination with the size of the subcomponents under the premise of observing the constraints.

And this Bcomponent is UnconstrainedBoxthe component, which means that UnconstrainedBoxthe 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×100red box will eventually be displayed. But since the parent's restriction is UnconstrainedBox" removed " ConstrainedBox, it will eventually be ConstrainedBoxdrawn according to the child's restriction redBox, that is 90×20, as shown in the figure:

insert image description here

However, please note that UnconstrainedBoxthe "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, 80there is still an . That is to say, the restriction of the parent minHeight(100.0)is still in effect, but it does not affect redBoxthe size of the final child element, but it still occupies the corresponding space. It can be considered that the parent at this time ConstrainedBoxacts on the child UnconstrainedBoxand redBoxis only ConstrainedBoxlimited by the child. This point Please pay attention.

So is there any way to completely remove the parent ConstrainedBoxrestriction? 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 SizedBoxor ConstrainedBoxspecified 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, AppBarin the right menu of the (navigation bar) in the Material component library, we use SizedBoxthe 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:

insert image description here

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 AppBarhave 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:

insert image description here

You can see it works! In fact, it is also possible to UnconstrainedBoxreplace Centerwith or .Align

In addition, it should be noted that UnconstrainedBoxalthough the constraints can be canceled during the layout of its subcomponents (subcomponents can be infinite), but UnconstrainedBoxitself is constrained by its parent component, so when UnconstrainedBoxits subcomponents become larger, if UnconstrainedBoxthe 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:

insert image description here

The text has exceeded the width of the screen, overflowing.

AspectRatio

AspectRatioThe function is to adjust the aspect ratio of the child element child .

AspectRatioFirst, 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 BoxFitmiddle 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, AspectRatioit 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,
      ),
    );
  }
}

insert image description here

LimitedBox

LimitedBoxIt is used to specify childthe maximum width and height. It can be childlimited to the maximum width and height set by it, but this limitation is conditional. When LimitedBoxit 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 Columnso on. If LimitedBoxthe parent component is constrained, LimitedBoxnothing 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)),
              ),
      );
  }

insert image description here

TextComponents are not constrained 100x50within the region as we would like, because Centercomponents impose LimitedBoxconstraints on them, so we can use the UnconstrainedBoxremoved LimitedBoxparent 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:

insert image description here

If we ListViewadd 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, 0just wrap Containerthe in LimitedBoxa :

ListView(
  children: <Widget>[
    LimitedBox(
      maxHeight: 100, 
      child: Container(color: Colors.green),
    ),
    LimitedBox(
      maxHeight: 100, 
      child: Container(color: Colors.red), 
    ), 
  ], 
)

Effect:

insert image description here

FractionallySizedBox

FractionallySizedBoxThe 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:

insert image description here

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 Rowand , similar to controls Columnin Android . and both inherit from .LinearLayoutRowColumnFlex

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.

MainAxisAlignmentIn 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 widgetshorizontal space the child actually occupies, Rowthe width of the sub is always equal to the maximum width in the horizontal direction; while it MainAxisSize.minmeans as little as possible The horizontal space occupied by , when the subcomponent does not occupy the remaining horizontal space, the Rowactual 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 mainAxisSizethe value is MainAxisSize.min, this property is meaningless, because the width of subcomponents is equal to Rowthe width of . This property is only meaningful when mainAxisSizethe 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 ( MainAxisAlignmentincluding start、end、 centerthree values). The difference is that crossAxisAlignmentthe reference system is verticalDirectionthat verticalDirectionthe value refers VerticalDirection.downto crossAxisAlignment.startTop alignment, when verticalDirectionthe value is bottom alignmentVerticalDirection.upcrossAxisAlignment.start ;
children array of child components

The values ​​of mainAxisAlignmentare as follows:

MainAxisAlignment illustrate
MainAxisAlignment.start Indicates textDirectionthe alignment along the initial direction. If textDirectionthe value TextDirection.ltris set , it MainAxisAlignment.startmeans left-aligned, and when textDirectionthe value is TextDirection.rtl, it means right-aligned
MainAxisAlignment.end and MainAxisAlignment.startexactly 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: textDirectionYes is mainAxisAlignmentthe 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:

insert image description here
explain:

  • The first one Rowis 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 widths mainAxisSize, the alignment is meaningless, so it will be displayed from left to right;MainAxisSize.minRowText
  • The third Rowset textDirectionthe value TextDirection.rtl, so the subcomponents will be arranged in order from right to left, and at this time MainAxisAlignment.endit means left alignment, so the final display result is like the third row in the figure;
  • The fourth Rowtest is the alignment of the vertical axis. Since the two sub Text-fonts are different, their heights are also different. We specify verticalDirectiona value VerticalDirection.up, that is, arrange from bottom to top, and crossAxisAlignmentthe value at this time CrossAxisAlignment.startmeans bottom alignment.

Column

ColumnIts subcomponents can be arranged vertically. The parameters are Rowthe 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:

insert image description here

explain:

  • Since we didn't specify Columnone mainAxisSize, using the default willMainAxisSize.max take up as much space as possible vertically, in this case the full screen height.Column
  • Since we specified crossAxisAlignmentthe attribute as CrossAxisAlignment.center, the child items Columnwill 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 .WidgetColumnWidgetTextColumnText("world")Text("hi")Text("world")

In fact, Rowand Columnboth 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:

  1. Specify Columnthe width of the as the screen width; this is simple, we can force the width limit to be changed by ConstrainedBoxor .SizedBox

    For example: set to ConstrainedBox, to make the width take up as much space as possible.minWidthdouble.infinity

ConstrainedBox(	
  constraints: BoxConstraints(minWidth: double.infinity), 
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.center,
    children: <Widget>[
      Text("hi"),
      Text("world"),
    ],
  ),
);

insert image description here

  1. CenterComponents can be used

Nesting

If Rowit is nested inside Row, or Columnnested inside again Column, only the outermost one Rowwill Columntake up as much space as possible, and the space occupied by the inside Rowor inside is the actual size. The following is an example:ColumnColumn

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:

insert image description here

If you want the inside Columnto fill the outside Column, you can use Expandedthe component:

Expanded( 
  child: Container(
    color: Colors.red,
    child: Column(
      mainAxisAlignment: MainAxisAlignment.center, //垂直方向居中对齐
      children: <Widget>[
        Text("hello world "),
        Text("I am Jack "),
      ],
    ),
  ),
)

running result:

insert image description here

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 Flexand Expanded.

Flex

FlexComponents can arrange subcomponents along the horizontal or vertical direction. If you know the direction of the main axis , you can use it directly Rowor Columnit will be more convenient, because they all inherit from Flex, and the parameters are basically the same, so Flexbasically you can use Rowor Column.

FlexThe function itself is very powerful, and it can also Expandedcooperate with components to achieve flexible layout. Next, we only discuss Flexproperties related to flexible layout (other properties have been introduced in the introduction Rowand ).Column

Flex({
    
    
  ...
  required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
  List<Widget> children = const <Widget>[],
})

FlexInherited from MultiChildRenderObjectWidget, corresponding RenderObjectto RenderFlex, RenderFleximplements its layout algorithm in.

Flexible

FlexibleIt can only be used as Row、Column、Flexa child of (otherwise an error will be reported, because both inherit from Rowand , 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.ColumnFlexFlexibleFlexibleRow、Column、Flex

For example, Rowthere 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:

insert image description here

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:

insert image description here

Sub-control ratio = current sub-control flex / sum of all sub-control flex.

FlexibleThe middle fitparameter 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:

insert image description here

This code is based on the top code to Containeradd Textsub-controls to the red in the middle.
At this time, the red Containeris no longer filling the space, and then Containeradd 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:

insert image description here
This again fills the remaining space. ContainerThe 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 Flexiblethe neutron control from to , the code is as follows:ContainerOutlineButton

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, ), 
 ],
)

OutlineButtonUnder normal circumstances, the parent control is not filled, so the final effect should not fill the remaining space, as shown in the figure:

insert image description here

Expanded

Expandedis Flexiblea 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 Expandedcan only be used as Row、Column、Flexa child of , it can "expand" Flexthe space occupied by the child components in proportion. Note that the parameter Expandedof fitis fixed FlexFit.tight, which means Expandedmust (force) to fill the remaining space.

  • flex: Elasticity coefficient, if it is 0or null, childit is inelastic, that is, the space that will not be occupied by expansion. If greater 0, all free spaces of the main axis are divided Expandedaccording to its proportion.flex

The above OutlineButtonwant 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:

insert image description here

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:

insert image description here

SpacerThe function in the example is to occupy a specified proportion of space. In fact, it is just Expandeda wrapper class. SpacerThe 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.

Figure 11-1 Several typical layouts of the target UI style

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 Flexiblebe 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- Flexnode, and the title should be set as Flexa 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, ConstrainedBoxthe maximum width of the label part is constrained, and then Flexiblethe title can fully fill the remaining space with the help of components.

Flow layout (Wrap, Flow)

When introducing Rowand Colum, if the child widgetexceeds the screen range, an overflow error will be reported, such as:

Row(
  children: <Widget>[
    Text("xxx"*100)
  ],
);

insert image description here

As you can see, the overflow part on the right reports an error. This is because Rowthere 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, Wrapand are Flowused to support flow layout. If the above example is Rowreplaced Wrapwith the overflow part, the line will be automatically folded. Next, we will introduce Wrapand respectively Flow.

Wrap

WrapRow\ColumnSimilar to most of the attributes of and are both single-row Rowand Columnsingle-column, Wrapwhich breaks through this limitation. When mainAxisthe upper space is insufficient, crossAxisthe display will be expanded upwards.

The following are Wrapsome 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 Flexrelated 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:
insert image description here

Flow

We generally seldom use it Flow, because it is too complicated and needs to realize widgetthe position conversion of the sub by itself. In many scenarios, the first thing to consider is Wrapwhether it meets the requirements. FlowIt is mainly used in scenes that require custom layout strategies or high performance requirements (such as in animation). FlowIt has the following advantages:

  • Good performance; Flowit is a very efficient control for adjusting the size and position of subcomponents. It Flowuses the transformation matrix to optimize the position adjustment of subcomponents: after Flowpositioning, if the size or position of subcomponents changes, in FlowDelegatethe The paintChildren()method calls context.paintChildto redraw, and context.paintChildthe 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.
  • FlowIt 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 it TestFlowDelegate.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:

insert image description here

可以看到我们主要的任务就是实现paintChildren,它的主要任务是确定每个子widget位置。由于Flow不能自适应子widget的大小,我们通过在getSize返回一个固定大小来指定Flow的大小。

注意,如果我们需要自定义布局策略,一般首选的方式是通过直接继承RenderObject,然后通过重写 performLayout 的方式实现。

层叠布局(Stack、Positioned)

层叠布局和 Web 中的绝对定位、Android 中的 FrameLayout 相似,子组件可以根据距父容器四个角的位置来确定自身的位置。子组件是按照代码中声明的顺序堆叠起来。Flutter中使用Stack结合PositionedAlign这两个组件来配合实现定位。Stack允许子组件堆叠,而Positioned用于根据Stack的四个角来确定子组件的位置。

Stack

常用属性:

属性 说明
alignment 此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子组件。
所谓部分定位,在这里特指没有在某一个轴上定位:leftright为横轴,topbottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。默认值是AlignmentDirectional.topStart
textDirection RowWraptextDirection功能一样,都用于确定alignment对齐的参考系,
即:textDirection的值为TextDirection.ltr,则alignmentstart代表左,end代表右,即从左往右的顺序;
textDirection的值为TextDirection.rtl,则alignmentstart代表右,end代表左,即从右往左的顺序
fit 此参数用于确定没有定位的子组件如何去适应Stack的大小。StackFit.loose表示使用子组件的大小,StackFit.expand表示扩伸到Stack的大小,默认是StackFit.loose
clipBehavior 此属性决定对超出Stack显示空间的部分如何剪裁,Clip枚举类中定义了剪裁的方式,默认是Clip.hardEdge 表示直接剪裁,不应用抗锯齿

Positioned

常用属性:

属性 说明
left 子元素距离左侧距离
top 子元素距离顶部的距离
right 子元素距离右侧距离
bottom 子元素距离底部的距离
child 子组件
width **子组件的高度 **
height 子组件的高度

注意,Positionedwidthheight 和其他地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom来定位组件,举个例子,在水平方向时,你只能指定left、right、width三个属性中的两个,如指定leftwidth后,right会自动算出(left+width),如果同时指定三个属性则会报错,垂直方向同理。另外,宽度和高度必须是固定值,没法使用double.infinity

示例:下面代码通过对几个Text组件的定位来演示StackPositioned的特性

//通过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"),
      )        
    ],
  ),
);

效果:

insert image description here

  • 由于第一个子文本组件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"),
    )
  ],
),

效果:

insert image description here

可以看到,由于第二个子文本组件没有定位,所以fit属性会对它起作用,就会占满Stack。由于Stack子元素是堆叠的,所以第一个子文本组件被第二个遮住了,而第三个在最上层,所以可以正常显示。

StackPositioned实现固定导航案例

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),
              ),
            ))
      ],
    );
  }
}

效果:

insert image description here
上面代码中MediaQuery可以到获取屏幕宽度和高度,可以在build方法中调用:

Widget build(BuildContext context) {
    
    
	final size =MediaQuery.of(context).size;
	final width =size.width;
	final height =size.height;
	...
}

对齐与相对定位(Align)

通过StackPositioned,我们可以指定多个子元素相对于父元素各个边的精确偏移,并且可以重叠。但如果我们只想简单的调整一个子元素在父元素中的位置的话,使用 Align 组件会更简单一些。

属性 说明
alignment 需要一个AlignmentGeometry类型的值,表示子组件在父组件中的起始位置。AlignmentGeometry 是一个抽象类,它有两个常用的子类:AlignmentFractionalOffset
widthFactorheightFactor 用于确定 Align 组件本身宽高的属性;它们是两个缩放因子,会分别乘以子元素的宽、高,最终的结果就是 Align 组件的宽高。如果值为null,则组件的宽高将会占用尽可能多的空间。

简单示例:

Container(
  height: 120.0,
  width: 120.0,
  color: Colors.blue.shade50,
  child: Align(
    alignment: Alignment.topRight,
    child: FlutterLogo(size: 60),
  ),
)

效果:

insert image description here

FlutterLogo 是 Flutter SDK 提供的一个组件,内容就是 Flutter 的 logo 。在上面的例子中,我们显式指定了Container的宽、高都为 120。如果我们不显式指定宽高,而通过同时指定widthFactorheightFactor2 也是可以达到同样的效果:

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的值从-11分别代表矩形左边到右边的距离和顶部到底边的距离,因此2个水平(或垂直)单位则等于矩形的宽(或高),如Alignment(-1.0, -1.0) 代表矩形的左侧顶点,而Alignment(1.0, 1.0)代表右侧底部终点,而Alignment(1.0, -1.0) 则正是右侧顶点,即Alignment.topRight。为了使用方便,矩形的原点、四个顶点,以及四条边的终点在Alignment类中都已经定义为了静态常量。

insert image description here
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)。实际运行如图所示:

insert image description here

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),
  ),
)

效果:

insert image description here

我们将FractionalOffset(0.2, 0.6)带入坐标转换公式得FlutterLogo实际偏移为(12,36),和实际运行效果吻合。

建议在需要制定一些精确的偏移时应优先使用FractionalOffset,因为它的坐标原点和布局系统相同,能更容易算出实际偏移。

Align和Stack对比

可以看到,AlignStack/Positioned都可以用于指定子元素相对于父元素的偏移,但它们还是有两个主要区别:

  1. 定位参考系统不同Stack/Positioned定位的的参考系可以是父容器矩形的四个顶点;而Align则需要先通过 alignment 参数来确定坐标原点,不同的alignment会对应不同原点,最终的偏移是需要通过alignment的转换公式来计算出。
  2. 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

上面我们讲过当widthFactorheightFactornull时组件的宽高将会占用尽可能多的空间,这一点需要特别注意,我们通过一个示例说明:

...//省略无关代码
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"),
  ),
)

效果:
insert image description here

熟悉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"),
              ),
            ],
          ),
        ),
      ],
    );
  }
}

insert image description here

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"),
              )
            ],
          ),
        )
      ],
    );
  }
}

insert image description here

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 的使用很简单,但是不要小看它,因为它非常实用且重要,它主要有两个使用场景:

  1. 可以使用 LayoutBuilder 来根据设备的尺寸来实现响应式布局。
  2. 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 来打印。

运行效果:

insert image description here

Flutter 的 build 和 layout

通过观察 LayoutBuilder 的示例,我们还可以发现一个关于 Flutter 构建(build)和 布局(layout)的结论:Flutter 的 build 和 layout 是可以交错执行的,并不是严格的按照先 buildlayout 的顺序。比如在上例中 ,在build过程中遇到了 LayoutBuilder 组件,而 LayoutBuilderbuilder 是在 layout 阶段执行的(layout阶段才能取到布局过程的约束信息),在 builder 中新新建了一个 widget 后,Flutter 框架随后会调用该 widgetbuild 方法,又进入了 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 的大小,对于BuilderStatelessWidget 以及 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 类继承自 RenderBoxRenderBox 有一个 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('追加字符串'),
        ),
      ],
    );
  }
}

运行效果:

insert image description here

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"

Guess you like

Origin blog.csdn.net/lyabc123456/article/details/130843115