- 上一节,我们熟悉了Widget、文字样式、ListView,本节,我们主要讲
开发过程
中最常见的自动布局
:
-
Center
和Alignment
- 三种布局方式(
Row
、Column
、Stack
) -
Expanded
自动填充和AspectRatio
宽高比 -
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" ),
),
);
}
}
-
展示样式:
1.2 Alignment
-
Alignment
是自定义水平
和垂直
方向位置。 默认(0,0)
是居中对齐
,与center
一样。
【参数一】水平
方向的百分比。-1
:对齐最左边
,0
:水平居中
,1
:对齐最右边
。-0.5
和0.5
等中间数值
,是按比例
展示位置
。
【参数二】垂直
方向的百分比。-1
:对齐最上边
,0
:垂直居中
,1
:对齐最下边
。-0.5
和0.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" ),
);
}
}
-
展示样式:
2. 三种布局方式(Row、Column、Stack)
-
Row
、Column
、Stack
实际上是空间坐标系
的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
:主轴方向对齐(Row
和Column
都有这个属性)
在Row
中:
start
:靠左
(默认)
end
:靠右
center
:居中
spaceAround
: 剩下空间平均分配
在周围
(每个部件周围等间距)
spaceBetween
:剩下空间平均分配
在小部件中间
(两边无间距,中间等间距)
spaceEvenly
: 完全等间距
)
crossAxisAlignment
:交叉轴方向对齐(Row
和Column
都有这个属性)
在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),
),
],
),
);
}
}
-
展示样式:
2.2 Column
-
Column
:Y轴(垂直方向
)布局,默认垂直撑满
父容器空间。
Alignment
对齐方式和mainAxisAlignment
主轴对齐、crossAxisAlignment
交叉轴对齐与Row
类似,只是一个是水平
一个是垂直
方向。- 需要注意
crossAxisAlignment
的textBaseline
类型,必须配合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),
],
),
);
}
}
-
展示样式:
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
)],
)
);
}
}
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),
],
),
);
}
}
-
展示样式:
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),
),
],
);
}
}
-
展示样式:
3. Expanded
自动填充和AspectRatio
宽高比
3.1 Expanded自动填充
上面提到Expanded
是填充式布局
,完全等分主轴宽度
,会自动换行
。(row
宽度无效,column
高度无效)
- 现在,我们以
多个文本
组件为例,体验两种情况:
- 组件内
所有子组件
都是Expanded
,布局情况如何? - 组件内
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),
),
]);
}
}
-
展示样式:
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),
),
]);
}
}
-
展示样式:
如果
超过屏幕宽度
,我们需要对宽高
做设置
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),
),
)
);
}
}
-
展示样式:
我们也可以通过
属性设定
,来读取宽高值
。下面分析可变组件
和不可变组件
的本质区别
。
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
文件,StateManagerDemo
为StatelessWidget
不可变部件,使用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
我们忽略不管
(暂时不管,后面做比较),继续运行
,可以看到界面运行正常
:
- 点击
浮动按钮 ➕号
,打印台可以看到count
数递增
了,但是页面中心
的UI未同步更新
。
我们发现,不用
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");
},
)
);
}
}
-
页面样式:
总结:
StatelessWidget
不可变部件:直接
build
返回UI部件
即可,变量都使用final修饰
,不可更改
。每次通过构造方法
传入新数据
,都会进行旧部件
的销毁
和新部件
的创建
。
StatefulWidget
可变部件:
State
组件管理状态
,并build
返回最新状态
的UI部件
。每次状态变更,需要setState(() {})
更新UI部件
(渲染机制会自动销毁旧UI部件
,创建新UI部件
,State
的生命周期与StatefulWidget
一致)
StatefulWidget
可变部件只需要返回State
对象即可
(实际是返回State
中build
的最新UI部件
)
【思考】
实际上,在
Flutter
开发中我们需要注意
一些优化点
。
- 我们整个页面,其实只有
+
号按钮点击
,触发
中心的计数UI
的更新
。并不需要
每次数据变化,都更新整个外部部件
。- 我们可以将
Chip
更新为一个StatefulWidget
可变部件,点击+
号时,通过回调
让Chip
部件重新渲染
即可。
本节,我们主要分析自动布局
的几种方式
,以及StatefulWidget
和StatefulWidget
的区别。
下一节,Flutter入门四:搭建项目、资源调用、简单开发。从项目入手
,一步步理解
和熟悉Flutter
。