flutter-状态、布局、组件

500 阅读15分钟

前言

本章直接介绍flutter中常用的布局,其中主要介绍常用的Flex布局,以及常用的一些基础布局组件,最后讲一下组件状态更新(常听到的状态机)

flexbox布局参考地址(可以参考部分理解)

demo地址(内容在statusAndLayout文件夹中,可以替换main中注释尝试效果)

icon图标地址:放在前面方便参考

其中这里面常见的组件以及属性,主要介绍下面这些:

FlexColumnRowContainerSizedBoxAlignmentFlexibleExpandedStackPositionWrapListView(滚动视图UIScrollView)ListView.Builder(表格视图UITableView)TextButtonMaterialButtonCupertinoButtonImageTextTextFieldIconIconButton

其中Text尝鲜篇章就可以看到,这里不多解释

下面通过 Flex组件来介绍 Flex布局

Flex布局(盒式布局、弹性布局)

下面先给出一个 Flex简单的属性展示案例,后面一点介绍

Flex(
    direction: Axis.vertical, //水平horizontal
    //相当于Column,可以不用Comlum和Row,使用Flex打天下,当然有简单的为何不用是吧
    
    mainAxisAlignment: MainAxisAlignment.spaceEvenly, //主轴
    crossAxisAlignment: CrossAxisAlignment.center, //次轴
    children: [
        Container(
            width: 300,
            height: 60,
            alignment: Alignment.center, //设置每部元素排列方式,居中,相当于普通View
            child: const Text('我是一个文本标签')
        )
    ]
)

direction主轴布局方向

direction: 设置 Flex布局的主轴和次轴, Axis.vertical表示主轴沿垂直方向布局,相当于 Column, Axis.horizontal表示主轴沿水平方向布局,相当于 Row

tips: width标识横向长度,height标识纵向方向长度,不受方向布局约束

下面主要以 垂直方向 为案例,演示各种效果,水平方向原理一致

设置vertical或者使用 Column

16d43ec293482d72_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0.webp

设置horizontal或者使用 Row

16d43eb297b9c8fe_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0.webp

mainAxisAlignment主轴属性

主轴 mainAxisAlignment,参数类型 MainAxisAlignment 枚举,分别为 start、end、center、spaceBetween、spaceAround、spaceEvenly

start

start:默认初始位置,就像上面的 ColumnRow一样都是默认

Column: 从顶部开始,一个挨着一个

Row: 从左侧开始,一个挨着一个

16d43ec293482d72_tplv-t2oaga2asx-zoom-in-crop-mark_1304_0_0_0.webp

Row: 从左侧开始,一个挨着一个

center

沿主轴方向元素居中,以 Column为例

end

沿主轴方向元素贴尾,以 Column为例

spaceBetween

沿主轴方向,两边元素贴边,中间间距平分剩余空间

spaceAround

沿主轴方向,两边元素不贴边、两边元素距离边为r,元素之间内部间距2r

spaceEvenly

沿主轴方向,两边元素不贴边, 子元素与边缘之间间隙一样

crossAxisAlignment次轴属性

次轴 crossAxisAlignment:参数类型 CrossAxisAlignment 枚举,分别为 start、end、center、stretch、baseline

start

沿次轴方向,元素居中,以 Column为例,主轴方向都是 start,查看下效果

center

沿次轴方向元素居中,以 Column为例,主轴方向都是 start,查看下效果

end

沿次轴方向,元素贴尾,以 Column为例,主轴方向都是 start,查看下效果

stretch

沿次轴方向,将子元素拉伸至末尾(设置固定长度的子元素不受限制),主轴方向都是 start,查看下效果

baseline

沿子元素文字基线方向对齐,平时用的比较少,可以被上面的参数搭配嵌套代替,不上图了

常用布局介绍

FlexColumnRowContainerSizedBoxAlignmentFlexibleExpandedStackPositionWrapListView(滚动视图UIScrollView)ListView.Builder(表格视图UITableView)textButtonMaterialButtonCupertinoButtonImageTextTextFieldIconIconButton

ps: 常用布局中,Flex布局会默认填充父布局次轴剩余空间,其他组件一般默认不会,需要设置大小,例如:Container和其他小组件默认受内容(一般为子组件)影响,为空时就跟没有一样,可以通过设置宽高,或者通过Expanded(其也是Flex控件一种)扩张

另外 Cupertino 系列的组件一般都是偏向ios风格的组件

Flex、Column、Row

从前面的 Flex布局也可以了解到,Column、Row也是不过是Flex设置了direction的结果,只不过还默认居中了而已

使用如下所示,就不多介绍了,可以通过布局方式、布局属性来调整子元素的位置

Flex(
    direction: Axis.vertical, //水平horizontal
    //相当于Column,可以不用Comlum和Row,使用Flex打天下,当然有简单的为何不用是吧
    
    mainAxisAlignment: MainAxisAlignment.spaceEvenly, //主轴
    crossAxisAlignment: CrossAxisAlignment.center, //次轴
    children: [
        Container(
            width: 300,
            height: 60,
            alignment: Alignment.center, //设置每部元素排列方式,居中,相当于普通View
            child: const Text('我是一个文本标签')
        )
    ]
)
Column(
  mainAxisAlignment: MainAxisAlignment.start, //主轴
  crossAxisAlignment: CrossAxisAlignment.center,//次轴
  children: <Widget>[]
)
//作为案例使用
Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    crossAxisAlignment: CrossAxisAlignment.center,
    children: const <Widget>[
      Text('左'),
      Text('中'),
      Text("右")
    ]
),

如下所示,为Row设置的效果

image.png

Container、SizedBox、alignment

Container:其也是一个很重要的元素,其就想 UIView一样,只有一些较为基础的功能,例如:设置背景、宽高、间距等,但却经常使用

SizedBox:可以理解为Container的简化版,一般作为容器使用,只能设置子视图或容器大小,一般设置间距时都是用它来调整,也很常用

他们往往配合alignment属性使用,可以调整子元素再其上面的位置,一般通过其设置

alignment:

通过 Alignment类来设置属性,Alignment(0, 0)构造方法,前后参数分别表示左右上下对齐属性(0表示居中,-1、1表示左右、上下),可以通过此属性

另外 Alignment,还可以通过固定属性,设置排列位置,这些可以理解为调用Alignment()构造方法,参数如下所示,可以通过设置属性直接调整子组件的位置

/// The top left corner.
static const Alignment topLeft = Alignment(-1.0, -1.0);

/// The center point along the top edge.
static const Alignment topCenter = Alignment(0.0, -1.0);

/// The top right corner.
static const Alignment topRight = Alignment(1.0, -1.0);

/// The center point along the left edge.
static const Alignment centerLeft = Alignment(-1.0, 0.0);

/// The center point, both horizontally and vertically.
static const Alignment center = Alignment(0.0, 0.0);

/// The center point along the right edge.
static const Alignment centerRight = Alignment(1.0, 0.0);

/// The bottom left corner.
static const Alignment bottomLeft = Alignment(-1.0, 1.0);

/// The center point along the bottom edge.
static const Alignment bottomCenter = Alignment(0.0, 1.0);

/// The bottom right corner.
static const Alignment bottomRight = Alignment(1.0, 1.0);

ContainerAlignment的使用如下所示,一般Text调整间距都会带上Container

Container(
    width: 300,
    height: 60,
    margin: const EdgeInsets.all(10),
    // alignment: const Alignment(0, 0), //分别表示左右、上下对齐属性  0表示居中,-1表示居左/上, 1居右/下
    alignment: Alignment.center, //设置每部元素排列方式,居中,相当于普通View
    child: const Text('我是Layout,,配合container可以解决自己背景大小问题')
)

Stack、Position

Stack 栈式布局,其实就是堆叠式布局、帧布局,一个上面放另一个元素,和 ios中的默认布局、 androidFrameLayout类似,通过设置个元素的间距,以实现不同位置效果

Position 一般配合 Position使用,在Stack中,无论前面放置多少元素,其设置了位置之后,其参数都是相对于 Stack 的,可以理解为以Stack为背景的绝对布局

Container(
  width: 300,
  height: 200,
  color: Colors.blue,
  //设置一个Stack相对布局容器,被Container包围,方便设置颜色对比
  child: Stack(
    alignment: Alignment.center,
    children: <Widget>[
      //设置一个Container,采用默认的方式居中
      Container(
        width: 200,
        height: 100,
        color: Colors.green,
        alignment: Alignment.center,
        //输入框
        child: const TextField(
            keyboardType: TextInputType.number,
            decoration: InputDecoration(
                hintText: "请输入手机号"//占位符
            )
        ),
      ),
      //使用Position,设置相对父布局的位置,这里居右上角,可以查看效果非常好
      //仅仅用Flex布局,类似这种稍微复杂点,就不是很好解决
      const Positioned(
          top: 0,
          right: 0,
          width: 200,
          child: Text("我是在Stack中的帧布局、绝对布局,配合Stack使用能随意调整距离边界位置,从而发挥出效果")
      )
    ],
  ),
);

image.png

TextField输入框

用于输入内容的,可以点进去查看具体设置属性,默认android的样式,默认样式有点丑(个人感觉谷歌审美比苹果要差不少),使用过程中一般需要自己封装一下,不多介绍

//为TextField代理,可以用来清除数据,设置更新text等
final TextEditingController _controller = TextEditingController();

//焦点node,初始化node之后可以使用点击事件主动唤起聚焦当前输入框
//需要配合FocusScope.of(context).requestFocus使用
final FocusNode _focus = FocusNode();

TextField(
    autofocus: true, 开始即可获取焦点
     //controller可以声明到上面,
    controller: _controller,
    style: TextStyle(), //可以设置输入文本属性
    focusNode: _focus, //可以用来激活取消输入框
    keyboardType: TextInputType.number,
    onChanged: (String text) {} //输入后的回调
    cursorColor: Colors.green,//设置光标颜色
    maxLength: 100, //最大输入数量
    //其他的设置一般在装饰文件中,可以根据需要点进去看
    //里面可以设置浮动图标、头尾文案、等信息
    //这里只列举出来几条
    decoration: InputDecoration(
        hintText: "请输入手机号",//占位符
        border: InputBorder.none,//去掉底部很丑的线条
        contentPadding: EdgeInsets.only(left: 5),//设置内边距
    ),
    //也可以使用这一个,设置圆角
    //OutlineInputBorder(
    //  borderRadius: BorderRadius.all(
    //    Radius.circular(10),
    //),
    ),
),

_controller.clear(); //清空输入框,不会走onChanged
_controller.text = ''; //设置输入框内容,可以通过该参数同步内容

//使用原有focusNode可以获取焦点,一般再点击中使用,让其恢复焦点,也可以用来切换输入框
FocusScope.of(context).requestFocus(_focus);
//传入新的 FocusNode 可以取消context当前输入框的焦点
FocusScope.of(context).requestFocus(FocusNode());

注意:默认聚焦不可直接用在 initState 中,此时组件还没初始化完毕,默认聚焦可声明 autofoucus: true

输入框默认效果可以参考上一张图

Wrap

Wrap 能做层包围效果,会自动换行(列),比较常用,需要了解具体属性,可以点进去,其一般配合Flex布局使用,这里他自己就是一个特殊的Flex布局,可以用来做一个简单的图片墙功能

如下所示是一个水平方向布局的 Wrap,占满自动换行,效果如下所示(会感觉,这做多个图片的展示简单多了呀😂)

Container(
  width: 300,
  height: 300,
  color: Colors.green,
  margin: const EdgeInsets.only(top: 10),
  child: Wrap(
      direction: Axis.horizontal, //横向布局布局,布满换行
      alignment: WrapAlignment.start, //主轴
      crossAxisAlignment: WrapCrossAlignment.start, //次轴
      spacing: 10, //主轴上的内容间距
      runSpacing: 20, //次轴的内容间距
      children: <Widget>[
        Container(
            width: 100,
            height: 100,
            color: Colors.red,
            child: const Text('我是中间的文字')
        ),
        Container(
            width: 100,
            height: 100,
            color: Colors.red,
            child: const Text('我是中间的文字')
        ),
        Container(
            width: 100,
            height: 100,
            color: Colors.red,
            child: const Text('我是中间的文字')
        ),
        Container(
            width: 100,
            height: 100,
            color: Colors.red,
            child: const Text('我是中间的文字')
        )
      ]
  ),
),

如下所示,横向布局,布满换行,自己设置好艰巨和大小,效果就会很nice

image.png

下面是纵向布局,布满横向换行的案例,非常类似,就不多说了

//主轴为纵向布局,布满了之后,换列
Container(
  width: 300,
  height: 300,
  margin: const EdgeInsets.only(top: 10),
  color: Colors.white,
  child: Wrap(
      direction: Axis.vertical, //主轴方向垂直布局,布满换列
      alignment: WrapAlignment.start, //主轴
      crossAxisAlignment: WrapCrossAlignment.start, //次轴
      spacing: 10, //主轴上的内容间距
      runSpacing: 20, //次轴的内容间距
      children: <Widget>[
        Container(
            width: 80,
            height: 80,
            color: Colors.red,
            child: const Text('我是中间的文字')
        ),
        Container(
            width: 80,
            height: 80,
            color: Colors.red,
            child: const Text('我是中间的文字')
        ),
        Container(
            width: 80,
            height: 80,
            color: Colors.red,
            child: const Text('我是中间的文字')
        ),
        Container(
            width: 80,
            height: 80,
            color: Colors.red,
            child: const Text('我是中间的文字')
        )
      ]
  ),
)

效果如下所示

image.png

ListView

我看很多人在各种使用ScrollView、CustomScrollView感觉很鸡肋,这里推荐使用ListView,他就是 ios版ScrollView,使用简单,只要嵌套就可以滚动,效果很不错

使用过程,只需要在 children中添加子布局就行了,默认垂直滚动,可以设置布局方向

return ListView(
  // scrollDirection: Axis.vertical, //默认垂直滚动
  children: <Widget>[]
);

ListView.Builder

ListView.Builder 其为表格视图,就想ios中的 TableView一样,把他单个拿出来是有原因的,其也是基于ListView的封装,从起调用手段就可以看出来(在尝鲜篇就有其案例,文件在demo文件夹中)

如下所示就是表格视图的使用

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

  //可以将row的内容放到这里main
  Widget cellForRow(BuildContext context, int index) {
    return Container();
  }

  @override
  Widget build(BuildContext context) {
    //想控制ListView的内间距或者外间距,可以尝试嵌套 Container
    return ListView.builder(
        itemCount: carsDatas.length,
        // itemBuilder: cellForRow, //可以指向外部方法
        itemBuilder: (BuildContext context, int index) {
          return Column(
            children: <Widget>[
              //网络加载图片,没有本地缓存
              Image.network(carsDatas[index].imageUrl,
                fit: BoxFit.fitWidth,
              ),
              Container(height: 8),
              Text(carsDatas[index].name),
              Container(height: 8)
            ],
          );
        }
    );
  }
}

image.png

Text

Text以及富文本 RickText的使用很简单,尝鲜篇已经有介绍,这里不多介绍,可以设置文字布局,也可以设置文字风格

平时都是配合 Container 使用,如果只想简单居中,可以直接使用 CenterCenter默认填充父视图布局且内容居中,因此Text就相当于在父视图中间了,不过可以完全被Container代替,平时使用不多

Center(
    child: Text("hello world!",
      textAlign: TextAlign.center,
      style: TextStyle( //可以在前面声明直接使用变量
          color: Colors.blue,
          fontSize: 36,
          fontWeight: FontWeight.bold
      ),
    )
)

Flexible、Expanded

Flexible就是flexbox布局的flex参数

fit: FlexFit则当前组件可以分配剩余空间,相当于开启 flex参数

flex:则根据当前flex与当前层级的Flexible的flex总占用比分配,flex值越大分配空间比例越多,与flex布局中的参数一致,只有一个则占满剩余空间

Flexible(
  fit: FlexFit.tight,
  //flex: 1, //默认flex为1,可以设置不同比例
  child: Container(),
)

Expanded:就是Flexible布局的一种常用形态,默认使用child属性,即可根据flex值分配剩余空间(只有一个则占据全部剩余空间),当然用Flexible也可以,使用如下所示(推荐

//相当于上面的 Flexible 的使用,如果不设置fit方式,那么推荐使用该方案
Expanded(
  child: Container(),
  //flex: 1  //默认flex为1,可以设置不同比例
)

tips:后面更新布局部分有案例、TextButton也一样

Image、TextButton、ElevatedButton、 MaterialButton、CupertinoButton

Image: 后面篇章会单独讲(image加载网络和本地图片、三方的使用),这里不介绍

TextButton: 后面布局有使用案例,之前flutter有很多种button现在系统都不推荐了,目前推荐使用该控件(可能也是为了避免更多控件,减少使用难度)

TextButton(
  onPressed: () {},
  style: ButtonStyle(
    //水波纹效果,不设置就没有
    overlayColor: MaterialStateProperty.all(Colors.black),
    //背景颜色,不设置水波纹就点击后有效果
    backgroundColor: MaterialStateProperty.resolveWith((states) {
      //设置按下时的背景颜色
      if (states.contains(MaterialState.pressed)) {
        return Colors.blue[200];
      }
      //默认不使用背景颜色
      return null;
    }),
  ),
  child: const Text('点击有惊喜'),
),

TextButton默认没有效果,一般都是伴随着Text而存在,有一个默认的最小宽度,和 ElevatedButton差不多,因此 ElevatedButton 就不多介绍了,只是多了一个流水点击效果

ElevatedButton 带海拔高度的按钮,使用上和其他的差不多

MaterialButton 其解决了 TextButtonElevatedButton的最小宽度问题,也可以主动设置高度,且拥有点击流水效果,使用比较简单

CupertinoButton 偏向ios风格的按钮,默认带小圆角,可以去掉,最小高度44可以设置,最佳点击高度

MaterialButton(
  minWidth: 80,
  height: 40,
  onPressed: () {
    Navigator.of(context).push(MaterialPageRoute(builder: (context) => TestWidget()));
  },
  child: const Text("测试按钮",),
),

Icon、IconButton

这两个也是比较常用的组件,IconButtonicon专用的button,类似于 TextButtonIcon是一个图标组件,话不多说直接上代码

icon图标地址:通过这个地址可以查看名称和效果图

IconButton(
  onPressed: () {
    //会从context的父类开始找组件context.findAncestorStateOfType
    //当前组件的context父组件是 MyApp 是没有 Scaffold,且没有drawer,因此无法打开
    //Builder是一个StatelessWidget基础组件,只不过返回了自己的context,因此没问题
    Scaffold.of(context).openDrawer();
    //Scaffold.of(context).closeDrawer(); //关闭侧边栏
    // Scaffold.of(context).openEndDrawer();//打开右侧侧边栏
  },
  icon: const Icon(Icons.table_rows_rounded),
  iconSize: 20,
);

更新布局state

我们一般布局使用 StatelessWidget,会发现页面不能使用动态变量更新,即使使用变量接收参数,也需要使用 final修饰,因此如果需要更新模块的StatelessWidget的内容,需要重新创建该控件,以达到更新效果

可是如果我们内部需要更新参数,然后更新自己怎么办,那就需要用到 StatefulWidget 控件了,通过此控件,其内部属性得到解放,不需要使用 final修饰也不会报错了,我们可以通过修改状态来更新固定内容了

创建默认案例

如下所示创建一个默认 StatelessWidget

class State extends StatelessWidget {
    //传参通过构造函数,这一句就是构造方法,只不过调用了系统默认的构造方法
    //可以在在第一个参数前面声明参数
    const State({Key? key}) : super(key: key);
    
    //这样就可以了
    //const State(this.age, {Key? key}) : super(key: key)
    //final int age;

    @override
    Widget build(BuildContext context) {
        return Container();
    }
}

如下所示创建一个默认 StatefulWidget

class State extends StatefulWidget {
  const State({Key? key}) : super(key: key);

  @override
  State<State> createState() => _StateState();
}

class _StateState extends State<State> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

通过创建默认案例可以发现他们的不同了,接下来看看怎么使用 StatefulWidget

StatefulWidget

下面是一个 StatefulWidget 的自增、自减器案例,配合 Flexible 使用(顺道查看一下 Flexible 效果)

设置一个默认属性,通过 setState 方法来更新内容,如果想更新全部的,更新参数后,直接setState不传递参数即可

//一个自增、自减器
class Counter extends StatefulWidget {
  const Counter({Key? key}) : super(key: key);

  @override
  State<Counter> createState() => _CounterState();
}

class _CounterState extends State<Counter> {
  int _count = 0;

  void onPress() {
    setState(() {
      _count++;
    });
    //相当于
    //setState(() {
    //  _count = count + 1;
    //});
    //如果想更新全部的,更新参数后,直接setState不传递参数即可
    _count++;
    setState((){});
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        //Flexible就是flexbox布局的flex,
        //设置fit: FlexFit则当前组件可以分配剩余空间
        //flex则根据当前flex与当前层级层级的Flexible的flex总占用比分配
        Flexible(
          fit: FlexFit.tight,
          flex: 1,
          child: Container(
            //不设置宽高默认填满
              color: Colors.black,
              // alignment: const Alignment(0, 0), //分别表示左右、上下对齐属性  0表示居中,-1表示居左/上, 1居右/下
              alignment: Alignment.center, //一般使用这个属性,上面的在百分比位置的时候使用
              //放一个按钮
              child: TextButton(
                  onPressed: onPress,
                  child: Text('我是会自增的工具:$_count')
              )
          ),
        ),
        Flexible(
          fit: FlexFit.tight,
          flex: 1,
          child: Container(
            //不设置宽高默认填满
              color: Colors.cyan,
              // alignment: const Alignment(0, 0), //分别表示左右、上下对齐属性  0表示居中,-1表示居左/上, 1居右/下
              alignment: Alignment.center, //一般使用这个属性,上面的在百分比位置的时候使用
              //放一个按钮
              child: TextButton(
                  onPressed: () {
                    setState(() {
                      _count--;
                    });
                  },
                  child: Text('我是会自减的工具:$_count')
              )
          ),
        )
      ],
    );
  }
}

如下所示为显示效果

image.png

最后

自己下载下来案例测试一下吧,flutter其实上手写 UI 逻辑业务逻辑很简单,只要不涉及到硬件以及权限的,基本上不需要用到原生

快来自己动手写一个demo吧