Flutter必备 | Flex布局完全解读

320 阅读21分钟
原文链接: mp.weixin.qq.com

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

本文字数: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),1415---->[flutter/lib/src/widgets/basic.dart:4015]----16class Row extends Flex {1718---->[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.cn/post/6844903888307355662)。

 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); 910var redBox= Container(11  color: Colors.red,12  height: 50,13  width: 50,14);1516var blueBox= Container(17  color: Colors.blue,18  height: 30,19  width: 60,20);2122var yellowBox= Container(23  color: Colors.yellow,24  height: 50,25  width: 100,26);2728var 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  );1314  var yellowBox= Container(15    color: Colors.yellow,16    height: 10,17    width: 10,18  );1920  var greenBox= Container(21    color: Colors.green,22    height: 50,23    width: 10,24  );2526  var mainAxisAlignment =[27  MainAxisAlignment.start,MainAxisAlignment.center,28  MainAxisAlignment.end,MainAxisAlignment.spaceAround,29  MainAxisAlignment.spaceBetween,MainAxisAlignment.spaceEvenly];3031  var show = MultiShower(mainAxisAlignment,(e){32    return Flex(33      direction: Axis.horizontal,34      mainAxisAlignment: e,35      children: <Widget>[redBox,blueBox,yellowBox,greenBox],3637    );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  );1314  var yellowBox= Container(15    color: Colors.yellow,16    height: 10,17    width: 10,18  );1920  var greenBox= Container(21    color: Colors.green,22    height: 50,23    width: 10,24  );2526  var crossAxisAlignment =[CrossAxisAlignment.start,CrossAxisAlignment.center,27    CrossAxisAlignment.end,CrossAxisAlignment.stretch,CrossAxisAlignment.baseline];2829  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],3536    );37  },color: Colors.black12,width: 200,height: 140);3839  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    height: 50, 5    width: 50, 6  ); 7 8  var blueBox= Container( 9    color: Colors.blue,10    height: 30,11    width: 60,12  );1314  var yellowBox= Container(15    color: Colors.yellow,16    height: 10,17    width: 10,18  );1920  var greenBox= Container(21    color: Colors.green,22    height: 50,23    width: 10,24  );2526  return Center(child: Flex(27    direction: Axis.horizontal,28    mainAxisSize: MainAxisSize.max,29    children: <Widget>[redBox,blueBox,yellowBox,greenBox],3031  ),);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  );1314  var yellowBox= Container(15    color: Colors.yellow,16    height: 10,17    width: 10,18  );1920  var greenBox= Container(21    color: Colors.green,22    height: 50,23    width: 10,24  );2526  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],3233    );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  );1314  var yellowBox= Container(15    color: Colors.yellow,16    height: 10,17    width: 10,18  );1920  var greenBox= Container(21    color: Colors.green,22    height: 50,23    width: 10,24  );2526  var verticalDirection =[VerticalDirection.up,VerticalDirection.down];2728  var show = MultiShower(verticalDirection,(e){29    return Flex(30      direction: Axis.vertical,31      verticalDirection: e32      children: <Widget>[redBox,blueBox,yellowBox,greenBox],3334    );35  },color: Colors.black12,width: 200,height: 140);3637  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  ); 910  var yellowBox=  Text(11    "1994",style: TextStyle(fontSize: 30,backgroundColor: Colors.green),12  );1314  var textBaseline =[TextBaseline.alphabetic,TextBaseline.ideographic];1516  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);2425  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}2324Column({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由上中下三行,可以用Column2第一行由图标,文字和文字组成,其中两头分处左右,可以用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  );2021  var content = Column(//中间文字内容,交叉轴为start22    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  );4041  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  );5455  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  );7172  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            )));106107    return result;108  }109}110111typedef TapCallback = void Function(ArticleBean bean);112113class 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;122123  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}); //评论数132133}

也许你还想看

十分钟带你入坑Flutter

深入理解Flutter多线程

Flutter移动端实战手册

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

互联网架构演进之路


加入搜狐技术作者天团

千元稿费等你来!

戳这里!☛