前言
本章直接介绍flutter中常用的布局,其中主要介绍常用的Flex布局,以及常用的一些基础布局组件,最后讲一下组件状态更新(常听到的状态机)
demo地址(内容在statusAndLayout文件夹中,可以替换main中注释尝试效果)
icon图标地址:放在前面方便参考
其中这里面常见的组件以及属性,主要介绍下面这些:
Flex、Column、Row、Container、SizedBox、Alignment、Flexible、Expanded、Stack、Position、Wrap、ListView(滚动视图UIScrollView)、ListView.Builder(表格视图UITableView)、TextButton、MaterialButton、CupertinoButton、Image、Text、TextField、Icon、IconButton
其中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
设置horizontal或者使用 Row
mainAxisAlignment主轴属性
主轴 mainAxisAlignment,参数类型 MainAxisAlignment 枚举,分别为 start、end、center、spaceBetween、spaceAround、spaceEvenly
start
start:默认初始位置,就像上面的 Column 和 Row一样都是默认
Column: 从顶部开始,一个挨着一个
Row: 从左侧开始,一个挨着一个
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
沿子元素文字基线方向对齐,平时用的比较少,可以被上面的参数搭配嵌套代替,不上图了
常用布局介绍
Flex、Column、Row、Container、SizedBox、Alignment、Flexible、Expanded、Stack、Position、Wrap、ListView(滚动视图UIScrollView)、ListView.Builder(表格视图UITableView)、textButton、MaterialButton、CupertinoButton、Image、Text、TextField、Icon、IconButton
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设置的效果
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);
Container和Alignment的使用如下所示,一般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中的默认布局、 android的FrameLayout类似,通过设置个元素的间距,以实现不同位置效果
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使用能随意调整距离边界位置,从而发挥出效果")
)
],
),
);
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
下面是纵向布局,布满横向换行的案例,非常类似,就不多说了
//主轴为纵向布局,布满了之后,换列
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('我是中间的文字')
)
]
),
)
效果如下所示
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)
],
);
}
);
}
}
Text
Text以及富文本 RickText的使用很简单,尝鲜篇已经有介绍,这里不多介绍,可以设置文字布局,也可以设置文字风格
平时都是配合 Container 使用,如果只想简单居中,可以直接使用 Center,Center默认填充父视图布局且内容居中,因此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 其解决了 TextButton 和 ElevatedButton的最小宽度问题,也可以主动设置高度,且拥有点击流水效果,使用比较简单
CupertinoButton 偏向ios风格的按钮,默认带小圆角,可以去掉,最小高度44可以设置,最佳点击高度
MaterialButton(
minWidth: 80,
height: 40,
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (context) => TestWidget()));
},
child: const Text("测试按钮",),
),
Icon、IconButton
这两个也是比较常用的组件,IconButton 是icon专用的button,类似于 TextButton,Icon是一个图标组件,话不多说直接上代码
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')
)
),
)
],
);
}
}
如下所示为显示效果
最后
自己下载下来案例测试一下吧,flutter其实上手写 UI 逻辑、业务逻辑很简单,只要不涉及到硬件以及权限的,基本上不需要用到原生
快来自己动手写一个demo吧