Flutter入门三:自动布局(Row/Column/Stack)与两种Widget

Flutter入门 学习大纲

  1. CenterAlignment
  2. 三种布局方式(RowColumnStack
  3. Expanded自动填充和AspectRatio宽高比
  4. StatefulWidget可变组件 与 StatelessWidget不可变组件

  • 本节代码,都默认使用下面main.dart文件:
    设置APP入口组件,使用MaterialApp默认样式,设置home根组件,使用Scaffold支持导航控制器,设置body内容组件。
import 'package:flutter/material.dart';
import 'layout_demo.dart';

// 入口,展示MyWidget组件
void main() => runApp(App());

// 根组件
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Home(),
      theme: ThemeData(
        primaryColor: Colors.yellow
      ),
    );
  }
}

// Home 组件
class Home extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[100],
      appBar: AppBar(
        title: Text("Flutter Demo"),
      ),
      body: LayoutDemo(), // 展示LayoutDemo组件
    );
  }
}
  • 下面的代码,都在LayoutDemo中实现。

1. Center和Alignment

  • Center: 居中
  • Alignment: 自定义水平垂直方向位置

1.1 Center

  • Center是常用的快捷布局方式,将子组件居中展示在父组件中:
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      // Center: 居中
      child: Center(
        child: Text( "Layout Demo" ),
      ),
    );
  }
}
  • 展示样式:


    image.png

1.2 Alignment

  • Alignment是自定义水平垂直方向位置。 默认(0,0)居中对齐,与center一样。
    【参数一】水平方向的百分比。 -1:对齐最左边0水平居中1:对齐最右边-0.50.5中间数值,是按比例展示位置
    【参数二】垂直方向的百分比。-1:对齐最上边0垂直居中1:对齐最下边-0.50.5中间数值,是按比例展示位置
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      //  Alignment: 按比例展示位置
      alignment: Alignment(-1,-1),
      child: Text( "Layout Demo" ),
    );
  }
}
  • 展示样式:


    image.png

2. 三种布局方式(Row、Column、Stack)

  • RowColumnStack实际上是空间坐标系x、y、z三个方向布局
    【Row】X轴(水平方向)布局
    【Column】Y轴(垂直方向)布局
    【Stack】Z轴(前后纵深方向)布局

2.1 Row

  • Row: X轴(水平方向)布局,默认水平撑满父容器空间。
  • Alignment(0, 0)标注child子容器Row居中对齐
    由于Row水平撑满父容器空间,所以会看到Row子元素没水平居中对齐的假象。 实际上我们Alignment是影响了Row容器水平对齐了,Row子元素的对齐方式,是Row内部处理的。

  • mainAxisAlignment:主轴方向对齐(RowColumn都有这个属性)
    Row中:
    start靠左(默认)
    end: 靠右
    center: 居中
    spaceAround: 剩下空间平均分配周围(每个部件周围等间距)
    spaceBetween:剩下空间平均分配在小部件中间(两边无间距,中间等间距)
    spaceEvenly: 完全等间距

  • crossAxisAlignment:交叉轴方向对齐(RowColumn都有这个属性)
    Row中:
    start居上
    center: 居中(默认)
    end: 居下
    baseline: 文字底部对齐(如果在Column中,必须配合textBaseline使用,后面具体分析)
    stretch: 垂直填充长条

  • Expanded填充式布局,完全等分主轴宽度,会自动换行row宽度无效,column高度无效(后面会具体分析)

import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      alignment: Alignment(0, 0), // 居中对齐
      // Row 水平(X轴水平方向)
      // Row 是在父控件`Container`的居中展示。内部元素默认是从左到右
      child: Row(
        // 主轴方向(start:靠左(默认),end: 靠右, center: 居中
        //          spaceAround:  剩下空间平均分配在周围(每个部件周围等间距)
        //          spaceBetween: 剩下空间平均分配在小部件中间(两边无间距,中间等间距),
        //          spaceEvenly:  完全等间距)
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        // 交叉轴(y轴方向) (start: 居上,center: 居中(默认), end: 居下,
        //                 baseline: 文字底部对齐,stretch: 垂直填充长条)
        crossAxisAlignment: CrossAxisAlignment.baseline,
        children: <Widget>[
          // 使用Expanded填充式布局,完全等分主轴宽度,会自动换行。row宽度无效,column高度无效
          Expanded(
            child: Container(
                child: Icon(
                  Icons.add,
                  size: 120,
                ),
                color: Colors.red),
          ),
          Expanded(
            child: Container(
                child: Icon(
                  Icons.ac_unit,
                  size: 60,
                ),
                color: Colors.blue),
          ),
          Expanded(
            child: Container(
                child: Icon(
                  Icons.access_alarm,
                  size: 30,
                ),
                color: Colors.white),
          ),
        ],
      ),
    );
  }
}
  • 展示样式:


    image.png

2.2 Column

  • Column:Y轴(垂直方向)布局,默认垂直撑满父容器空间。
  • Alignment对齐方式和mainAxisAlignment主轴对齐、crossAxisAlignment交叉轴对齐与Row类似,只是一个是水平一个是垂直方向。
  • 需要注意crossAxisAlignmenttextBaseline类型,必须配合textBaseline使用(后面具体分析)
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      alignment: Alignment(0, 0),
      // Column 垂直(y轴垂直)
      // Column 是在父控件`Container`的居中展示。内部元素是从上到下
      child: Column(
        // 主轴方向(start:靠上(默认),end: 靠下, center: 居中,
        //          spaceAround:  剩下空间平均分配在周围(每个部件周围等间距)
        //          spaceBetween: 剩下空间平均分配在小部件中间(两边无间距,中间等间距),
        //          spaceEvenly:  完全等间距)
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        // 交叉轴(y轴方向) (start: 居左,center: 居中(默认), end: 居右,
        //                 baseline: 文字底部对齐(针对文本,需要配合textBaseLine),
        //                 stretch: 水平填充长条)
        crossAxisAlignment: CrossAxisAlignment.start,
        children: <Widget>[
          Container(
              child: Icon(
                Icons.add,
                size: 120,
              ),
              color: Colors.red),
          Container(
              child: Icon(
                Icons.ac_unit,
                size: 60,
              ),
              color: Colors.blue),
          Container(
              child: Icon(
                Icons.access_alarm,
                size: 30,
              ),
              color: Colors.white),
        ],
      ),
    );
  }
}
  • 展示样式:


    image.png
2.2.1 演示baseline(文字底部对齐)
  • baseline: 基于文字底部对齐,我们用下面这个案例多个Text组件高度相同字体不同)来体会

crossAxisAlignment设置为baseline时,需要添加textBaseline属性(Row非必须,但Column必须),可以设置为:

  • alphabetic字母
  • ideographic中文 (好像没区别)
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      // 演示baseline(文字底部对齐)
      alignment: Alignment(0,0),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        // row不会报错,但是Column会报错,必须指定textBaseline
        crossAxisAlignment: CrossAxisAlignment.baseline,
        // 文本基线textBaseline: alphabetic:英文字符  ideographic: 中文字符
        textBaseline: TextBaseline.alphabetic,
        children: <Widget>[
          Container(
            child: Text("你好!", style: TextStyle(fontSize: 20)),
            color: Colors.red,
            height: 80
          ),
          Container(
              child: Text("我是", style: TextStyle(fontSize: 30)),
              color: Colors.green,
              height: 80
          ),
          Container(
              child: Text("哈哈哈", style: TextStyle(fontSize: 40)),
              color: Colors.blue,
              height: 80
          )],
      )
    );
  }
}
image.png

2.3 Stack

  • Stack:Z轴(前后纵深方向)布局(设置多个大小不同的组件,就可以看到效果:)
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.yellow,
      alignment: Alignment(0, 0),
      //  Stack 重叠 (Z轴纵深方向)
      // Stack 是在父控件`Container`的居中展示。内部元素是从里到外
      child: Stack(
        children: <Widget>[
          Container(
              child: Icon(
                Icons.add,
                size: 120,
              ),
              color: Colors.red),
          Container(
              child: Icon(
                Icons.ac_unit,
                size: 60,
              ),
              color: Colors.blue),
          Container(
              child: Icon(
                Icons.access_alarm,
                size: 30,
              ),
              color: Colors.white),
        ],
      ),
    );
  }
}
  • 展示样式:


    image.png
2.3.1 positioned
  • 在一个200 * 200 白色背景组件中,如果我们想自主控制每个组件的位置,可以通过positioned来设置:

  • 具体的参数设置,可参考positioned构造方法和内部描述。

第一个子控件左上角展示,第二个子控件右下角展示,第三个子控件靠右,离顶部60像素展示

import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Colors.yellow,
      alignment: Alignment(0, 0),
      child: Container(
        width: 200,
        height: 200,
        color: Colors.white,
        child: getStack(),
      )
    );
  }

  // 获取Stack组件
  Widget getStack() {
    return Stack(
      children: <Widget>[
        // 左上(默认)
        Positioned(
          child:
          Container(
              child: Icon(
                Icons.add,
                size: 120,
              ),
              color: Colors.red),
        ),
        // 右下
        Positioned(
          right: 0,
          bottom: 0,
          child:
          Container(
              child: Icon(
                Icons.ac_unit,
                size: 60,
              ),
              color: Colors.blue),
        ),
        // 右上(距离顶部60像素)
        Positioned(
          right: 0,
          top: 60,
          child:
          Container(
              child: Icon(
                Icons.access_alarm,
                size: 30,
              ),
              color: Colors.white),
        ),
      ],
    );
  }
}
  • 展示样式:


    image.png

3. Expanded自动填充和AspectRatio宽高比

3.1 Expanded自动填充

上面提到Expanded填充式布局,完全等分主轴宽度,会自动换行。(row宽度无效,column高度无效)

  • 现在,我们以多个文本组件为例,体验两种情况:
  1. 组件内所有子组件都是Expanded,布局情况如何?
  2. 组件内Expanded其他组件共存,布局情况如何?

1. 全Expanded

  • Row组件中,child设置三个Text组件字体内容不一样,但使用Expanded后,所有组件宽度相等自动换行:
// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Colors.yellow, alignment: Alignment(0, 0), child: getRow());
  }

  // 获取Row组件
  Widget getRow() {
    return Row(children: <Widget>[
      Expanded(
        child: Container(
            child: Text("你好!你好!你好!你好!你好!", style: TextStyle(fontSize: 20)),
            color: Colors.red),
      ),
      Expanded(
        child: Container(
            child: Text("我是我是我是我是我是", style: TextStyle(fontSize: 30)),
            color: Colors.green),
      ),
      Expanded(
        child: Container(
            child: Text("哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈", style: TextStyle(fontSize: 40)),
            color: Colors.blue),
      ),
    ]);
  }
}
  • 展示样式:


    image.png

2. Expanded与其他组件共存

  • 我们将第二个子组件改为非Expanded组件,可以看到``非Expanded组件布局完成之后,剩余空间给Expanded组件平分宽度
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Colors.yellow, alignment: Alignment(0, 0), child: getRow());
  }

  // 获取Row组件
  Widget getRow() {
    return Row(children: <Widget>[
      Expanded(
        child: Container(
            child: Text("你好!你好!你好!你好!你好!", style: TextStyle(fontSize: 20)),
            color: Colors.red),
      ),
      // 第二个子组件不是`Expanded`
      Container(
          child: Text("我是我是我是我是我是我是", style: TextStyle(fontSize: 30)),
          color: Colors.green),
      Expanded(
        child: Container(
            child: Text("哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈", style: TextStyle(fontSize: 40)),
            color: Colors.blue),
      ),
    ]);
  }
}
  • 展示样式:


    image.png

如果超过屏幕宽度,我们需要对宽高设置

3.2 AspectRatio 宽高比

  • AspectRatio宽高比部件,可以设置child子视图的 宽高比:
import 'package:flutter/material.dart';

// 布局Demo
class LayoutDemo extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Colors.yellow,
        alignment: Alignment(0, 0),
        child: Container(
          color: Colors.blue,
          width: 300,
          // AspectRatio 宽高比部件
          child: AspectRatio(
            aspectRatio: 2/1,   // 设置宽高比
            child: Icon(Icons.add),
          ),
        )
    );
  }
}
  • 展示样式:


    image.png
  • 我们也可以通过属性设定,来读取宽高值。下面分析可变组件不可变组件本质区别

4. StatefulWidget可变部件 与 StatelessWidget不可变部件

  • StatelessWidget: 不可变部件,所有变量都是final修饰,不可以变更状态
    (实际每次变更状态,就是销毁原部件,根据新初始值创建新不可变部件
  • StatefulWidget:可变部件,实际是状态值(记录UI状态)与不可变部件(描述UI)的组合。
    (每一次状态值变更,都触发不可变部件重新创建。而状态值会跟随StatefulWidget的销毁而销毁。)

所以实际上可变部件不可变部件的区别就是可变部件 多记录状态值,在UI上,本质都是通过创建销毁 不可变部件来进行展示。

4.1 重写计数器Demo

  • 我们通过重写Flutter默认的计数器Demo,来体验StatelessWidget不可变部件和StatefulWidget可变部件的区别:
4.1.1 main.dart创建
  • 指定APP入口App(),使用MaterialApp构建APP,将home根视图设置为StateManagerDemo ()
import 'package:flutter/material.dart';
import 'package:hello_flutter/state_manager_demo.dart';

// 入口,展示MyWidget组件
void main() => runApp(App());

// 根组件
class App extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 返回`MaterialApp`
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: StateManagerDemo(), // 指定根视图为`StateManagerDemo`
      theme: ThemeData(
        primaryColor: Colors.blue
      ),
    );
  }
}
4.1.2 StateManagerDemo创建(StatelessWidget不可变部件)
  • 新建state_manager_demo.dart文件,StateManagerDemoStatelessWidget不可变部件,使用Scaffold导航控制器部件设置appBar导航栏、body内容和floatingActionButton浮动按钮。
    floatingActionButton新增onPressed点击事件,触发回调函数(每次点击,count+1打印count值)
import 'package:flutter/material.dart';

class StateManagerDemo extends StatelessWidget {

  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("StateManagerDemo"),
      ),
      body: Center(
        child: Chip(label: Text("$count"),), // 全圆角的组件
      ),
      // 浮动按钮(默认右下角)
      floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),
        // 点击触发: 给一个无参回调函数,可以直接给`(){}`,也可以外部创建一个函数,传入函数名
        // 每次点击,count+1,打印count值
        onPressed: (){
          count += 1;
          print("count = $count");
        },
      )
    );
  }
}
  • 当我们在StatelessWidget不可变部件中创建变量未使用final修饰,而使用var修饰时,会有提示:
    This class (or a class that this class inherits from) is marked as '@immutable', but one or more of its instance fields aren't final: StateManagerDemo.count
    image.png

    我们忽略不管(暂时不管,后面做比较),继续运行,可以看到界面运行正常:
    image.png
  • 点击浮动按钮 ➕号,打印台可以看到count递增了,但是页面中心的UI未同步更新
image.png

我们发现,不用final修饰变量count,也可以正常运行,并且count完成了计数任务,但是界面UI未更新

  • 如果我们使用final声明变量,就等同于swift中的let声明,是不可变的,不能进行count+=1的操作。

实际上,这是Flutter底层渲染原理决定的,StatelessWidget不可变部件,本身不支持记录状态,每次都是销毁重新渲染UI,所以编译器提示我们不可变部件内全部使用final修饰。这样从编码层减少错误产生

  • 如果一定需要记录状态,请使用StatefulWidget可变部件。
4.1.3 StateManagerDemo创建(StatefulWidget可变部件)
  • 上面说到,如果要记录状态,请使用StatefulWidget可变部件。
    StatefulWidget本身并不是直接改变UI,而是在StatelessWidget不可变部件的基础上,多了一个管理状态的State组件。这个组件记录状态,并决定了返回的UI样式
  • 所以,记录状态的变量(count)和返回组件构造方法(build),都应该放在State中。 而StatefulWidget就是直接返回State根据当前数据build返回的UI部件
  • 需要补充说明的是: 我们需要部件及时更新,需要在数据变动处,调用setState(){}更新状态。
import 'package:flutter/material.dart';

class StateManagerDemo extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    // 直接返回State根据最新数据build的组件
    return _StateManagerState();
  }
}

class _StateManagerState extends State<StateManagerDemo> {
  // 记录计数
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("StateManagerDemo"),
        ),
        body: Center(
          child: Chip(label: Text("$count"),), // 全圆角的组件
        ),
        // 浮动按钮(默认右下角)
        floatingActionButton: FloatingActionButton(
          child: Icon(Icons.add),
          // 点击触发: 给一个无参回调函数,可以直接给`(){}`,也可以外部创建一个函数,传入函数名
          // 每次点击,count+1,打印count值
          onPressed: (){
            count += 1; // 数据变动
            setState(() {}); // 更新状态(自动触发UI的重新渲染)
            print("count = $count");
          },
        )
    );
  }
}
  • 页面样式:


    image.png

总结:

  • StatelessWidget不可变部件:

    直接build返回UI部件即可,变量都使用final修饰不可更改。每次通过构造方法传入新数据,都会进行旧部件销毁新部件创建

  • StatefulWidget 可变部件:

  1. State组件管理状态,并build返回最新状态UI部件。每次状态变更,需要setState(() {})更新UI部件
    (渲染机制会自动销毁旧UI部件创建新UI部件State的生命周期与StatefulWidget一致)

  2. StatefulWidget可变部件只需要返回State对象即可
    (实际是返回Statebuild最新UI部件

【思考】

实际上,在Flutter开发中我们需要注意一些优化点

  • 我们整个页面,其实只有+号按钮点击触发中心的计数UI更新。并不需要每次数据变化,都更新整个外部部件
  • 我们可以将Chip更新为一个StatefulWidget可变部件,点击+号时,通过回调Chip部件重新渲染即可。

本节,我们主要分析自动布局几种方式,以及StatefulWidgetStatefulWidget的区别。
下一节,Flutter入门四:搭建项目、资源调用、简单开发。从项目入手,一步步理解熟悉Flutter

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容