查看原文
其他

Flutter必备 | Flex布局完全解读

张风捷特烈 搜狐技术产品 2021-01-15

☝点击上方蓝字,关注我们!


本文字数:4894

预计阅读时间:30分钟


作者介绍

本期特邀作者:张风捷特烈

掘金社区优秀作者,技术领域主要以Android为中心,兼顾涉猎全栈以及多语言。目前正全力研究flutter框架;热爱技术分享,常在掘金社区分享原创文章。


导读

Flutter作为一个跨平台的框架,最能展现它实力的当属界面了,一套界面可以同时运用在多个终端,这是一件非常令人兴奋的事。进入Flutter的世界,你的第一道拦路虎当属布局了,庞大的组件家族让你望而却步,但将他们进行梳理之后你就会发现,这是非常友善的。你可以在需要的时候使用相应的单体组件,需要排布的时候也会有众多的布局组件为你做后盾。 

Flutter中的Flex布局作为五虎上将之一,当然虎父无犬子,其子Row和Column也能力非凡,使用时你有没有被mainAxisAlignment、crossAxisAlignment弄得晕头转向?本文将助你把他们纳入麾下,成为你布局战场上的猛将而非敌人。

我们先看一下父子三人在Flutter布局体系中的位置:多子组件布局


01

看一下Flex家族在源码中的位置

Flex家族包括Flex组件、Row组件、Column组件,位于widgets包中的basic文件中。

1---->[flutter/lib/src/widgets/basic.dart:3677]----
2class Flex extends MultiChildRenderObjectWidget {
3    Flex({
4        Key key,
5        @required this.direction,
6        this.mainAxisAlignment = MainAxisAlignment.start,
7        this.mainAxisSize = MainAxisSize.max,
8        this.crossAxisAlignment = CrossAxisAlignment.center,
9        this.textDirection,
10        this.verticalDirection = VerticalDirection.down,
11        this.textBaseline,
12        List<Widget> children = const <Widget>[],
13      }) : assert(direction != null),
14
15---->[flutter/lib/src/widgets/basic.dart:4015]----
16class Row extends Flex {
17
18---->[flutter/lib/src/widgets/basic.dart:4213]----
19class Column extends Flex {


02

Flex的属性一览


03

轴向:direction:Axis

1enum Axis {
2  horizontal,//水平
3  vertical,//竖直
4}

也就是水平排放还是竖直排放,可以看出默认情况下都是主轴顶头,交叉轴居中,比如horizontal下,主轴为水平轴,交叉轴则为竖直,也就是水平顶头,竖直居中。这里使用MultiShower快速展示,更好地对比出不同之处,MultiShower详见(https://juejin.im/post/5d2842ac6fb9a07eb15d82ad)。



1var direction =[Axis.horizontal,Axis.vertical];
2var show = MultiShower(direction,(e){
3  return Flex(
4    direction: e,
5    children: <Widget>[redBox,blueBox,yellowBox,greenBox],
6
7  );
8},color: Colors.black12,width: 300,height: 200);
9
10var redBox= Container(
11  color: Colors.red,
12  height: 50,
13  width: 50,
14);
15
16var blueBox= Container(
17  color: Colors.blue,
18  height: 30,
19  width: 60,
20);
21
22var yellowBox= Container(
23  color: Colors.yellow,
24  height: 50,
25  width: 100,
26);
27
28var greenBox= Container(
29  color: Colors.green,
30  height: 60,
31  width: 60,
32);


04

主轴方向:

mainAxisAlignment:MainAxisAlignment

主轴方向的排布规则,这里以水平为例,主轴为水平方向;竖直类比即可。

1enum MainAxisAlignment {
2  start,//顶头
3  end,//接尾
4  center,//居中
5  spaceBetween,//顶头接尾,其他均分
6  spaceAround,//中间的孩子均分,两头的孩子空一半
7  spaceEvenly,//均匀平分


1testMainAxisAlignment(){
2  var redBox= Container(
3    color: Colors.red,
4    height: 50,
5    width: 50,
6  );
7
8  var blueBox= Container(
9    color: Colors.blue,
10    height: 30,
11    width: 60,
12  );
13
14  var yellowBox= Container(
15    color: Colors.yellow,
16    height: 10,
17    width: 10,
18  );
19
20  var greenBox= Container(
21    color: Colors.green,
22    height: 50,
23    width: 10,
24  );
25
26  var mainAxisAlignment =[
27  MainAxisAlignment.start,MainAxisAlignment.center,
28  MainAxisAlignment.end,MainAxisAlignment.spaceAround,
29  MainAxisAlignment.spaceBetween,MainAxisAlignment.spaceEvenly];
30
31  var show = MultiShower(mainAxisAlignment,(e){
32    return Flex(
33      direction: Axis.horizontal,
34      mainAxisAlignment: e,
35      children: <Widget>[redBox,blueBox,yellowBox,greenBox],
36
37    );
38  },color: Colors.black12,width: 200,height: 150);
39  return show;
40}


05

交叉轴方向:

crossAxisAlignment:CrossAxisAlignment
1enum CrossAxisAlignment {
2  start,//顶头
3  end,//接尾
4  center,//居中
5  stretch,//伸展
6  baseline,//基线
7}

还是水平为例,交叉轴便是竖轴,这里可以看出他们的布局行为。

其中需要注意的是CrossAxisAlignment.baseline使用时必须有textBaseline,其中textBaseline确定对齐的是那种基线,分为alphabeticideographic



1testCrossAxisAlignment(){
2  var redBox= Container(
3    color: Colors.red,
4    height: 50,
5    width: 50,
6  );
7
8  var blueBox= Container(
9    color: Colors.blue,
10    height: 30,
11    width: 60,
12  );
13
14  var yellowBox= Container(
15    color: Colors.yellow,
16    height: 10,
17    width: 10,
18  );
19
20  var greenBox= Container(
21    color: Colors.green,
22    height: 50,
23    width: 10,
24  );
25
26  var crossAxisAlignment =[CrossAxisAlignment.start,CrossAxisAlignment.center,
27    CrossAxisAlignment.end,CrossAxisAlignment.stretch,CrossAxisAlignment.baseline];
28
29  var show = MultiShower(crossAxisAlignment,(e){
30    return Flex(
31      direction: Axis.horizontal,
32      crossAxisAlignment: e,
33      textBaseline: TextBaseline.alphabetic,//基线类型
34      children: <Widget>[redBox,blueBox,yellowBox,greenBox],
35
36    );
37  },color: Colors.black12,width: 200,height: 140);
38
39  return show;
40}


06

主轴尺寸:mainAxisSize

1enum MainAxisSize {
2  min,
3  max,
4}


当父容器的宽未约束,Flex默认会将自身尽可能延伸,这便是MainAxisSize.max。

此时改为MainAxisSize.min时,它不会延伸自己的区域,自会包裹内容。



1testMainAxisSize(){
2  var redBox= Container(
3    color: Colors.red,
4    height50,
5    width50,
6  );
7
8  var blueBox= Container(
9    color: Colors.blue,
10    height30,
11    width60,
12  );
13
14  var yellowBox= Container(
15    color: Colors.yellow,
16    height10,
17    width10,
18  );
19
20  var greenBox= Container(
21    color: Colors.green,
22    height50,
23    width10,
24  );
25
26  return Center(child: Flex(
27    direction: Axis.horizontal,
28    mainAxisSize: MainAxisSize.max,
29    children<Widget>[redBox,blueBox,yellowBox,greenBox],
30
31  ),);
32}
33


07

文字方向:

textDirection:TextDirection
1enum TextDirection {
2  ltr,//从左到右
3  rtl,//从右到左
4}


这个非常好理解,不多言了。



1testTextDirection(){
2  var redBox= Container(
3    color: Colors.red,
4    height: 50,
5    width: 50,
6  );
7
8  var blueBox= Container(
9    color: Colors.blue,
10    height: 30,
11    width: 60,
12  );
13
14  var yellowBox= Container(
15    color: Colors.yellow,
16    height: 10,
17    width: 10,
18  );
19
20  var greenBox= Container(
21    color: Colors.green,
22    height: 50,
23    width: 10,
24  );
25
26  var textDirection =[TextDirection.ltr,TextDirection.rtl];
27  var show = MultiShower(textDirection,(e){
28    return Flex(
29      direction: Axis.horizontal,
30      textDirection: e,
31      children: <Widget>[redBox,blueBox,yellowBox,greenBox],
32
33    );
34  },color: Colors.black12,width: 200,height: 140);
35  return show;
36}


08

竖直方向排序:

verticalDirection:VerticalDirection
1enum VerticalDirection{
2    up,
3    down,
4}



1testVerticalDirection(){
2  var redBox= Container(
3    color: Colors.red,
4    height: 50,
5    width: 50,
6  );
7
8  var blueBox= Container(
9    color: Colors.blue,
10    height: 30,
11    width: 60,
12  );
13
14  var yellowBox= Container(
15    color: Colors.yellow,
16    height: 10,
17    width: 10,
18  );
19
20  var greenBox= Container(
21    color: Colors.green,
22    height: 50,
23    width: 10,
24  );
25
26  var verticalDirection =[VerticalDirection.up,VerticalDirection.down];
27
28  var show = MultiShower(verticalDirection,(e){
29    return Flex(
30      direction: Axis.vertical,
31      verticalDirection: e
32      children: <Widget>[redBox,blueBox,yellowBox,greenBox],
33
34    );
35  },color: Colors.black12,width: 200,height: 140);
36
37  return show;
38}


09

基线对齐方式:

textBaseline:TextBaseline
1enum TextBaseline {
2  alphabetic,
3  ideographic,
4}



1testTextBaseline(){
2  var redBox= Text(
3    "张风捷特烈",style: TextStyle(fontSize: 20,backgroundColor: Colors.red),
4  );
5
6  var blueBox= Text(
7    "toly",style: TextStyle(fontSize: 50,backgroundColor: Colors.blue),
8  );
9
10  var yellowBox=  Text(
11    "1994",style: TextStyle(fontSize: 30,backgroundColor: Colors.green),
12  );
13
14  var textBaseline =[TextBaseline.alphabetic,TextBaseline.ideographic];
15
16  var show = MultiShower(textBaseline,(e){
17    return Flex(
18      direction: Axis.horizontal,
19      crossAxisAlignment: CrossAxisAlignment.baseline,
20      textBaseline: e,
21      children: <Widget>[redBox,blueBox,yellowBox],
22    );
23  },color: Colors.black12,width: 300,height: 140);
24
25  return show;
26}


10

Expanded与Flex的搭配

还有一点是关于Expanded,也比较保用,它能与Flex布局结合,变更孩子尺寸。  


1c1:绿色  c2:红色  c3:黄色
21).Expanded--c2:c1和c3将不变,c2延伸自己占满剩余部分
32).同时Expanded--c2和c3,最终c2和c3的长度是一样的
43).同时Expanded--c1,c2和c3,最终c1,c2,c3长度都是一样的



11

Row和Column在源码中的实现

Row和Column作为最常使用的两大组件,看完Flex之后,看到他们的源码你应该不禁一笑,原来这么简单,Row继承了Flex的属性,仅是direction固定为Axis.horizontal,说明Row是一个水平方向的Flex布局;Column也类似,是一个竖直方向的布局。


1class Row extends Flex {
2  Row({
3    Key key,
4    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
5    MainAxisSize mainAxisSize = MainAxisSize.max,
6    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
7    TextDirection textDirection,
8    VerticalDirection verticalDirection = VerticalDirection.down,
9    TextBaseline textBaseline,
10    List<Widget> children = const <Widget>[],
11  }) : super(
12    children: children,
13    key: key,
14    direction: Axis.horizontal,  <--------------重点在这
15    mainAxisAlignment: mainAxisAlignment,
16    mainAxisSize: mainAxisSize,
17    crossAxisAlignment: crossAxisAlignment,
18    textDirection: textDirection,
19    verticalDirection: verticalDirection,
20    textBaseline: textBaseline,
21  );
22}
23
24Column({
25    Key key,
26    MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
27    MainAxisSize mainAxisSize = MainAxisSize.max,
28    CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
29    TextDirection textDirection,
30    VerticalDirection verticalDirection = VerticalDirection.down,
31    TextBaseline textBaseline,
32    List<Widget> children = const <Widget>[],
33  }) : super(
34    children: children,
35    key: key,
36    direction: Axis.vertical,  <--------------重点在这
37    mainAxisAlignment: mainAxisAlignment,
38    mainAxisSize: mainAxisSize,
39    crossAxisAlignment: crossAxisAlignment,
40    textDirection: textDirection,
41    verticalDirection: verticalDirection,
42    textBaseline: textBaseline,
43  );
44}


至此,Flutter中的Flex布局就已经完全解读完了,所以Flex在手,天下我有。


12

用Flex布局写个小例子

1.布局分析

光说不练假把式,光练不说空把式,下面就用一个布局来实际看一下Flex的强大:

首先简单的分析一下:



1由上中下三行,可以用Column
2第一行由图标,文字和文字组成,其中两头分处左右,可以用Expanded处理  
3中间比较复杂由一个Row中包含两部分,左边是一个两行Column的内容,右边是文字  
4底部是一个Row,主轴对齐是start


可以画一个布局的结构图,从小到大,将总体拆分成肢体,再完成各肢体,最终拼合成需要的界面,这样逻辑会比较清晰,而不是上来就写布局,导致思维混乱。


2.布局代码

按照上面的逻辑,以小见大,一步步将界面的组成部分写出来,再进行组合,就像搭积木一样,这样你的布局层次会更加清晰。


1showItem() {
2  var infoStyle = TextStyle(color: Color(0xff999999), fontSize: 13);
3  var littleStyle = TextStyle(color: Colors.black, fontSize: 16);
4
5  var top = Row(//顶部,通过Flex布局的Row进行横向排列,Expanded中间
6    children: <Widget>[
7      Image.asset("images/icon_head.png", width: 20, height: 20),
8      Expanded(
9        child: Padding(
10          child: Text("张风捷特烈"),
11          padding: EdgeInsets.only(left: 4),
12        ),
13      ),
14      Text(
15        "Flutter/Dart",
16        style: infoStyle,
17      )
18    ],
19  );
20
21  var content = Column(//中间文字内容,交叉轴为start
22    mainAxisSize: MainAxisSize.min,
23    crossAxisAlignment: CrossAxisAlignment.start,
24    children: <Widget>[
25      Text(
26        "[Flutter必备]-Flex布局完全解读",
27        style: littleStyle,
28        maxLines: 2,
29        overflow: TextOverflow.ellipsis,
30      ),
31      Padding(
32        child: Text("也就是水平排放还是竖直排放,可以看出默认情况下都是主轴顶头,"
33            "交叉轴居中比如horizontal下主轴为水平轴,交叉轴则为竖直。也就是水平顶头,竖直居中"
34            "这里使用MultiShower快速展示,更好的对比出不同之处",
35            style: infoStyle, maxLines: 2, overflow: TextOverflow.ellipsis),
36        padding: EdgeInsets.only(top: 5),
37      ),
38    ],
39  );
40
41  var center = Row(//中间的部分
42    children: <Widget>[
43      Expanded(
44          child: Padding(
45        child: content,
46        padding: EdgeInsets.all(5),
47      )),
48      ClipRRect(
49        borderRadius: BorderRadius.all(Radius.circular(5)),
50        child: Image.asset("images/wy_300x200.jpg",
51          width: 80, height: 80, fit: BoxFit.cover),)
52    ],
53  );
54
55  var end = Row(//底部
56    children: <Widget>[
57      Icon(
58        Icons.grade,
59        color: Colors.green,
60        size: 20,
61      ),
62      Text(
63        "1000W",
64        style: infoStyle,
65      ),
66      Padding(child:Icon(Icons.tag_faces, color: Colors.lightBlueAccent, size: 20),
67          padding: EdgeInsets.symmetric(horizontal: 5),),
68      Text("2000W", style: infoStyle),
69    ],
70  );
71
72  var result = Card(//总体拼合
73      child: Container(
74          height: 160,
75          color: Colors.white,
76          padding: EdgeInsets.all(10),
77          child: Column(children: <Widget>[top, Expanded(child: center), end])));
78  return result;
79}


3.静态界面的封装使用

一个静态界面也只是一个玩偶,我们的目标是让静态组件成为一个有灵魂的组件,可以很容易复用。主要思路是抽离出写死字段抽离出来,自定义一个描述类作为入参,最终对点击事件进行回调,这样一个组件就封装好了,你只需要准备描述信息即可,动态变更界面。


1---->[使用]----
2ArticlePanel(
3    article: ArticleBean(userName: "张风捷特烈",
4        title: "[Flutter必备]-Flex布局完全解读",
5        info: "也就是水平排放还是竖直排放,可以看出默认情况下都是主轴顶头,"
6            "交叉轴居中比如horizontal下主轴为水平轴,交叉轴则为竖直。也就是水平顶头,竖直居中"
7            "这里使用MultiShower快速展示,更好的对比出不同之处",
8        type"Flutter/Dart",
9        starCount: "2000",
10        commentCount: "3000",
11    userIcon: Image.asset("images/icon_head.png"),
12      cover: Image.asset("images/wy_300x200.jpg",fit: BoxFit.cover)
13    ),
14  );


4.封装组件

1import 'package:flutter/material.dart';
2
3class ArticlePanel extends StatelessWidget {
4  ArticlePanel({Key key, this.article, this.onTap}) : super(key: key);
5
6  final ArticleBean article;
7  final TapCallback onTap;
8
9  @override
10  Widget build(BuildContext context) {
11    return _showItem(article);
12  }
13
14  _showItem(ArticleBean article) {
15    var infoStyle = TextStyle(color: Color(0xff999999), fontSize: 13);
16    var littleStyle = TextStyle(color: Colors.black, fontSize: 16);
17    var top = Row(
18      //顶部,通过Flex布局的Row进行横向排列
19      children: <Widget>[
20        Container(child: article.userIcon, width: 20, height: 20),
21        Expanded(
22          child: Padding(
23            child: Text(article.userName),
24            padding: EdgeInsets.only(left: 4),
25          ),
26        ),
27        Text(
28          article.type,
29          style: infoStyle,
30        )
31      ],
32    );
33
34    var content = Column(
35      //中间文字内容,交叉轴为start
36      mainAxisSize: MainAxisSize.min,
37      crossAxisAlignment: CrossAxisAlignment.start,
38      children: <Widget>[
39        Text(
40          article.title,
41          style: littleStyle,
42          maxLines: 2,
43          overflow: TextOverflow.ellipsis,
44        ),
45        Padding(
46          child: Text(article.info,
47              style: infoStyle, maxLines: 2, overflow: TextOverflow.ellipsis),
48          padding: EdgeInsets.only(top: 5),
49        ),
50      ],
51    );
52
53    var center = Row(
54      //中间的部分
55      children: <Widget>[
56        Expanded(
57            child: Padding(
58          child: content,
59          padding: EdgeInsets.all(5),
60        )),
61        ClipRRect(
62          borderRadius: BorderRadius.all(Radius.circular(5)),
63          child: Container(
64            width: 80,
65            height: 80,
66            child: article.cover,
67          ),
68        )
69      ],
70    );
71
72    var end = Row(
73      //底部
74      children: <Widget>[
75        Icon(
76          Icons.grade,
77          color: Colors.green,
78          size: 20,
79        ),
80        Text(
81          article.starCount,
82          style: infoStyle,
83        ),
84        Padding(
85          child: Icon(Icons.tag_faces, color: Colors.lightBlueAccent, size: 20),
86          padding: EdgeInsets.symmetric(horizontal: 5),
87        ),
88        Text(article.commentCount, style: infoStyle),
89      ],
90    );
91
92    var result = Card(
93        //总体拼合
94        child: InkWell(
95            onTap: () {
96              if (this.onTap != null) {
97                onTap(article);
98              }
99            },
100            child: Container(
101              height: 160,
102              padding: EdgeInsets.all(10),
103              child:
104                  Column(children: <Widget>[top, Expanded(child: center), end]),
105            )));
106
107    return result;
108  }
109}
110
111typedef TapCallback = void Function(ArticleBean bean);
112
113class ArticleBean {
114  Image userIcon; //头像
115  Image cover; //图片
116  String userName; //用户名
117  String title; //标题
118  String type; //类型
119  String info; //简介
120  String starCount; //赞
121  String commentCount;
122
123  ArticleBean(
124      {this.userIcon,
125      this.cover,
126      this.userName,
127      this.title,
128      this.type,
129      this.info,
130      this.starCount,
131      this.commentCount}); //评论数
132
133}






也许你还想看

十分钟带你入坑Flutter

深入理解Flutter多线程

Flutter移动端实战手册

新闻推荐系统的CTR预估模型

互联网架构演进之路





加入搜狐技术作者天团

千元稿费等你来!

戳这里!☛


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存