美文网首页Flutter入门Flutter
Flutter入门三:自动布局(Row/Column/Stack

Flutter入门三:自动布局(Row/Column/Stack

作者: markhetao | 来源:发表于2021-02-04 16:52 被阅读0次

    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

    相关文章

      网友评论

        本文标题:Flutter入门三:自动布局(Row/Column/Stack

        本文链接:https://www.haomeiwen.com/subject/ifoptltx.html