Flutter Notes | Flutter Container Components

Padding

This component can best reflect a difference between Flutter and other UI frameworks, that is, in other UI frameworks, it is paddingbasically an attribute of the component. For example, common layout tags in htmlpadding have attributes, and the same is true in Android , but in Components in Flutter do not have a property called . Instead, a component called paddingis provided . (It embodies the concept that everything is a Widget in Flutter)PaddingWidget

PaddingYou can add padding (blank space) to its child nodes, similar to the margin effect.

Here is Paddingthe definition of:

Padding({
    
    
  ...
  EdgeInsetsGeometry padding,
  Widget child,
})

It can be seen that its paddingparameters need to pass a EdgeInsetsGeometrytype, which is an abstract class. In development, we generally use EdgeInsetsclass, which is EdgeInsetsGeometrya subclass of , and defines some convenient methods for setting padding.

The following convenience methods are EdgeInsetsprovided:

  • fromLTRB(left, top, right, bottom): Specify padding in four directions respectively.
  • all(value): Padding with the same value in all directions.
  • only({left, top, right ,bottom }): You can set the fill in a specific direction (multiple directions can be specified at the same time).
  • symmetric({ vertical, horizontal })vertical: It is used to set the fill, finger sum top, bottomand horizontalfinger leftsum of the symmetry direction right.

The following examples show EdgeInsetsdifferent uses of :

class PaddingTestRoute extends StatelessWidget {
    
    
  const PaddingTestRoute({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Padding(
      //上下左右各添加16像素补白
      padding: const EdgeInsets.all(16),
      child: Column(
        //显式指定对齐方式为左对齐,排除对齐干扰
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: const <Widget>[
          Padding(
            //左边添加8像素补白
            padding: EdgeInsets.only(left: 8),
            child: Text("Hello world"),
          ),
          Padding(
            //上下各添加8像素补白
            padding: EdgeInsets.symmetric(vertical: 8),
            child: Text("I am Jack"),
          ),
          Padding(
            // 分别指定四个方向的补白
            padding: EdgeInsets.fromLTRB(20, 0, 20, 20),
            child: Text("Your friend"),
          )
        ],
      ),
    );
  }
}

running result:

insert image description here

Container

ContainerIt is a combination class container, which itself does not correspond to specific ones RenderObject. It is a multifunctional container composed of components such as DecoratedBox, ConstrainedBox, Transform, Padding, etc., so we only need to use one component to realize scenes that need to be decorated, transformed, and restricted at the same time. Here is the definition of:AlignContainerContainer

Container({
    
    
  this.alignment,
  this.padding, //容器内补白,属于decoration的装饰范围
  Color color, // 背景色
  Decoration decoration, // 背景装饰
  Decoration foregroundDecoration, //前景装饰
  double width,//容器的宽度
  double height, //容器的高度
  BoxConstraints constraints, //容器大小的限制条件
  this.margin,//容器外补白,不属于decoration的装饰范围
  this.transform, //变换
  this.child,
  ...
})
Attributes illustrate
alignment topCenter: top center alignment
topLeft: top left alignment
topRight: top right alignment
center: horizontal vertical center alignment
centerLeft: vertical center horizontal alignment left:
centerRightvertical center horizontal alignment right:
bottomCenterbottom center alignment
bottomLeft: bottom left alignment
bottomRight: bottom right alignment
decoration BoxDecorationbackground decoration
foregroundDecoration BoxDecorationforeground decoration
margin Indicates the distance between the Container and other external components. such as margin:EdgeInsets.all(20.0),
padding The inner margin of the Container refers to the distance between the edge of the Container and the Child, such aspadding:EdgeInsets.all(10.0)
transform Make it easy for the Container to perform some rotations, such astransform: Matrix4.rotationZ(0.2)
height container height
width container width
child container child element
color background color
constraints Container size constraints

There are two points that need special attention:

  1. The size of the container can be specified by the width, heightattribute, or by constraints; if they exist at the same time, width, heighttakes precedence. In fact, one Containerwill be generated internally based widthon .heightconstraints
  2. colorand decorationare mutually exclusive, if they are set at the same time, an error will be reported! In fact, when specified color, Containerone is automatically created within decoration.

Sample code 1:

class MyCard extends StatelessWidget {
    
    
  const MyCard({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return Center(
      child: Container(
        width: 200,
        height: 200,
        color: Colors.blue,
        alignment: Alignment.center,
        child: const Text("你好Flutter", style: TextStyle(fontSize: 20)),
      ),
    );
  }
}

Effect:

insert image description here

Sample code 2:

Container(
    margin: const EdgeInsets.only(top: 50.0, left: 120.0),
    constraints: const BoxConstraints.tightFor(width: 200.0, height: 150.0), // 卡片大小
    decoration: BoxDecoration(  // 背景装饰
      gradient: const RadialGradient( // 背景径向渐变
        colors: [Colors.red, Colors.orange],
        center: Alignment.topLeft,
        radius: .98,
      ),
      // LinearGradient 是背景线性渐变
      // gradient: LinearGradient( colors: [Colors.red, Colors.orange]),
      boxShadow: const [ //卡片阴影
        BoxShadow(
          color: Colors.black54,
          offset: Offset(2.0, 2.0),
          blurRadius: 4.0,
        )
      ],
      border: Border.all(color: Colors.red, width: 2.0), 
      borderRadius: BorderRadius.circular(8.0), // 圆角 ,
      color: Colors.blue, 
    ),
    transform: Matrix4.rotationZ(.2),//卡片倾斜变换
    alignment: Alignment.center, //卡片内文字居中
    child: const Text("5.20", style: TextStyle(color: Colors.white, fontSize: 40.0),), //卡片文字
),

Effect:

insert image description here

By Containercreating a button:

class MyButton extends StatelessWidget {
    
    
  const MyButton({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Container(
      alignment: Alignment.center,
      width: 200,
      height: 40,
      // margin: const EdgeInsets.all(10),   //四周margin
      margin: const EdgeInsets.fromLTRB(0, 40, 0, 0),
      // padding: const EdgeInsets.fromLTRB(40, 0, 0, 0),
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius:BorderRadius.circular(20)
      ),
      child: const Text("按钮",style: TextStyle(
        color: Colors.white,
        fontSize: 20
      )),
    );
  }
}

Effect:

insert image description here

We can see Containerthat it has the functions of various components. By viewing Containerthe source code, we can easily find that it is a combination of various components. In Flutter, Containercomponents are also instances where composition takes precedence over inheritance .

Padding和Margin

Next, let's look at the difference between Containercomponents marginand paddingproperties:

...
Container(
  margin: EdgeInsets.all(20.0), //容器外补白
  color: Colors.orange,
  child: Text("Hello world!"),
),
Container(
  padding: EdgeInsets.all(20.0), //容器内补白
  color: Colors.orange,
  child: Text("Hello world!"),
),
...

Effect:

insert image description here

It can be found that the intuitive feeling is marginthat the blank space is outside the container, while paddingthe blank space is inside the container, and this difference needs to be remembered. In fact, Containerinternal marginsums paddingare all Paddingimplemented through the component, and the above sample code is actually equivalent to:

...
Padding(
  padding: EdgeInsets.all(20.0),
  child: DecoratedBox(
    decoration: BoxDecoration(color: Colors.orange),
    child: Text("Hello world!"),
  ),
),
DecoratedBox(
  decoration: BoxDecoration(color: Colors.orange),
  child: Padding(
    padding: const EdgeInsets.all(20.0),
    child: Text("Hello world!"),
  ),
),
...    

double.infinity 和 double.maxFinite

double.infinityand double.maxFinitecan make the size of the current element widthor heightreach the size of the parent element.

static const double nan = 0.0 / 0.0;
static const double infinity = 1.0 / 0.0;
static const double negativeInfinity = -infinity;
static const double minPositive = 5e-324;
static const double maxFinite = 1.7976931348623157e+308;

The following code can Containerfill the entire screen:

Widget build(BuildContext context) {
    
    
  return Container(
    height: double.infinity,
    width: double.infinity,
    color: Colors.black26,
    child: const Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [
        Icon(Icons.home, color: Colors.red),
        Icon(Icons.search, color: Colors.blue),
        Icon(Icons.send, color: Colors.orange),
      ],
    ),
  );
}

The following code can make Containerthe width and height of the element equal to the width and height of the parent element:

class HomePage extends StatelessWidget {
    
    
  const HomePage({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return Container(
      height: 400,
      width: 600,
      color: Colors.red,
      child: Container(
        height: double.maxFinite,
        width: double.infinity,
        color: Colors.black26,
        child: const Column(
          crossAxisAlignment: CrossAxisAlignment.center,
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            Icon(Icons.home, color: Colors.red),
            Icon(Icons.search, color: Colors.blue),
            Icon(Icons.send, color: Colors.orange),
          ],
        ),
      ),
    );
  }
}

DecoratedBox

DecoratedBoxSome decorations (Decoration), such as background, border, gradient, etc., can be drawn before (or after) its subcomponents are drawn. DecoratedBoxIt is defined as follows:

const DecoratedBox({
    
    
  Decoration decoration,
  DecorationPosition position = DecorationPosition.background,
  Widget? child
})
  • decoration: Represents the decoration to be drawn, its type is Decoration. DecorationIt is an abstract class, which defines an interface createBoxPainter(), and the main responsibility of the subclass is to create a brush by implementing it, which is used to draw decorations.
  • position: This attribute determines where to draw Decoration, and DecorationPositionthe enumeration type it receives, which has two values:
    • background: Drawn below the child component, which is the background decoration.
    • foreground: Draws on top of the child component, i.e. the foreground.

BoxDecoration

We usually use BoxDecorationthe class directly, which is a Decorationsubclass of a class that implements the drawing of commonly used decorative elements.

BoxDecoration({
    
    
  Color color, //颜色
  DecorationImage image,//图片
  BoxBorder border, //边框
  BorderRadiusGeometry borderRadius, //圆角
  List<BoxShadow> boxShadow, //阴影,可以指定多个
  Gradient gradient, //渐变
  BlendMode backgroundBlendMode, //背景混合模式
  BoxShape shape = BoxShape.rectangle, //形状
})

Example: The following code implements a button with a shadowed background color gradient

DecoratedBox(
   decoration: BoxDecoration(
     gradient: LinearGradient(colors:[Colors.red,Colors.orange.shade700]), //背景渐变
     borderRadius: BorderRadius.circular(3.0), //3像素圆角
     boxShadow: [ //阴影
       BoxShadow(
         color:Colors.black54,
         offset: Offset(2.0,2.0),
         blurRadius: 4.0
       )
     ]
   ),
  child: Padding(
    padding: EdgeInsets.symmetric(horizontal: 80.0, vertical: 18.0),
    child: Text("Login", style: TextStyle(color: Colors.white),),
  )
)

Effect:

insert image description here

The class used in the above example LinearGradientis used to define the class of linear gradients. Flutter also provides other gradient configuration classes, such as RadialGradient, SweepGradient, and you can check the API documentation yourself if necessary.

Transform

TransformSome special effects can be achieved by applying some matrix transformations to its child components when they are drawn. Matrix4is a 4D matrix, through which we can implement various matrix operations, the following is an example:

Container(
  color: Colors.black,
  child: Transform(
    alignment: Alignment.topRight, //相对于坐标系原点的对齐方式
    transform: Matrix4.skewY(0.3), //沿Y轴倾斜0.3弧度
    child: Container(
      padding: const EdgeInsets.all(8.0),
      color: Colors.deepOrange,
      child: const Text('Apartment for rent!'),
    ),
  ),
)

Effect:

insert image description here

Performance is good because matrix changes happen at draw time, without re-layouting, building, etc.

translate

Transform.translateReceives a parameter that translates the subcomponent by the specified distance offsetalong the axis when drawing .x、y

DecoratedBox(
  decoration:BoxDecoration(color: Colors.red), 
  child: Transform.translate(
    offset: Offset(-20.0, -5.0), // 默认原点为左上角,左移20像素,向上平移5像素  
    child: Text("Hello world"),
  ),
)

Effect:
insert image description here

to rotate

Transform.rotateSubcomponents can be rotated and transformed, such as:

import 'dart:math' as math;  // 要使用math.pi需先进行导包

DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  child: Transform.rotate( 
    angle:math.pi / 2 ,  // 旋转90度
    child: Text("Hello world"),
  ),
)

Effect:

insert image description here

zoom

Transform.scaleSubcomponents can be reduced or enlarged, such as:

DecoratedBox(
  decoration:BoxDecoration(color: Colors.red),
  child: Transform.scale(
    scale: 1.5, //放大到1.5倍
    child: Text("Hello world")
  )
);

Effect:

insert image description here

Transform Notes

  • TransformThe transformation is applied in the drawing phase , not in the layout (layout) phase , so no matter what changes are applied to the child component, its size and position on the screen are fixed , because these are determined during the layout phase . Below we explain in detail:
Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    DecoratedBox(
      decoration:BoxDecoration(color: Colors.red),
      child: Transform.scale(scale: 1.5,
          child: Text("Hello world")
      )
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
  ],
)

Effect:

insert image description here

Explanation: After the transformation (enlargement) is applied to the first one Text, it will be enlarged when drawing, but the space it occupies is still the red part, so the second one Textwill be next to the red part, and eventually the text will overlap.

  • Since the matrix change will only be applied in the drawing phase , in some scenarios, when the UI needs to change, the visual UI change can be achieved directly through the matrix change without re-triggering the build process, which will save layout . overhead, so performance will be better. As the component introduced before Flow, it uses matrix transformation to update the UI internally. In addition, Transform is also widely used in Flutter's animation components to improve performance.

RotatedBox

RotatedBoxSimilar to Transform.rotatethe function, they can both rotate and transform the subcomponents, but there is a difference: RotatedBoxthe transformation is in the layout stage, which will affect the position and size of the subcomponents . Let's Transform.rotatechange the example introduced above:

Row(
  mainAxisAlignment: MainAxisAlignment.center,
  children: <Widget>[
    DecoratedBox(
      decoration: BoxDecoration(color: Colors.red),
      // 将 Transform.rotate 换成 RotatedBox  
      child: RotatedBox(
        quarterTurns: 1, // 旋转90度(1/4圈)
        child: Text("Hello world"),
      ),
    ),
    Text("你好", style: TextStyle(color: Colors.green, fontSize: 18.0),)
  ],
),

Effect:

insert image description here

Since RotatedBoxit acts on the layout stage, the subcomponent will be rotated 90 degrees (not just the drawn content), which decorationwill affect the actual space occupied by the subcomponent, so the final effect is the effect of the above picture, which can be Transform.rotatecompared and understood with the previous example.

Clip

Tailoring class components

Flutter provides some clipping components for clipping components.

Clipping Widgets default behavior
ClipOval When the subcomponent is a square, it is clipped into a circle; when it is a rectangle, it is clipped into an ellipse
ClipRRect Clip child components to rounded rectangles
ClipRect By default, the drawing content outside the layout space of the subcomponent is clipped (the overflow part is clipped)
ClipPath Cut according to a custom path

Example:

import 'package:flutter/material.dart';

class ClipTestRoute extends StatelessWidget {
    
    
  
  Widget build(BuildContext context) {
    
    
    // 头像  
    Widget avatar = Image.asset("imgs/avatar.png", width: 60.0);
    return Center(
      child: Column(
        children: <Widget>[
          avatar, //不剪裁
          ClipOval(child: avatar), //剪裁为圆形
          ClipRRect( //剪裁为圆角矩形
            borderRadius: BorderRadius.circular(5.0),
            child: avatar,
          ), 
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              Align(
                alignment: Alignment.topLeft,
                widthFactor: .5,//宽度设为原来宽度一半,另一半会溢出
                child: avatar,
              ),
              Text("你好世界", style: TextStyle(color: Colors.green),)
            ],
          ),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              ClipRect(//将溢出部分剪裁
                child: Align(
                  alignment: Alignment.topLeft,
                  widthFactor: .5,//宽度设为原来宽度一半
                  child: avatar,
                ),
              ),
              Text("你好世界",style: TextStyle(color: Colors.green))
            ],
          ),
        ],
      ),
    );
  }
}	 

Effect:

insert image description here

What is worth mentioning in the sample code above is the last two Row. After they are Alignset to widthFactor, 0.5the actual width of the picture is equal to 60×0.5half of the original width, but the overflow part of the picture will still be displayed at this time, so the first "Hello world" It will overlap with another part of the picture. In order to cut off the overflow part, we cut the overflow part Rowin the second one.ClipRect

custom crop

What if we want to crop a specific area of ​​the subcomponent, for example, in the picture in the example above, 40×30what should we do if we only want to capture the range of pixels in the middle of the picture? At this time, we can use custom CustomClipperto clip the area, and the implementation code is as follows:

class MyClipper extends CustomClipper<Rect> {
    
    

  
  Rect getClip(Size size) => Rect.fromLTWH(10.0, 15.0, 40.0, 30.0);

  
  bool shouldReclip(CustomClipper<Rect> oldClipper) => false;
}
  • getClip()is the interface used to get the clipping area. Since the size of the image is 60×60, we return the clipping area Rect.fromLTWH(10.0, 15.0, 40.0, 30.0)as 40×30the range of pixels in the middle of the image.

  • shouldReclip()The interface determines whether to re-clipping. If the clipping area never changes in the application, it should be returned false, so that re-clipping will not be triggered and unnecessary performance overhead will be avoided. If the clipping area will change (such as performing an animation on the clipping area), then you should go back trueand re-execute the clipping after the change.

Then, we ClipRectperform clipping by , in order to see the actual position of the picture, we set a red background:

DecoratedBox(
  decoration: BoxDecoration(
    color: Colors.red
  ),
  child: ClipRect(
    clipper: MyClipper(), //使用自定义的clipper
    child: avatar
  ),
)

Effect:

insert image description here

It can be seen that our clipping is successful, but the size of the space occupied by the picture is still 60×60(red area). This is because the size of the component is determined in the layout stage, and the clipping is performed in the subsequent drawing stage , so it will not Affects the size of the component, which Transformis similar to the principle.

ClipPathIt can be tailored according to a custom path. It needs to customize a CustomClipper<Path>type Clipper, and the definition method MyClipperis similar to that, except getClipthat needs to return one Path, so I won’t go into details.

FittedBox

When the size of the child component exceeds the size of the parent component, if it is not processed, an overflow warning will be displayed in Flutter and an error log will be printed on the console. For example, the following code will cause overflow:

Padding(
  padding: const EdgeInsets.symmetric(vertical: 30.0),
  child: Row(children: [Text('xx'*30)]), //文本长度超出 Row 的最大宽度会溢出
)

Effect:

insert image description here
You can see that there is an overflow of 45 pixels to the right.

The above is just an example. In theory, we often encounter situations where the size of a child element exceeds the size of its parent container. For example, a large image needs to be displayed in a smaller space. According to Flutter’s layout protocol, the parent component will The maximum display space of itself is passed to the child component as a constraint. The child component should abide by the constraint of the parent component. If the original size of the child component exceeds the constraint area of ​​the parent component, some reduction, cropping or other processing is required. Different components The processing method is specific. For example Text, if a component has a fixed width and an unlimited height, the text Textwill wrap by default when the text reaches the width of the parent component. So what if we want Textthe text to shrink instead of wrapping when it exceeds the width of the parent component? There is another situation, such as the width and height of the parent component is fixed, but Textthe text is less. At this time, what should we do if we want the text to enlarge to fill the entire space of the parent component?

In fact, the essence of the above two problems is: how does the child component adapt to the space of the parent component. According to the Flutter layout protocol, the adaptation algorithm should be implemented in the layout of the container or layout component . In order to facilitate developers to customize the adaptation rules, Flutter provides a FittedBoxcomponent, which is defined as follows:

const FittedBox({
    
    
  Key? key,
  this.fit = BoxFit.contain, // 适配方式
  this.alignment = Alignment.center, //对齐方式
  this.clipBehavior = Clip.none, //是否剪裁
  Widget? child,
})

Adaptation principle

  1. FittedBoxWhen laying out subcomponents , the constraints passed by their parent components are ignored , and subcomponents can be allowed to be infinitely large , that is, FittedBoxthe constraints passed to subcomponents are ( 0 <= width <= double.infinity, 0 <= height <= double.infinity).

  2. FittedBoxThe real size of the subcomponent can be obtained after the layout of the subcomponent is completed .

  3. FittedBoxKnowing the real size of the child component and the constraints of its parent component , then FittedBoxyou can use the specified adaptation method ( BoxFitspecified in the enumeration) to make the child component FittedBoxdisplay in the specified way within the constraints of the parent component.

We illustrate with a simple example:

Widget build(BuildContext context) {
    
    
  return Center(
    child: Column(
      children: [
        wContainer(BoxFit.none),
        Text('Flutter'),
        wContainer(BoxFit.contain),
        Text('Flutter'),
      ],
    ),
  );
}

Widget wContainer(BoxFit boxFit) {
    
    
  return Container(
    width: 50,
    height: 50,
    color: Colors.red,
    child: FittedBox(
      fit: boxFit,
      // 子容器超过父容器大小
      child: Container(width: 60, height: 70, color: Colors.blue),
    ),
  );
}

Effect:

insert image description here

Because the parent Containeris smaller than the child Container, when no fitting method is specified, the child component will be drawn according to its real size, so the first blue area will exceed the space of the parent component, so the red area cannot be seen. The second one we specified is the adaptation method BoxFit.contain, which means scaling according to the proportion of the child component, occupying as much space as possible in the parent component, because the length and width of the child components are not the same, so after scaling and adapting the parent component according to the proportion, the parent Components can display a part.

It should be noted that when the adaptation method is not specified, although FittedBoxthe size of the child component exceeds the space of FittedBoxthe parent , it still has to abide by the constraints passed by its parent component , so the final size of is , which is why the blue color will match The reason for the overlap of the text below is that in the layout space, the parent only occupies the size, and then the text will be laid out next to each other. At this time, the size of the subcomponents exceeds itself, so the final effect is that the drawing range exceeds the , but the layout position is normal, so it overlaps. If we don't want the blue to exceed the layout range of the parent component, we can use to clip the excess part:ContainerFittedBoxFittedBox50×50Container50×50TextContainerContainerContainerClipRect

ClipRect( // 将超出子组件布局范围的绘制内容剪裁掉
  child: Container(
    width: 50,
    height: 50,
    color: Colors.red,
    child: FittedBox(
      fit: boxFit,
      child: Container(width: 60, height: 70, color: Colors.blue),
    ),
  ),
);

Effect:

insert image description here

The BoxFitvarious adaptation rules of are the same as the attribute specification Imageof , you can check the corresponding effects of various adaptation rules when we introduce the component.fixImage

Example: Scale layout with single row

For example, we have three data indicators that need to be displayed in one line, because changing the line will disrupt our page layout, so line changing is unacceptable. Because the screen widths of different devices are different, and the data of different people is also different, so when the data is too long or the screen is too narrow, the three data cannot be displayed in one line. Appropriate scaling to ensure that a row can be displayed, for which we wrote a test demo:


  Widget build(BuildContext context) {
    
    
    return Center(
      child: Column(
        children:  [
	          wRow(' 90000000000000000 '),
	          FittedBox(child: wRow(' 90000000000000000 ')),
	          wRow(' 800 '),
	          FittedBox(child: wRow(' 800 ')),
	    	].map((e) => Padding(
	              padding: EdgeInsets.symmetric(vertical: 20),
	              child: e,
            )).toList();,
      ),
    );
  }

 // 直接使用Row
  Widget wRow(String text) {
    
    
    Widget child = Text(text);
    child = Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      children: [child, child, child],
    );
    return child;
  }

Effect:

insert image description here

First, because we Rowspecify the alignment on the main axis as MainAxisAlignment.spaceEvenly, this will divide the remaining display space in the horizontal direction into multiple parts interspersed between each child.

It can be seen that when the number is ' 90000000000000000', the combined length of the three numbers has exceeded the screen width of the test device, so the direct use Rowwill overflow, and when Rowadded FittedBox, it can be scaled to a line of display , to achieve the desired effect.

But when the number is not so large, such as the following ' ', it is okay 800to use directly , but although the last three numbers can be displayed normally, they are crowded together, which does not meet our expectations. The reason for this is actually very simple: in the case of specifying the alignment of the main axis as , the constraints of the parent component will be obtained during layout . If the parent constraint is not infinite, it will be based on the number of child components and their The size is divided in the main axis direction according to the fill algorithm to divide the length in the horizontal direction, and the final width is ; but if the parent constraint is infinite , it cannot be divided, so at this time the sum of the widths of the subcomponents will be used as own width.RowFittedBoxspaceEvenlyRowmaxWidthRowspaceEvenlyRowmaxWidthmaxWidthRow

Going back to the example, when Rowis not FittedBoxwrapped, Rowthe constraint passed to by maxWidththe parent component at this time is the screen width , at this time, Rowthe width of is also the screen width , and when it is FittedBoxwrapped, the constraint passed FittedBoxto is infinite ( ) , so the final width of the is the sum of the widths of the child components.RowmaxWidthdouble.infinityRow

LayoutLogPrintThe constraints passed by the parent component to the child component can be printed out with our previous package :

LayoutLogPrint(tag: 1, child: wRow(' 800 ')),
FittedBox(child: LayoutLogPrint(tag: 2, child: wRow(' 800 '))),

The console log after running is as follows:

flutter: 1: BoxConstraints(0.0<=w<=396.0, 0.0<=h<=Infinity)
flutter: 2: BoxConstraints(unconstrained)

If the cause of the problem is found, the solution is very simple. We only need to make the FittedBoxconstraint received by the sub-element maxWidthbe the width of the screen . For this reason, we encapsulate a SingleLineFittedBoxto replace FittedBoxto achieve our expected effect. The implementation is as follows:

class SingleLineFittedBox extends StatelessWidget {
    
    
 const SingleLineFittedBox({
    
    Key? key,this.child}) : super(key: key);
 final Widget? child;
  
  Widget build(BuildContext context) {
    
    
    return LayoutBuilder(
      builder: (_, constraints) {
    
    
        return FittedBox(
          child: ConstrainedBox(
            constraints: constraints.copyWith( 
              maxWidth: constraints.maxWidth //让 maxWidth 使用屏幕宽度
            ),
            child: child,
          ),
        );
      },
    );
  }
}

Change the test code to:

wRow(' 90000000000000000 '),
SingleLineFittedBox(child: wRow(' 90000000000000000 ')),
wRow(' 800 '),
SingleLineFittedBox(child: wRow(' 800 ')),

running result:

insert image description here

The one found 800displayed normally, but the one SingleLineFittedBoxwrapped with ' 90000000000000000' Rowoverflowed! The reason for the overflow is actually very simple, because after we SingleLineFittedBoxset the passed to as the screen width in the Row, maxWidththe effectSingleLineFittedBox is the same as without adding , Rowand the parent component is constrained by maxWidththe screen width, so it took a long time to realize it lonely. However, don’t give up. In fact, we are only one step away from victory. As long as we make a little modification, we can achieve our expectations. Without further ado, let’s go directly to the code:

class SingleLineFittedBox extends StatelessWidget {
    
    
  const SingleLineFittedBox({
    
    Key? key,this.child}) : super(key: key);
  final Widget? child; 
  
  Widget build(BuildContext context) {
    
    
    return LayoutBuilder(
      builder: (_, constraints) {
    
    
        return FittedBox(
          child: ConstrainedBox(
            constraints: constraints.copyWith(
              minWidth: constraints.maxWidth,
              maxWidth: double.infinity,
              // maxWidth: constraints.maxWidth
            ),
            child: child,
          ),
        );
      },
    );
  }
}

The code is very simple. We specify the minimum width ( minWidth) constraint as the screen width , because the constraints of the parent componentRow must be obeyed , so the width of is at least equal to the screen width , so there will be no shrinkage; at the same time, we specify as infinite Larger , you can handle the case where the total length of the numbers exceeds the screen width .RowmaxWidth

The effect after re-running is as shown in the figure:

insert image description here

Find out that our SingleLineFittedBox works fine for both long and short numbers, and you're done!

OverflowBox

OverflowBoxThe role is to allow the child to display beyond the scope of the parent .

OverflowBoxDefinition:

OverflowBox({
    
    
	Key key, this.alignment = Alignment.center,//对齐方式。
	this.minWidth,//允许 child 的最小宽度。如果 child 宽度小于这个值,则按照最小宽度进行显示。
	this.maxWidth,//允许 child 的最大宽度。如果 child 宽度大于这个值,则按照最大宽度进行展示。
	this.minHeight,//允许 child 的最小高度。如果 child 高度小于这个值,则按照最小高度进行显示。
	this.maxHeight,//允许 child 的最大高度。如果 child 高度大于这个值,则按照最大高度进行展示。
	Widget child, 
})
  • When OverflowBoxthe maximum size of is larger than child, childit can be displayed completely,
  • When OverflowBoxthe maximum size of is smaller childthan , the maximum size is used as the benchmark. Of course, this size can break through the parent node.
  • When the minimum and maximum width and height are null, take the parent node constraintinstead.

Example:

 class OverflowBoxWidget extends StatelessWidget {
    
    
  const OverflowBoxWidget({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Container(
            color: Colors.yellow,
            width: 200.0,
            height: 200.0,
            padding: const EdgeInsets.all(5.0), 
            child: OverflowBox(
              alignment: Alignment.topLeft,
              maxWidth: 300.0,
              maxHeight: 300.0,
              child: Container(
                color: const Color(0x33FF00FF),
                width: 400.0,
                height: 500.0,
              ), 
           ),
     );
  }
}

Effect:

insert image description here

Visible OverflowBoxchild elements are shown in the yellow area whose 300x300size overflows the parent element .200x200

If the code is modified to:


  Widget build(BuildContext context) {
    
    
    return Container(
            color: Colors.yellow,
            width: 200.0,
            height: 200.0,
            padding: const EdgeInsets.all(5.0), 
            child: OverflowBox(
              alignment: Alignment.topLeft,
              minWidth: 150.0,
              maxWidth: 300.0,
              minHeight: 150.0,
              maxHeight: 300.0,
              child: Container(
                color: const Color(0x33FF00FF),
                width: 100.0,
                height: 100.0,
              ), 
           ),
     );
  }

Effect:

insert image description here

OverflowBoxThe child elements that can be seen at this time will be 150x150displayed in 200x200the yellow area of ​​​​the parent element with a size of .

Material App and Scaffold

Flutter provides a rich and powerful set of basic components. On top of the basic component library, Flutter provides a set of Material-style (Android default visual style) and a set of Cupertino-style (iOS visual style) component libraries. To use the base component library, you need to import:

import 'package:flutter/widgets.dart';

Developers use MaterialAppand Scaffoldtwo components to decorate the App.

Material App

MaterialAppis a convenience Widgetthat encapsulates some of what an app needs to implement Material DesignWidget . Generally widgetused as a top layer.

MaterialAppCommon attributes:

  • home(home page)
  • title(title)
  • color(color)
  • theme(theme)
  • routes(routing)
  • ...

For example, by MaterialAppconfiguring a global theme :

MaterialApp( 
    title: 'Flutter Demo',
    theme: ThemeData(
        primarySwatch: Colors.blue,
        appBarTheme: const AppBarTheme(
          centerTitle: true,
        )
    ),
    initialRoute: "/",
    routes: routes,
    debugShowCheckedModeBanner: false,
 );

Scaffold

ScaffoldIt is a scaffold to realize the basic layout structure of Material Design . This class provides APIs for display drawer, snackbarand bottom .sheet

ScaffoldIt has the following main properties:

  • appBar- The one displayed at the top of the interface AppBar.
  • body- The main content displayed on the current interface Widget.
  • drawer- Drawer menu control.
  • ...

Here's Scaffolda simple example implemented using :

class ScaffoldRoute extends StatefulWidget {
    
    
  
  _ScaffoldRouteState createState() => _ScaffoldRouteState();
}

class _ScaffoldRouteState extends State<ScaffoldRoute> {
    
    
  int _selectedIndex = 1;

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar( //导航栏
        title: Text("App Name"), 
        actions: <Widget>[ //导航栏右侧菜单
          IconButton(icon: Icon(Icons.share), onPressed: () {
    
    }),
        ],
      ),
      drawer: MyDrawer(), //抽屉
      bottomNavigationBar: BottomNavigationBar( // 底部导航
        items: <BottomNavigationBarItem>[
          BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
          BottomNavigationBarItem(icon: Icon(Icons.business), title: Text('Business')),
          BottomNavigationBarItem(icon: Icon(Icons.school), title: Text('School')),
        ],
        currentIndex: _selectedIndex,
        fixedColor: Colors.blue,
        onTap: _onItemTapped,
      ),
      floatingActionButton: FloatingActionButton( //悬浮按钮
          child: Icon(Icons.add),
          onPressed:_onAdd
      ),
    );
  }
  void _onItemTapped(int index) {
    
    
    setState(() {
    
    
      _selectedIndex = index;
    });
  }
  void _onAdd(){
    
    
  }
}

The running effect is as follows:

insert image description here

In the above code we used the following components:

  • AppBarA navigation bar skeleton
  • MyDrawerdrawer menu
  • BottomNavigationBarbottom navigation bar
  • FloatingActionButtonfloating button

AppBar

AppBarIt is a Material -style navigation bar, through which you can set the title of the navigation bar, the menu of the navigation bar, Tabthe title at the bottom of the navigation bar, etc. Let's look at the definition below AppBar:

AppBar({
    
    
  Key? key,
  this.leading, //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
  this.automaticallyImplyLeading = true, //如果leading为null,是否自动实现默认的leading按钮
  this.title,// 页面标题
  this.actions, // 导航栏右侧菜单
  this.bottom, // 导航栏底部菜单,通常为Tab按钮组
  this.elevation = 4.0, // 导航栏阴影
  this.centerTitle, //标题是否居中 
  this.backgroundColor,
  ...   //其他属性见源码注释
})
Attributes describe
leading A control displayed in front of the title, usually displays the logo of the application on the home page; it is usually displayed as a back button on other interfaces
title Title, usually displayed as the title text of the current interface, you can put components
actions Usually represented by IconButton, you can put button groups
bottom Usually tabBar is placed, and a Tab navigation bar is displayed under the title
backgroundColor Navigation background color
iconTheme icon style
centerTitle Whether the title is displayed in the center

Simple example:

AppBar(
   //导航栏
   title: const Text("App Name"),
   actions: <Widget>[
     //导航栏右侧菜单
     IconButton(
         icon: const Icon(Icons.share), onPressed: () => print("share")),
   ],
   //导航栏最左侧Widget,常见为抽屉菜单按钮或返回按钮。
   leading: Builder(builder: (context) {
    
    
     return IconButton(
         icon: const Icon(Icons.dashboard, color: Colors.white), //自定义图标
         onPressed: () => Scaffold.of(context).openDrawer() // 打开抽屉菜单
         );
   }),
   //顶部导航栏下面的Tab菜单 tabbar本身支持横向滚动
   bottom: TabBar(
       controller: _tabController,
       tabs: tabs.map((e) => Tab(text: e)).toList() //Tab可添加图标
       ),
),

If Scaffolda drawer menu is added, by default Scaffoldit will be automatically AppBarset leadingas a menu button, and clicking it will open the drawer menu. If we want to customize the menu icon, we can set it manually leading, such as:

Scaffold(
  appBar: AppBar(
    title: Text("App Name"),
    leading: Builder(builder: (context) {
    
    
      return IconButton(
        icon: Icon(Icons.dashboard, color: Colors.white), //自定义图标
        onPressed: () {
    
    
          // 打开抽屉菜单  
          Scaffold.of(context).openDrawer(); 
        },
      );
    }),
    ...  
  )  

Code running effect:
insert image description here
You can see that the left menu has been replaced successfully.

The method of opening the drawer menu in the code is in ScaffoldState, through which the object of Scaffold.of(context)the parent's nearest Scaffoldcomponent can be obtained State.

TabBar + TabBarView

AppBarCombining TabBarcan achieve top Tabswitching effect.

insert image description here
TabBarThere are many configuration parameters, through which we can define TabBarthe style of , many attributes are configured indicatorand label, take the above picture as an example, Labelit is Tabthe text of each , which indicatorrefers to the white underline under "History".

TabBarCommon attributes:

Attributes describe
tabs The content of the displayed label, generally using the Tab object, can also be other Widgets
controller TabController object
isScrollable Is it scrollable
indicatorColor indicator color
indicatorWeight indicator height
indicatorPadding Padding of the bottom indicator
indicator Indicator decoration, such as borders, etc.
indicatorSize 指示器大小计算方式,TabBarIndicatorSize.label跟文字等宽,TabBarIndicatorSize.tab跟每个tab等宽
labelColor 选中label颜色
labelStyle 选中label的Style
labelPadding 每个label的padding值
unselectedLabelColor 未选中label颜色
unselectedLabelStyle 未选中label的Style

TabBarView 是 Material 组件库中提供了 Tab 布局组件,通常和 TabBar 配合使用。TabBarView 内部封装了 PageView,它的构造方法很简单:

 TabBarView({
    
    
  Key? key,
  required this.children, // tab 页
  this.controller, // TabController
  this.physics,
  this.dragStartBehavior = DragStartBehavior.start,
}) 

TabController 用于监听和控制 TabBarView 的页面切换,通常和 TabBar 联动。

TabBar 通常位于 AppBar 的底部(bottom属性),它也可以接收一个 TabController ,如果需要和 TabBarView 联动, TabBarTabBarView 使用同一个 TabController 即可,注意,联动时 TabBarTabBarView 的孩子数量需要一致。如果没有指定 controller,则会在组件树中向上查找并使用最近的一个 DefaultTabController

另外我们需要创建需要的 tab 并通过 tabs 传给 TabBartab 可以是任何 Widget,不过Material 组件库中已经实现了一个 Tab 组件,我们一般都会直接使用它:

const Tab({
    
    
  Key? key,
  this.text, //文本
  this.icon, // 图标
  this.iconMargin = const EdgeInsets.only(bottom: 10.0),
  this.height,
  this.child, // 自定义 widget
})

注意,textchild互斥的,不能同时指定。

使用AppBar结合 Tabbar + TabBarView实现类似头条顶部导航效果

1、混入SingleTickerProviderStateMixin

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin{
    
    }

由于 TabController 需要一个 TickerProvidervsync 参数), 所以我们需要混入 SingleTickerProviderStateMixin这个类。

2、定义TabController

  late TabController _tabController;
  
  void initState() {
    
    
    super.initState();
    _tabController = TabController(length: 8, vsync: this);
    _tabController.addListener(() {
    
    
      if (_tabController.animation!.value == _tabController.index) {
    
    
        print(_tabController.index); //获取点击或滑动页面的索引值
      }
    });
  }

3、配置TabBarTabBarView

import 'package:flutter/material.dart';

void main() {
    
    
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
    
    
  const MyApp({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
    
    
  const HomePage({
    
    Key? key}) : super(key: key);
  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
    
    
  late TabController _tabController;
  
  void initState() {
    
    
    super.initState();
    _tabController = TabController(length: 3, vsync: this);
  }

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
        appBar: AppBar(
          title: const Text("Flutter App"),
          bottom: TabBar(
            controller: _tabController,
            tabs: const [
              Tab(child: Text("热门")),
              Tab(child: Text("推荐")),
              Tab(child: Text("视频"))
            ],
          ),
        ),
        body: TabBarView(
            controller: _tabController,
            children: const [Text("热门"), Text("推荐"), Text("视频")])
      );
  }
	
  
  void dispose() {
    
    
    // 释放资源
    _tabController.dispose();
    super.dispose();
  }	
}

注意,TabBar是放在ScaffoldAppBar中的,而TabBarView 是放在Scaffoldbody中的。

上面代码中,由于创建 TabController 需要一个vsync 参数所以需要混入一个类,由于 TabController 中会执行动画,持有一些资源,所以我们在页面销毁时必须得释放资源(dispose)。

我们发现创建 TabController 的过程还是比较复杂,实战中,如果需要 TabBarTabBarView 联动,通常会创建一个 DefaultTabController 作为它们共同的父级组件,这样它们在执行时就会从组件树向上查找,都会使用我们指定的这个 DefaultTabController。我们可以这样做:

class HomePage extends StatelessWidget {
    
    
  
  const HomePage({
    
    super.key});
  
  
  Widget build(BuildContext context) {
    
    
    List tabs = ["新闻", "历史", "图片"];
    return DefaultTabController(
      length: tabs.length,
      child: Scaffold(
        appBar: AppBar(
          title: const Text("App Name"),
          bottom: TabBar(tabs: tabs.map((e) => Tab(text: e)).toList(),),
        ),
        body: TabBarView( //构建
          children: tabs.map((e) {
    
    
            return KeepAliveWrapper(
              child: Container(
                alignment: Alignment.center,
                child: Text(e, textScaleFactor: 5),
              ),
            );
          }).toList(),
        ),
      ),
    );
  }
}

这样省力一些。但是这样无法通过Controller拿到tab变化的index。

解决“双下巴”标题

假如我们把上面的HomePage组件嵌入到main.dart中的MyApp组件中,可能出现下图的“双下巴”顶部标题:

insert image description here

//main.dart
class MyApp extends StatelessWidget {
    
    
  const MyApp({
    
    Key? key}) : super(key: key);
  
  Widget build(BuildContext context) {
    
    
    return MaterialApp(
      title: 'Flutter App',
      theme: ThemeData(primarySwatch: Colors.blue,),
      home: HomePage(),
    );
  }
}

如何去掉这个“双下巴”呢?我们可以将HomePage中原本放在AppBarbottom属性上的TabBar放到AppBartitle属性上即可,然后去掉bottom属性:

Scaffold(
   appBar: AppBar(
      title: TabBar(
        controller: _tabController,
        tabs: const [...],
      ),
    ),
    body: TabBarView(...),
);

这样就去掉了子组件的标题,只有最外面的标题了。

insert image description here

PreferredSize改变appBar的高度

通过PreferredSize组件可以修改appBar的默认高度:

Scaffold(
  appBar: PreferredSize(
      preferredSize: Size.fromHeight(50),
      child: AppBar(
      ....
      )
  ),
  body: Test(),
)

监听 TabController 的改变事件

void initState() {
    
    
  super.initState();
  _tabController = TabController(length: 8, vsync: this);
  //监听_tabController的改变事件
  _tabController.addListener(() {
    
    
  	// print(_tabController.index); // 如果直接在这里获取会执行2次
    if (_tabController.animation!.value==_tabController.index){
    
    
      print(_tabController.index); //获取点击或滑动页面的索引值
    }
  });
}

注意,上面代码中如果直接在addListener回调中获取_tabController.index会执行2次,所以需要加上判断。而如果你在TarBaronTap点击回调中获取index,则只能获取到点击触发切换tab时的index,当Tab由于用户滚动改变index时就不能获取到了,所以最佳位置就是放在tabController.addListener中。获取到这个index后,我们就可以在tab切换时去请求数据了,或者做其他业务逻辑。

解决 TabView 的页面缓存问题

由于 TabBarView 内部封装了 PageView,存在页面缓存失效问题,会导致在进行Tab切换时丢失页面状态。比如,假如 TabBarView 的内容是一个ListView列表,当用户在当前Tab页面滑动ListView到某个位置时,切换到了其他Tab页面浏览,然后再次返回当前Tab页,此时会发现列表滚动位置失效,回到初始状态,即当前Tab被销毁重建了。

import 'package:flutter/material.dart';
import '../../tools/KeepAliveWrapper.dart';
 
class HomePage extends StatefulWidget {
    
    
  const HomePage({
    
    super.key});

  
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage>
    with SingleTickerProviderStateMixin {
    
    
  late TabController _tabController;
  
  void initState() {
    
    
    super.initState();
    _tabController = TabController(length: 8, vsync: this);
    //监听_tabController的改变事件
    _tabController.addListener(() {
    
    
      // print(_tabController.index);
      if (_tabController.animation!.value == _tabController.index) {
    
    
        print(_tabController.index); //获取点击或滑动页面的索引值
      }
    });
  }

  //组件销毁的时候触发
  
  void dispose() {
    
    
    // TODO: implement dispose
    super.dispose();
    //销毁_tabController
    _tabController.dispose();
  }

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: PreferredSize(
        //可以配置appBar的高度
        preferredSize: const Size.fromHeight(40),
        child: AppBar(
          elevation: 0.5,
          backgroundColor: Colors.white,
          title: SizedBox(
            //改TabBar的高度
            height: 30,
            child: TabBar(
              labelStyle: const TextStyle(fontSize: 14),
              isScrollable: true,
              indicatorColor: Colors.red, //底部指示器的颜色
              labelColor: Colors.red,
              unselectedLabelColor: Colors.black, //lable未选中的颜色
              indicatorSize: TabBarIndicatorSize.label,
              controller: _tabController,
              // onTap: (index){   //只能监听点击事件 没法监听滑动
              //   print(index);
              // },
              tabs: const [
                Tab(child: Text("关注"),),
                Tab(child: Text("热门"),),
                Tab(child: Text("视频"),),
                Tab(child: Text("娱乐"),),
                Tab(child: Text("篮球"),),
                Tab(child: Text("深圳"),),
                Tab(child: Text("疫情"),),
                Tab(child: Text("其他"),),
              ],
            ),
          ),
        ),
      ),
      body: TabBarView(controller: _tabController, children: [
        //自定义的缓存组件
        KeepAliveWrapper(
            child: ListView(
              children: List.generate(30, (index) => ListTile(title: Text("关注列表$index")))
            )
        ),
        KeepAliveWrapper(
            child: ListView(
                children: List.generate(30, (index) => ListTile(title: Text("热门列表$index")))
            )
        ),
        KeepAliveWrapper(
            child: ListView(
                children: List.generate(30, (index) => ListTile(title: Text("视频列表$index")))
            )
        ),
        KeepAliveWrapper(
            child: ListView(
                children: List.generate(30, (index) => ListTile(title: Text("娱乐列表$index")))
            )
        ),
        KeepAliveWrapper(
            child: ListView(
                children: List.generate(30, (index) => ListTile(title: Text("篮球列表$index")))
            )
        ),
        KeepAliveWrapper(
            child: ListView(
                children: List.generate(30, (index) => ListTile(title: Text("深圳列表$index")))
            )
        ),
        KeepAliveWrapper(
            child: ListView(
                children: List.generate(30, (index) => ListTile(title: Text("疫情列表$index")))
            )
        ),
        KeepAliveWrapper(
            child: ListView(
                children: List.generate(30, (index) => ListTile(title: Text("其他列表$index")))
            )
        ),
      ]),
    );
  }
}

其中KeepAliveWrapper是对AutomaticKeepAlive的一个简单封装,具体请参考后文 可滚动组件 部分中如何开启滚动组件的子项缓存。假如不使用KeepAliveWrapper包装TabBarViewchildren,那么当从当前Tab切换到其他Tab再返回当前Tab页时,会发生列表滚动位置失效问题,即当前Tab被销毁重建了。

BottomNavigationBar

我们通过Material组件库提供的BottomNavigationBarBottomNavigationBarItem两种组件来实现Material风格的底部导航栏。它可以让我们定义底部Tab切换。可以用过
Scaffold组件的参数bottomNavigationBar来配置BottomNavigationBar

BottomNavigationBar 常见的属性:

属性 描述
items List底部导航条按钮集合
iconSize icon
currentIndex 默认选中第几个
onTap 选中变化回调函数
fixedColor 选中的颜色
type BottomNavigationBarType.fixed、BottomNavigationBarType.shifting

简单示例:

import 'package:flutter/material.dart';

void main() {
    
    
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
    
    
  const MyApp({
    
    Key? key}) : super(key: key);
  
  
  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 Center(
            child: Text("我是一个文本"),
          ),
          bottomNavigationBar: BottomNavigationBar(
              items: const [
	              BottomNavigationBarItem(
		              icon: Icon(Icons.home),
		              label: "首页"
		          ),
		          BottomNavigationBarItem(
		              icon: Icon(Icons.category),
		              label: "分类"
		          ),
		          BottomNavigationBarItem(
		              icon: Icon(Icons.settings),
		              label: "设置"
		          ) ]
		     ),
    	),
    );
  }
}

点击底部Tab的时候实现Tab切换:

import 'package:flutter/material.dart';

void main() {
    
    
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
    
    
  const MyApp({
    
    Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const Tabs(),
    );
  }
}

class Tabs extends StatefulWidget {
    
    
  const Tabs({
    
    super.key});
  
  State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
    
    
  int _currentIndex = 0;

  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(title: const Text("Flutter App")),
      body: const Center(
        child: Text("我是一个文本"),
      ),
      bottomNavigationBar: BottomNavigationBar(
          currentIndex: _currentIndex,
          onTap: (index) => setState(() {
    
    _currentIndex = index;}), // 点击时更新index
          items: const [
            BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
            BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
            BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
          ]
      ),
    );
  }
}

这样我们就可以拿到底部Tab点击切换的index,拿到这个index后,就可以实现根据index变化动态切换Scaffold中的body页面了,比如:

Scaffold( 
    body: _pages[_currentIndex], // index状态改变时这里自动切换body
    bottomNavigationBar: BottomNavigationBar(
      currentIndex:_currentIndex,  //第几个菜单选中 
      onTap: (index) => setState(() {
    
    _currentIndex = index;}), // 点击时更新index
      items: ...
    ),
);

完整代码如下:

import 'package:flutter/material.dart';
import './tabs/home.dart';
import './tabs/category.dart';
import './tabs/setting.dart';
import './tabs/user.dart';

class Tabs extends StatefulWidget {
    
    
  const Tabs({
    
    super.key});

  
  State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
    
    
  int _currentIndex=0;
  final List<Widget> _pages=const [ // 使用一个List保存不同的页面组件
    HomePage(),
    CategoryPage(),
    SettingPage(),
    UserPage()
  ];
  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
        appBar: AppBar(title: const Text("Flutter App")),
        body: _pages[_currentIndex],
        bottomNavigationBar: BottomNavigationBar(
          fixedColor:Colors.red,  //选中的颜色
          iconSize:35,           //底部菜单大小
          currentIndex:_currentIndex,  //第几个菜单选中
          type:BottomNavigationBarType.fixed,  //如果底部有4个或者4个以上的菜单的时候就需要配置这个参数
          onTap: (index) => setState(() {
    
    _currentIndex = index;}), // 点击时更新index
          items: const [
            BottomNavigationBarItem(
              icon:Icon(Icons.home),
              label: "首页"
            ),
            BottomNavigationBarItem(
              icon:Icon(Icons.category),
              label: "分类"
            ),
            BottomNavigationBarItem(
              icon:Icon(Icons.settings),
              label: "设置"
            ),
             BottomNavigationBarItem(
              icon:Icon(Icons.people),
              label: "用户"
            )
        ]),
      );
  }
}

上面代码中还有一个点需要特别注意:当底部Tab的数量 ≥ 4 时,BottomNavigationBartype属性必须设置为BottomNavigationBarType.fixed,否则不会显示

FloatingActionButton

FloatingActionButton简称FAB , 是Material设计规范中的一种特殊Button,可以实现悬浮按钮,通常用于悬浮在页面的某一个位置作为某种常用动作的快捷入口,也可以实现类似闲鱼app的底部凸起效果。

我们可以通过ScaffoldfloatingActionButton属性来设置一个FloatingActionButton,同时通过floatingActionButtonLocation属性来指定其在页面中悬浮的位置。

FloatingActionButton的常用属性:

属性 描述
child 子视图,一般为Icon,不推荐使用文字
tooltip FAB被长按时显示,也是无障碍功能
backgroundColor 背景颜色
elevation 未点击的时候的阴影
hignlightElevation 点击时阴影值,默认12.0
onPressed 点击事件回调
shape 可以定义FAB的形状等
mini 是否是mini类型默认false

实现 App 底部导航栏凸起 Tab 效果

使用BottomNavigationBar实现常规的底部导航栏非常简单,但是如果我们想实现如下图所示的底部突出Tab效果应该怎么做呢?
insert image description here

Material组件库中提供了一个BottomAppBar 组件,它可以和FloatingActionButton配合实现这种“打洞”效果,源码如下:

bottomNavigationBar: BottomAppBar(
  color: Colors.white,
  shape: CircularNotchedRectangle(), // 底部导航栏打一个圆形的洞
  child: Row(
    children: [
      IconButton(icon: Icon(Icons.home)),
      SizedBox(), //中间位置空出
      IconButton(icon: Icon(Icons.business)),
    ],
    mainAxisAlignment: MainAxisAlignment.spaceAround, //均分底部导航栏横向空间
  ),
)

可以看到,上面代码中没有控制打洞位置的属性,实际上,打洞的位置取决于FloatingActionButton的位置,下面代码设置打洞位置在底部导航栏的正中间:

floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,

BottomAppBarshape属性决定洞的外形,CircularNotchedRectangle实现了一个圆形的外形,我们也可以自定义外形,比如,Flutter Gallery 示例中就有一个“钻石”形状的示例,读者感兴趣可以自行查看。

Flutter Gallery 是Flutter官方提供的 Flutter Demo, 它是一个很全面的Flutter示例应用,是非常好的参考Demo,该示例应用也是官方为了给初次打算入手Flutter的技术团队提供评估。

也许不一定要打洞,另一种实现方式是直接将FloatingActionButton盖在底部的BottomNavigationBar上面,例如闲鱼App的底部突出tab明显不是打洞:

insert image description here

要实现这种效果,还是要将floatingActionButtonLocation设置为centerDocked,不过实现代码稍微麻烦一点:

import 'package:flutter/material.dart';
import './tabs/home.dart';
import './tabs/category.dart';
import './tabs/message.dart';
import './tabs/setting.dart';
import './tabs/user.dart';

class Tabs extends StatefulWidget {
    
    
  const Tabs({
    
    super.key});

  
  State<Tabs> createState() => _TabsState();
}

class _TabsState extends State<Tabs> {
    
    
  int _currentIndex = 0;
  final List<Widget> _pages = const [
    HomePage(),
    CategoryPage(),
    MessagePage(),
    SettingPage(),
    UserPage()
  ];
  
  Widget build(BuildContext context) {
    
    
    return Scaffold(
      appBar: AppBar(title: const Text("Flutter App")),
      body: _pages[_currentIndex],
      bottomNavigationBar: BottomNavigationBar(
          fixedColor: Colors.red, //选中的颜色
          // iconSize:35,           //底部菜单大小
          currentIndex: _currentIndex, //第几个菜单选中
          type: BottomNavigationBarType.fixed, //如果底部有4个或者4个以上的菜单的时候就需要配置这个参数
          onTap: (index) => setState(() {
    
    _currentIndex = index;}), // 点击时更新index
          items: const [
            BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
            BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
            BottomNavigationBarItem(icon: Icon(Icons.message), label: "消息"),
            BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
            BottomNavigationBarItem(icon: Icon(Icons.people), label: "用户")
          ]),
      floatingActionButton: Container(
        height: 60,  //调整FloatingActionButton的大小
        width: 60,
        padding: const EdgeInsets.all(5),
        margin: const EdgeInsets.only(top: 5),  //调整FloatingActionButton的位置
        decoration: BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.circular(30),
        ),
        child: FloatingActionButton(
            backgroundColor:_currentIndex==2?Colors.red:Colors.blue,
            child: const Icon(Icons.add), 
            onPressed: () {
    
    
              setState(() {
    
    
                _currentIndex=2;
              });
            }
        ),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, //配置浮动按钮的位置
    );
  }
}

这种实现需要设置奇数个tab,中间专门留出一个tab给FloatingActionButton覆盖。

Drawer

ScaffolddrawerThe and properties of the component endDrawercan accept one respectively Widgetas the left and right drawer menus of the page. If the developer provides a drawer menu, the drawer menu can be opened when the user's finger slides in from the left (or right) side of the screen. (The sidebar is hidden by default, we can display the sidebar by sliding our fingers, or click the button to display the sidebar)

DefaultTabController(
   Scaffold(
     appBar: AppBar(title: Text("Flutter App"),),
     drawer: Drawer(child: Text('左侧边栏'),),
     endDrawer: Drawer(child: Text('右侧侧边栏'),),
);

use DrawerHeadercomponent

Attributes describe
decoration set top background color
child configuration child element
padding Padding
margin Margin

Sample code:

Drawer(
  child: Column(
    children: [
    	Row(children: [Expanded(
              flex: 1,
              child: DrawerHeader(
                decoration: const BoxDecoration(
                    // color: Colors.yellow,
                    image: DecorationImage(
                        image: NetworkImage(
                            "https://www.itying.com/images/flutter/2.png"),
                        fit: BoxFit.cover)),
                child: Column(
                  children: const [
                    ListTile(
                      leading: CircleAvatar(
                        backgroundImage:NetworkImage("https://www.itying.com/images/flutter/3.png")                             
                      ),
                      title: Text("张三",style: TextStyle(
                        color: Colors.red
                      )),
                    ),
                    ListTile(
                      title: Text("邮箱:[email protected]"),
                    )
                  ],
                ),
              ))
        ],
      ),
      const ListTile(
        leading: CircleAvatar(
          child: Icon(Icons.people),
        ),
        title: Text("个人中心"),
      ),
      const Divider(),
      const ListTile(
        leading: CircleAvatar(
          child: Icon(Icons.settings),
        ),
        title: Text("系统设置"),
      ),
      Divider(),
    ],
  ),
),

Effect:

insert image description here

use UserAccountsDrawerHeadercomponent

Attributes describe
decoration set top background color
accountName account name
accountEmail Account email
currentAccountPicture profile picture
otherAccountsPictures It is used to set the avatar of other accounts of the current account
margin

Sample code:

Drawer(
  child: Column(
    children: [
      Row(children: [Expanded(
              flex: 1,
              child: UserAccountsDrawerHeader(
                accountName: const Text("itying"),
                accountEmail: const Text("[email protected]"),
                otherAccountsPictures:[
                  Image.network("https://www.itying.com/images/flutter/1.png"),
                     Image.network("https://www.itying.com/images/flutter/2.png"),
                     Image.network("https://www.itying.com/images/flutter/3.png"),
                ],
                currentAccountPicture:const CircleAvatar(
                  backgroundImage:NetworkImage("https://www.itying.com/images/flutter/3.png")
                ),
                decoration: const BoxDecoration(
                    image: DecorationImage(
                      fit: BoxFit.cover,
                        image: NetworkImage(
                            "https://www.itying.com/images/flutter/2.png"))),
              ))
        ],
      ),
      const ListTile(
        leading: CircleAvatar(
          child: Icon(Icons.people),
        ),
        title: Text("个人中心"),
      ),
      const Divider(),
      const ListTile(
        leading: CircleAvatar(
          child: Icon(Icons.settings),
        ),
        title: Text("系统设置"),
      ),
      const Divider(),
    ],
  ),
),

Effect:

insert image description here

HeaderIt is also possible to completely define the content of the component without using the two components provided by the system Drawer, because its childproperties can be set to any Widget, for example:

class MyDrawer extends StatelessWidget {
    
    
  const MyDrawer({
    
    
    Key? key,
  }) : super(key: key);

  
  Widget build(BuildContext context) {
    
    
    return Drawer(
      child: MediaQuery.removePadding(
        context: context,
        //移除抽屉菜单顶部默认留白
        removeTop: true,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Padding(
              padding: const EdgeInsets.only(top: 38.0),
              child: Row(
                children: <Widget>[
                  Padding(
                    padding: const EdgeInsets.symmetric(horizontal: 16.0),
                    child: ClipOval(
                      child: Image.asset(
                        "imgs/avatar.png",
                        width: 80,
                      ),
                    ),
                  ),
                  Text(
                    "Wendux",
                    style: TextStyle(fontWeight: FontWeight.bold),
                  )
                ],
              ),
            ),
            Expanded(
              child: ListView(
                children: <Widget>[
                  ListTile(
                    leading: const Icon(Icons.add),
                    title: const Text('Add account'),
                  ),
                  ListTile(
                    leading: const Icon(Icons.settings),
                    title: const Text('Manage accounts'),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}

In the above code , some default blank space MediaQuery.removePaddingcan be removed (for example, the top of the Drawer will leave a blank space at the same height as the status bar of the mobile phone by default), and you can try to pass different parameters to see the actual effect. DrawerThe drawer menu page is composed of top and bottom, the top is composed of user avatar and nickname, and the bottom is a menu list, which is ListViewimplemented with .


Reference: "Flutter Combat Second Edition"

Guess you like

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