当前位置:   article > 正文

Flutter开发③——组件_flutter 组件

flutter 组件

目录

Container容器组件

decoration属性

 padding和maring属性

transform属性

Text组件

 TextStyle参数

图片组件

Container实现圆形图片

 ClipOval实现圆形图片

 加载本地图片

 图标组件

自带的Icons图标

借助阿里巴巴图标库自定义字体图标

ListView列表组件

垂直列表

水平列表 可左右滑动

 动态列表

 通过ListView的构造函数builder来生成动态列表

GridView网格组件

GridView.count

 GridView.extend

动态生成

GridView.builder实现动态列表

页面布局Padding Row Column Flex Expanded组件

Padding

  线性布局(Row和Column)

Row水平布局组件

Column垂直布局组件

弹性布局(Flex Expanded)

通过Row或Column实现弹性布局

 通过Flex实现弹性布局

层叠布局(Stack、Align、Positioned)

Stack组件

Positioned

获取屏幕宽高:

实现一个导航悬浮在列表顶部

Align

 Align结合Stack实现容器里两个组件分别居左居右

页面布局 AspectRatio Card CircleAvatar

AspectRatio

Card

 Card实现一个通讯录/图文的卡片,如下

 CircleAvatar实现圆形图片

Flutter按钮组件

 带图标的按钮

修改按钮的宽高

自适应按钮

圆角按钮

圆形按钮

修改OutlinedButton边框

Wrap组件

完整实现搜索页布局

StatefulWidget有状态组件

 StatefulWidget实现计数器功能

StatefulWidget实现动态列表

Scaffold组件

BottomNavigationBar自定义底部导航

 BottomNavigationBar常用属性

 接下来实现点击对应的Tab页展示对应的组件

type属性

FloatingActionButton组件实现底部导航凸起按钮

Scaffold  floatingActionButtonLocation

Scaffold 左右侧菜单Drawer

DrawerHeader

 UserAccountsDrawHeader

AppBar TabBar TabBarView实现顶部滑动导航

AppBar自定义顶部按钮图标、颜色

 类似头条的Appbar结合TabBar来实现顶部Tab切换

 1、混入SingleTickerProviderStateMixin

 2、定义TabController

3、配置TabBar和TabBarView

​编辑

TabBar属性

 顶部导航bottomNavigationBar与TabBar一起使用

自定义KeepAliveWrapper缓存页面

 顶部导航监听事件


Container容器组件

基本的代码结构

  1. import 'package:flutter/material.dart';
  2. class MyApp extends StatelessWidget {
  3. const MyApp({super.key});
  4. @override
  5. Widget build(BuildContext context) {
  6. return const Center(
  7. child: Text("hello flutter"),
  8. );
  9. }
  10. }
  11. main() {
  12. runApp(MaterialApp(
  13. home: Scaffold( //类型为Widget表示可以是任意组件类型
  14. appBar: AppBar(
  15. title: const Text("title!"),
  16. ),
  17. body: const MyApp(),
  18. )));
  19. }

container是容器组件,与html中的div较类似。

Constains不是常量构造函数,所以其外部的const也要都去掉

  1. class MyApp extends StatelessWidget {
  2. const MyApp({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return Center(
  6. child: Container(
  7. alignment: Alignment.center, //配置container容器内元素的方位
  8. width: 100,
  9. height: 100,
  10. //decoration设置背景
  11. decoration: const BoxDecoration(
  12. color: Colors.red), //这里提示要用Decoration类装饰decoration,我们用其子类也是可以的
  13. child: const Text(
  14. "hello flutter",
  15. style: TextStyle(color: Colors.white),
  16. ),
  17. ),
  18. );
  19. }
  20. }

decoration属性

 

 decoration设置背景,如背景色、背景边框等

  1. decoration: BoxDecoration(//这里提示要用Decoration类装饰decoration,我们用其子类也是可以的
  2. color: Colors.yellow,
  3. border: Border.all(color: Colors.red, width: 5)
  4. ),

 因为Border.all是个factory构造函数,不是常量的,所以上面的const BoxDecoration需要去掉const

decoration其他属性

  1. borderRadius: BorderRadius.circular(10), //配置圆角,足够大则可以变成圆
  2. boxShadow: const [ //配置阴影效果
  3. BoxShadow(color: Colors.blue, blurRadius: 20.0)
  4. ]

  1. //gradient设置背景颜色渐变,LinearGradient背景线性渐变(下面这个表示红色渐变到黄色),RadialGradient径向渐变
  2. gradient:
  3. const RadialGradient(colors: [Colors.red, Colors.yellow])

通过Container创建一个按钮

  1. class MyButton extends StatelessWidget {
  2. //自定义一个按钮
  3. const MyButton({super.key});
  4. @override
  5. Widget build(BuildContext context) {
  6. return Container(
  7. alignment: Alignment.center,
  8. width: 200,
  9. height: 40,
  10. // 设置外边距,上下左右(EdgeInsets.all)都是10,分开设置EdgeInsets.fromLTRB
  11. margin: const EdgeInsets.all(10),
  12. decoration: BoxDecoration(
  13. color: Colors.blue,
  14. // 这里borderRadius提示应用BorderRadiusGeometry修饰,但BorderRadiusGeometry是个抽象类,我们要用其子类来修饰
  15. // 而BorderRadius是个命名构造函数,不是常量构造函数,外层const应去掉;当然我们也可以用BorderRadius的其他常量构造函数,此时就不用去掉const了,如borderRadius: BorderRadius.all(Radius.circular(10))
  16. borderRadius: BorderRadius.circular(10)
  17. ),
  18. child: const Text(
  19. "按钮",
  20. style: TextStyle(color: Colors.white, fontSize: 20),
  21. ),
  22. );
  23. }
  24. }
  25. main() {
  26. runApp(MaterialApp(
  27. home: Scaffold(
  28. //类型为Widget表示可以是任意组件类型
  29. appBar: AppBar(
  30. title: const Text("title!"),
  31. ),
  32. body: Column(
  33. //通过列加载多个组件
  34. children: const [MyApp(), MyButton()],
  35. ),
  36. )));
  37. }

 padding和maring属性

padding是让容器和里面的元素有相应的间距,margin是容器和容器外部的其他容器有相应的间距

transform属性

让容器旋转等

  1. // 位移,分别向x、y、z位移的距离(在当前位置进行位移)
  2. transform: Matrix4.translationValues(40, 0, 0), //向右,负数则为向左
  3. //旋转
  4. transform: Matrix4.rotationZ(0.2),
  5. //缩放
  6. transform: Matrix4.skewY(0.2),

Text组件

 TextStyle参数

  1. class MyText extends StatelessWidget {
  2. const MyText({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return Container(
  6. width: 200,
  7. height: 200,
  8. margin: const EdgeInsets.fromLTRB(0, 60, 0, 0), //设置和上面一个间隔开一些
  9. decoration: const BoxDecoration(color: Colors.yellow),
  10. child: const Text(
  11. "Text组件",
  12. textAlign: TextAlign.center, //文字居中显示
  13. ),
  14. );
  15. }
  16. }

  1. maxLines: 1, //最多显示一行
  2. overflow: TextOverflow.ellipsis, //溢出时显示的内容

图片组件

Flutter中,可以通过Image组件来加载并显示图片,Image的数据源可以是asset、文件、内存以及网络

Image.asset:本地图片

Image.network:远程图片

Image组件常用属性:

 

 其他属性参数:Image class - widgets library - Dart API

基本代码结构

  1. import 'package:flutter/material.dart';
  2. main(){
  3. runApp(MaterialApp(
  4. home: Scaffold(
  5. appBar: AppBar(title: const Text("title"),),
  6. body: const MyApp(),
  7. )
  8. ));
  9. }
  10. class MyApp extends StatelessWidget {
  11. const MyApp({super.key});
  12. @override
  13. Widget build(BuildContext context) {
  14. return Container();
  15. }
  16. }
  1. Widget build(BuildContext context) {
  2. return Center(
  3. // 一般放在容器里面,比较好控制
  4. child: Container(
  5. height: 150,
  6. width: 150,
  7. decoration: const BoxDecoration(color: Colors.yellow),
  8. child: Image.network("https://img1.baidu.com/it/u=413643897,2296924942&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"),
  9. ),
  10. );
  11. }

 Image.network的一些属性

src:图片源地址

scale:缩放比例,如2即缩小一倍

alignment:设置位置,默认居中,可以通过Alignment.centerLeft设置靠左 

fit:控制图片的拉伸和挤压,如BoxFit.fill为全图显示,图片会被拉伸充满父容器;BoxFit.cover对图片剪裁并充满容器(图片不变形);BoxFit.contain全图显示,默认的;BoxFit

Container实现圆形图片

通过container的decoration来实现

  1. class Circular extends StatelessWidget {
  2. const Circular({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return Container(
  6. height: 150,
  7. width: 150,
  8. decoration: BoxDecoration(
  9. color: Colors.yellow,
  10. // 圆形需要配置成高度的一半
  11. borderRadius: BorderRadius.circular(75),
  12. image: const DecorationImage(
  13. image: NetworkImage("https://img1.baidu.com/it/u=413643897,2296924942&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500"),
  14. fit: BoxFit.cover
  15. )
  16. ),
  17. );
  18. }
  19. }

 ClipOval实现圆形图片

  1. class ClipImage extends StatelessWidget {
  2. const ClipImage({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return ClipOval( //默认可能会是椭圆,配置一下高宽
  6. child: Image.network(
  7. "https://img1.baidu.com/it/u=413643897,2296924942&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500",
  8. width: 150,
  9. height: 150,
  10. fit: BoxFit.cover,
  11. ),
  12. );
  13. }
  14. }
  15. Column(
  16. children: const [MyApp(), SizedBox(height: 20,), Circular(),SizedBox(height: 20,), ClipImage()],
  17. )
  18. Scaffold的children中可以加入一个SizeBox区块实现分隔的效果

 加载本地图片

本项目目录下新建一个images文件夹,里面新建两个文件夹:2.0x、3.0x,然后把文件在images及两个文件夹都放一份

接下来修改pubspec.yaml文件

  1. class LocalImage extends StatelessWidget {
  2. const LocalImage({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return Container(
  6. height: 150,
  7. width: 150,
  8. decoration: const BoxDecoration(color: Colors.yellow),
  9. child: Image.asset("images/a.jpg", fit: BoxFit.cover),
  10. );
  11. }
  12. }

 图标组件

MaterialApp的theme,可以设置主题的相关样式,如ThemeData(primarySwatch:Colors.yellow)设置主题为黄色。

基本结构

  1. import 'package:flutter/material.dart';
  2. main(){
  3. runApp(const MyApp());
  4. }
  5. class MyApp extends StatelessWidget {
  6. const MyApp({super.key});
  7. @override
  8. Widget build(BuildContext context) {
  9. return MaterialApp(
  10. theme: ThemeData(primarySwatch: Colors.yellow),
  11. home:Scaffold(
  12. appBar: AppBar(title: const Text("title"),),
  13. body: const MyHomePage(),
  14. )
  15. );
  16. }
  17. }
  18. class MyHomePage extends StatelessWidget {
  19. const MyHomePage({super.key});
  20. @override
  21. Widget build(BuildContext context) {
  22. return const Text("hello");
  23. }
  24. }

自带的Icons图

  1. class MyHomePage extends StatelessWidget {
  2. const MyHomePage({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return Column(
  6. children: const [
  7. Icon(Icons.home, size: 40, color: Colors.red,) //定义大小和图标颜色
  8. ],
  9. );
  10. }
  11. }

Materia Design所有图标可以在官网查看:https://material.io/tools/icons/

借助阿里巴巴图标库自定义字体图标

也是自定义字体图标,阿里巴巴图标库(iconfont-阿里巴巴矢量图标库)上右很多字体图标素材

可以选择自己需要的图标打包下载后,会生成一些不同格式的字体文件,flutter中用ttf格式即可。

在iconfont下载这些图标的代码

1、导入字体图标文件,假设路径为fonts/iconfont.ttf

 2、在pubsepc.yaml中引入我们的图标文件。

在项目根目录下新建一个fonts文件夹,将从iconfont下载的文件中的.json(json不一定要复制,主要是引入的时候需要用到里面的编码)和.ttf文件放到这个目录下

接下来修改pubspec.yaml中的font

配置一个字体如下:

 接下来再lib下新建一个MyFont.dart引入

  1. MyFont.dart中的内容
  2. import 'package:flutter/material.dart';
  3. class MyFont{
  4. static const IconData book = IconData(
  5. 0xf00a1, //在json文件中每个图标有个unicode编码,再起前面加上0x就是这个参数
  6. fontFamily: "MyIcon", //pubspec.yaml中font参数的family
  7. matchTextDirection: true
  8. );
  9. static const IconData weixin = IconData(
  10. 0xe8bb,
  11. fontFamily: "MyIcon",
  12. matchTextDirection: true
  13. );
  14. static const IconData cart = IconData(
  15. 0xf0179,
  16. fontFamily: "MyIcon",
  17. matchTextDirection: true
  18. );
  19. }

main.dart中引用

  1. import './MyFont.dart';
  2. Icon(MyFont.book, size: 40,color: Colors.orange,),
  3. SizedBox(height: 20,),
  4. Icon(MyFont.weixin, size: 40,color: Colors.green,),
  5. SizedBox(height: 20,),
  6. Icon(MyFont.cart, size: 40,color: Colors.black,),

多个图标文件加载,同样是将.ttf文件保存到font文件夹下,然后修改pubspec.yaml文件

  1. fonts:
  2. - family: MyIcon
  3. fonts:
  4. - asset: fonts/iconfont.ttf
  5. - family: MyIcon1
  6. fonts:
  7. - asset: fonts/iconfont1.ttf

接下来一样在lib的MyFont.dart中如上面那样编写,只不过要注意修改fontFamily参数。

ListView列表组件

列表布局是最常用的一种布局方式。flutter中可以通过ListView来定义列表项,支持垂直和水平方向展示。通过一个属性就可以控制列表的显示方向。

列表有以下几种分类:

  • 垂直列表
  • 垂直图文列表
  • 水平列表
  • 动态列表

前面用Column的时候如果超出界面范围,下面的就不显示了,我们直接把Column换成ListView即可,此时可以滑动。 

垂直列表

  1. return ListView(
  2. children: const [
  3. ListTile(title: Text("列表"),),
  4. Divider(), //横线
  5. ListTile(title: Text("列表"),),
  6. Divider(), //横线
  7. ListTile(title: Text("列表"),),
  8. Divider(), //横线
  9. ListTile(title: Text("列表"),),
  10. ListTile(title: Text("列表"),),
  11. ListTile(title: Text("列表"),),
  12. ListTile(title: Text("列表"),),
  13. ListTile(title: Text("列表"),),
  14. ListTile(title: Text("列表"),),
  15. ListTile(title: Text("列表"),),
  16. ],
  17. );

 ListTile(列表项)的一些属性用法:

title:标题

subtitle:副标题

leading:可以在前面加图标或图片

trailing:可以在后面加图标或图片

onTap:配置点击事件

  1. 列表项显示图标和分割线
  2. return ListView(
  3. children: const [
  4. ListTile(
  5. leading: Icon(Icons.home),
  6. title: Text("首页")
  7. ),
  8. Divider(),
  9. ListTile(
  10. leading: Icon(Icons.assignment, color: Colors.red,),
  11. title: Text("全部订单"),
  12. ),
  13. Divider(),
  14. ListTile(
  15. leading: Icon(Icons.payment, color: Colors.green,),
  16. title: Text("待付款"),
  17. ),
  18. ListTile(
  19. leading: Icon(Icons.favorite, color: Colors.lightGreen,),
  20. title: Text("我的收藏"),
  21. ),
  22. Divider(),
  23. ListTile(
  24. leading: Icon(Icons.people, color: Colors.black54,),
  25. title: Text("在线客服"),
  26. trailing: Icon(Icons.chevron_right_sharp),
  27. )
  28. ],
  29. );

实现图文列表,修改ListTile中的title、leading、subtitle。

  1. return ListView(
  2. padding: const EdgeInsets.fromLTRB(0, 10, 0, 0), //设备内部组件边距
  3. children: [
  4. ListTile(
  5. leading: Image.network("https://pic.rmb.bdstatic.com/bjh/news/80f7dbb7d23db98155e168d17521f5ec6303.jpeg"),
  6. title: const Text("iPhone SE4参数曝光:全面屏设计+A16处理器,起售价3499元起"),
  7. subtitle: const Text("最近关于iPhone SE4的相关爆料也越来越多,尤其是iPhone14系列爆冷,使得很多喜欢小屏党的用户更加期待iPhone SE4这款机型。"),
  8. ),
  9. const Divider(),
  10. ListTile( //前后都加图片
  11. leading: Image.network("https://pic.rmb.bdstatic.com/bjh/news/80f7dbb7d23db98155e168d17521f5ec6303.jpeg"),
  12. title: const Text("iPhone SE4参数曝光:全面屏设计+A16处理器,起售价3499元起"),
  13. subtitle: const Text("最近关于iPhone SE4的相关爆料也越来越多,尤其是iPhone14系列爆冷,使得很多喜欢小屏党的用户更加期待iPhone SE4这款机型。"),
  14. trailing: Image.network("https://pic.rmb.bdstatic.com/bjh/news/dc4994713776e6038b5e79b6646e8c852704.jpeg"),
  15. )
  16. ]
  17. );

 ListView用Widget修饰即可,因此可以用任意的组件。这里我们试试用Image组件

  1. return ListView(
  2. padding: const EdgeInsets.all(10),
  3. children: [
  4. Image.network("https://pic.rmb.bdstatic.com/bjh/news/80f7dbb7d23db98155e168d17521f5ec6303.jpeg"),
  5. Container(
  6. padding: const EdgeInsets.fromLTRB(0, 6, 0, 0),
  7. height: 44,
  8. child: const Text("标题1", textAlign: TextAlign.center,style: TextStyle(fontSize: 22),),
  9. ),
  10. Image.network("https://pic.rmb.bdstatic.com/bjh/news/80f7dbb7d23db98155e168d17521f5ec6303.jpeg"),
  11. Image.network("https://pic.rmb.bdstatic.com/bjh/news/dc4994713776e6038b5e79b6646e8c852704.jpeg"),
  12. ]
  13. );

水平列表 可左右滑动

 ListView中的container的宽度是自适应的,改了也没有用(对于垂直列表来说)

  1. return ListView(
  2. scrollDirection: Axis.horizontal, //水平列表 此时只能指定宽度 高度是自适应的
  3. padding: const EdgeInsets.fromLTRB(0, 6, 0, 0),
  4. children: [
  5. Container(
  6. height: 120,
  7. width: 120,
  8. decoration: const BoxDecoration(color: Colors.red),
  9. ),
  10. Container(
  11. height: 120,width: 120,
  12. decoration: const BoxDecoration(color: Colors.orange),
  13. ),
  14. Container(
  15. height: 120,width: 120,
  16. decoration: const BoxDecoration(color: Colors.black),
  17. ),
  18. Container(
  19. height: 120,width: 120,
  20. decoration: const BoxDecoration(color: Colors.blue),
  21. ),
  22. ],
  23. );

如果让高度有效?

在ListView外层加容器去控制高度

  1. return SizedBox(
  2. height: 120,
  3. child: ListView(
  4. scrollDirection: Axis.horizontal, //水平列表 此时只能指定宽度 高度是自适应的
  5. padding: const EdgeInsets.fromLTRB(0, 6, 0, 0),
  6. children: [
  7. Container(
  8. height: 120,
  9. width: 120,
  10. decoration: const BoxDecoration(color: Colors.red),
  11. ),
  12. Container(
  13. height: 120,width: 120,
  14. decoration: const BoxDecoration(color: Colors.orange),
  15. ),
  16. Container(
  17. height: 120,width: 120,
  18. decoration: const BoxDecoration(color: Colors.black),
  19. ),
  20. Container(
  21. height: 120,width: 120,
  22. decoration: const BoxDecoration(color: Colors.blue),
  23. ),
  24. ],
  25. ),
  26. );

 动态列表

常量构造函数里面不能执行语句,如果需要在构造函数里面跑语句,把const去掉。

for循环生成列表项

  1. class MyHomePage extends StatelessWidget {
  2. const MyHomePage({super.key});
  3. List<Widget> _initListData(){
  4. List<Widget> ls = [];
  5. for(var i=0; i<20; i++){
  6. ls.add(ListTile(title: Text("列表 $i"),));
  7. }
  8. return ls;
  9. }
  10. @override
  11. Widget build(BuildContext context) {
  12. return ListView(
  13. children: _initListData(),
  14. );
  15. }
  16. }
  17. 因为children需要一个list<Widget>类型,所以我们在外面定义一个方法返回List<Widget>即可
  18. 当然这里也可以通过map去遍历
  19. List<Widget> _initListData(){
  20. List listdata = [{"a":123, "b":456}, {"a":7, "b":8}];
  21. var templist = listdata.map((value) {
  22. return ListTile(
  23. leading: Image.network(value['a']),
  24. title: Text(value['b'])
  25. );
  26. });
  27. return templist.toList();
  28. }

 通过ListView的构造函数builder来生成动态列表

  1. class MyHomePage extends StatelessWidget {
  2. List<String> ls=[];
  3. MyHomePage({super.key}){
  4. for (var i=0; i<20; i++){
  5. ls.add("第$i条数据");
  6. }
  7. }
  8. @override
  9. Widget build(BuildContext context) {
  10. return ListView.builder(
  11. itemCount: ls.length, //遍历的长度
  12. itemBuilder: (context, index){ //index0到itemCount
  13. return ListTile(
  14. title: Text(ls[index]),
  15. );
  16. },
  17. );
  18. }
  19. }

GridView网格组件

GridView创建网格列表主要有下面三种方式

1、通过GridView.count实现网格布局

2、通过GridView.extent实现网格布局

3、通过GridView.builder实现动态网格布局

常用属性:

  1. return GridView.count(
  2. crossAxisCount: 5, //一行的Widget数量
  3. children: const [
  4. Icon(Icons.pedal_bike),
  5. Icon(Icons.pedal_bike),
  6. Icon(Icons.pedal_bike),
  7. Icon(Icons.pedal_bike),
  8. Icon(Icons.pedal_bike),
  9. Icon(Icons.pedal_bike),
  10. Icon(Icons.pedal_bike),
  11. Icon(Icons.pedal_bike),
  12. ],
  13. );

 

  1. return GridView.extent(
  2. maxCrossAxisExtent: 200, //横轴元素最大长度 会自动计算每行能显示多少个去排列
  3. children: const [
  4. Icon(Icons.pedal_bike),
  5. Icon(Icons.pedal_bike),
  6. Icon(Icons.pedal_bike),
  7. Icon(Icons.pedal_bike),
  8. Icon(Icons.pedal_bike),
  9. Icon(Icons.pedal_bike),
  10. Icon(Icons.pedal_bike),
  11. Icon(Icons.pedal_bike),
  12. ],
  13. );

GridView.count和GirdView.extent主要区别就是maxCrossAxisExtent和crossAxisCount,其他属性基本一致

GridView.count

  1. class MyHomePage extends StatelessWidget {
  2. const MyHomePage({super.key});
  3. List<Widget> _initData(){
  4. List<Widget> tempList = [];
  5. for (var i=0; i<12; i++){
  6. tempList.add(
  7. Container(
  8. alignment: Alignment.center,
  9. decoration: const BoxDecoration(color: Colors.blue),
  10. child: Text("第${i+1}个元素", style: const TextStyle(fontSize: 20),),
  11. ),
  12. );
  13. }
  14. return tempList;
  15. }
  16. @override
  17. Widget build(BuildContext context) {
  18. return GridView.count(
  19. padding: const EdgeInsets.all(10), //配置GridView离四周的间距
  20. crossAxisCount: 2, //横轴元素最大长度 会自动计算每行能显示多少个去排列
  21. crossAxisSpacing: 10, //配置横轴子元素间距
  22. mainAxisSpacing: 10, //配置垂直子元素间距
  23. childAspectRatio: 0.7, //配置子元素宽高比
  24. children: _initData(),
  25. );
  26. }
  27. }

 GridView.extend

修改maxCrossAxisExtent参数即可,其他一样  

动态生成

  1. 从listData.dart文件中拿数据
  2. listData.dart数据样例
  3. List listData = [
  4. {
  5. "title": "Candy Shop",
  6. "author": "Mohamed Chahin",
  7. "imageUrl": "https://www.itying.com/images/flutter/1.png"
  8. },
  9. {
  10. "title": "Childhood",
  11. "author": "Google",
  12. "imageUrl": "https://www.itying.com/images/flutter/1.png"
  13. },
  14. ];
  15. main.dart
  16. import "listData.dart";
  17. class MyHomePage extends StatelessWidget {
  18. const MyHomePage({super.key});
  19. List<Widget> _initData(){
  20. var templist = listData.map((value){
  21. return Container(
  22. decoration: BoxDecoration(border: Border.all(color: Colors.black26)), //设置边框颜色
  23. child: Column(
  24. children: [
  25. Image.network(value["imageUrl"]),
  26. const SizedBox(height: 10,),
  27. Text(value["title"], style: const TextStyle(fontSize: 18))
  28. ],
  29. ),
  30. );
  31. });
  32. return templist.toList();
  33. }
  34. @override
  35. Widget build(BuildContext context) {
  36. return GridView.count(
  37. padding: const EdgeInsets.all(10), //配置GridView离四周的间距
  38. crossAxisCount: 2, //横轴元素最大长度 会自动计算每行能显示多少个去排列
  39. crossAxisSpacing: 10, //配置横轴子元素间距
  40. mainAxisSpacing: 10, //配置垂直子元素间距
  41. childAspectRatio: 1, //配置子元素宽高比
  42. children: _initData(),
  43. );
  44. }
  45. }

GridView.builder实现动态列表

查看builder源码可以发现有两个必须参数:gridDelegate、itemBuilder

  1. itemBuilder源码追踪:
  2. required IndexedWidgetBuilder itemBuilder,
  3. |
  4. typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
  5. 因此需要一个方法修饰
  6. itemBuilder: (context, index){
  7. },
  1. gridDelegate源码追踪
  2. required this.gridDelegate,
  3. |
  4. final SliverGridDelegate gridDelegate 因此需要SliverGridDelegate
  5. abstract class SliverGridDelegate { 而这个类是个抽象类,我们应该用非抽象的子类
  6. class SliverGridDelegateWithFixedCrossAxisCount extends SliverGridDelegate {
  7. class SliverGridDelegateWithMaxCrossAxisExtent extends SliverGridDelegate {
  8. 这两个都是SliverGridDelegate 的非抽象子类。
  9. 一个就是GridView.count的实现 一个是.extend的实现
  1. class MyApp extends StatelessWidget {
  2. const MyApp({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return MaterialApp(
  6. theme: ThemeData(primarySwatch: Colors.yellow),
  7. home:Scaffold(
  8. appBar: AppBar(title: const Text("title"),),
  9. body: const MyHomePage(),
  10. )
  11. );
  12. }
  13. }
  14. class MyHomePage extends StatelessWidget {
  15. const MyHomePage({super.key});
  16. Widget _initData(context, index){
  17. return Container(
  18. decoration: BoxDecoration(border: Border.all(color: Colors.black26)), //设置边框颜色
  19. child: Column(
  20. children: [
  21. Image.network(listData[index]["imageUrl"]),
  22. const SizedBox(height: 10,),
  23. Text(listData[index]["title"], style: const TextStyle(fontSize: 18))
  24. ],
  25. ),
  26. );
  27. }
  28. @override
  29. Widget build(BuildContext context) {
  30. return GridView.builder(
  31. padding: const EdgeInsets.all(10), //配置GridView离四周的间距
  32. itemCount: listData.length,
  33. gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
  34. crossAxisCount: 2,
  35. crossAxisSpacing: 10,
  36. mainAxisSpacing: 10,
  37. childAspectRatio: 1
  38. ),
  39. itemBuilder: _initData
  40. );
  41. }
  42. }

 gridDelegate用SliverGridDelegateWithMaxCrossAxisExtent 修饰也类似Grid.extend,这是需要用maxCrossAxisExtent修饰即可。

如果出现上面右边图这种场景,可能是溢出了,这时候可以设置宽高比来调整,后续则通过程序来设置。

页面布局Padding Row Column Flex Expanded组件

Padding

让里面的元素和上下左右有间距

Padding功能更单一,比Container组件占用内存更小,因为可以简单功能实现周围间距,可以考虑用Padding而不是创建Container然后设置其padding属性。

  1. class MyHomePage extends StatelessWidget {
  2. const MyHomePage({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return const Padding(
  6. padding: EdgeInsets.all(20),
  7. child: Text("你好flutter"),
  8. );
  9. }
  10. }

  线性布局(Row和Column)

 自定义图表类实现一个红色背景的图标

  1. class MyHomePage extends StatelessWidget {
  2. const MyHomePage({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return IconContainer(Icons.home, color: Colors.red);
  6. }
  7. }
  8. //自定义一个IconContainer,可以传入color和icon实现不同IconContainer
  9. class IconContainer extends StatelessWidget {
  10. Color color;
  11. IconData icon;
  12. /*
  13. this.icon就相当于正常的构造函数使用this.icon=icon一样
  14. */
  15. IconContainer(this.icon, {Key? key, required this.color}) : super(key: key);
  16. @override
  17. Widget build(BuildContext context) {
  18. return Container(
  19. alignment: Alignment.center,
  20. height: 120,
  21. width: 120,
  22. color: color,
  23. child: Icon(icon, color: Colors.white, size:28),
  24. );
  25. }
  26. }

Row水平布局组件

 在上面自定义图标容器类上实现Row

  1. class MyHomePage extends StatelessWidget {
  2. const MyHomePage({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return Row(
  6. children: [
  7. IconContainer(Icons.home, color: Colors.yellow),
  8. IconContainer(Icons.search, color: Colors.red),
  9. IconContainer(Icons.ac_unit_sharp, color: Colors.orange),
  10. ],
  11. );
  12. }
  13. }

mainAxisAlignment: MainAxisAlignment.center,设置居中显示

MainAxisAlignment.spaceBetween设置元素中间有间隔,与两边无间隔

还有spaceAround、spaceEvenly、end等其他可选

 crossAxisAlignment设置次轴的排序方式(相对于外层容器)

如果没有Container包围此时单独设置crossAxisAlignment是没有效果的。

  1. @override
  2. Widget build(BuildContext context) {
  3. return Container(
  4. width: 400,
  5. height: 700,
  6. color: Colors.black12,
  7. child: Row(
  8. mainAxisAlignment: MainAxisAlignment.spaceEvenly,
  9. crossAxisAlignment: CrossAxisAlignment.center,
  10. children: [
  11. IconContainer(Icons.home, color: Colors.yellow),
  12. IconContainer(Icons.search, color: Colors.red),
  13. IconContainer(Icons.ac_unit_sharp, color: Colors.orange),
  14. ],
  15. ),
  16. );
  17. }

 

 如何充满整个屏幕,此时需要把外层Container的宽高设置的足够大,当超过屏幕大小时,Container的宽高就是当前屏幕宽高

  1. return Container(
  2. width: double.infinity,
  3. height: double.infinity,}

double.infinity和double.maxFinite都可以让当前元素的width/height达到父元素的尺寸

Column垂直布局组件

 与Row相反,此时主轴是纵轴,次轴是横轴

弹性布局(Flex Expanded)

Flex组件可以沿水平或垂直方向排列子组件,Row和Column都继承自Flex,参数基本相同,能试用Flex 的地方基本都可以用用Row或Column。Flex本身功能强大,可以和Expanded组件实现弹性布局。(Row或Column也可以和Expanded配合实现弹性布局)

通过Row或Column实现弹性布局

  1. @override
  2. Widget build(BuildContext context) {
  3. return Row(
  4. children: [
  5. Expanded(
  6. flex: 1, //左边占一块
  7. child: IconContainer(Icons.home, color: Colors.yellow), //此时该元素设置宽度是无效的
  8. ),
  9. Expanded(
  10. flex: 2, //左边占一块
  11. child: IconContainer(Icons.search, color: Colors.red), //此时该元素设置宽度是无效的
  12. ),
  13. Expanded(
  14. flex: 3, //左边占一块
  15. child: IconContainer(Icons.ac_unit_sharp, color: Colors.orange), //此时该元素设置宽度是无效的
  16. ),
  17. ],
  18. );
  19. }

 通过Flex实现弹性布局

需要设置direction属性来设置是水平还是垂直排列

  1. @override
  2. Widget build(BuildContext context) {
  3. return Flex(
  4. direction: Axis.vertical,
  5. children: [
  6. Expanded(
  7. flex: 1, //左边占一块
  8. child: IconContainer(Icons.home, color: Colors.yellow), //此时该元素设置宽度是无效的
  9. ),
  10. Expanded(
  11. flex: 2, //左边占一块
  12. child: IconContainer(Icons.search, color: Colors.red), //此时该元素设置宽度是无效的
  13. ),
  14. Expanded(
  15. flex: 3, //左边占一块
  16. child: IconContainer(Icons.ac_unit_sharp, color: Colors.orange), //此时该元素设置宽度是无效的
  17. ),
  18. ],
  19. );
  20. }

左边自适应,右边固定:children中固定宽度的组件就不用Expanded嵌套即可

  1. Widget build(BuildContext context) {
  2. return ListView(
  3. children: [
  4. Container(
  5. width: double.infinity, //填充满行
  6. height: 200,
  7. color: Colors.black,
  8. ),
  9. Row(
  10. children: [
  11. SizedBox(
  12. height: 180,
  13. child: Expanded(
  14. flex: 2,
  15. child: Image.network("https://www.itying.com/images/flutter/2.png", fit: BoxFit.cover)
  16. ),
  17. ),
  18. SizedBox(
  19. height: 180,
  20. child:Expanded(
  21. flex: 1,
  22. child: Column(
  23. children: [
  24. Expanded(
  25. flex: 1,
  26. child: Image.network("https://www.itying.com/images/flutter/3.png", fit: BoxFit.cover)
  27. ),
  28. Expanded(
  29. flex: 1,
  30. child: Image.network("https://www.itying.com/images/flutter/4.png", fit: BoxFit.cover)
  31. ),
  32. ],),),) ],)],);
  33. }

实现这个效果,首先用个ListView实现列表项,其子元素第一行是个黑色的容器、第二个元素是个Row并实现自适应,左边占两块,右边占一块,右边又有两部分构成,通过Column实现自适应,上下各占一块。为了实现第二行固定高度,我们通过SizedBox对第二行进行嵌套

这里第二行右边两个图片没有占满一行,我们也可以对这两个Image在外层加一个父组件SizedBox,设置width=double.infinity来占满整行

层叠布局(Stack、Align、Positioned)

Stack组件

表示堆的意思,可以用Stack或者Stack结合Align或者Stack结合Positioned来实现页面的定位布局

属性说明
alignment 配置所有子元素的显示位置
children子组件

基础的main.dart模板

  1. import 'package:flutter/material.dart';
  2. void main(List<String> args) {
  3. runApp(const MyApp());
  4. }
  5. class MyApp extends StatelessWidget {
  6. const MyApp({super.key});
  7. @override
  8. Widget build(BuildContext context) {
  9. return MaterialApp(
  10. title: "Flutter Demo",
  11. theme: ThemeData(primarySwatch: Colors.blue),
  12. home: Scaffold(
  13. appBar: AppBar(
  14. title: const Text("Flutter App"),
  15. ),
  16. body: const HomePage(),
  17. ),
  18. );
  19. }
  20. }
  21. class HomePage extends StatelessWidget {
  22. const HomePage({super.key});
  23. @override
  24. Widget build(BuildContext context) {
  25. return const Text("Hello Flutter!");
  26. }
  27. }

Stack可以使多个元素堆在在一起

  1. Widget build(BuildContext context) {
  2. return Stack(
  3. children: [
  4. Container(height: 400, width: 300, color: Colors.red,),
  5. Container(height: 200, width: 200, color: Colors.yellow,),
  6. const Text("你好ss flutter")
  7. ],
  8. );
  9. }

通过设置Stack的alignment元素可以设置子元素的排列,如居中:

alignment: Alignment.center,  效果如上面右图(可以发现第一个元素并没有居中)

Stack是相对于父容器定位的,如果没有父容器,就相对于整个屏幕定位。

Positioned

使用Positioned必须设置width和height表示子组件的高宽,否则会报错

 

 Stack结合Positioned控制子元素的显示位置

  1. return Container(
  2. height: 400,
  3. width: 300,
  4. color: Colors.red,
  5. child: Stack(
  6. children: [
  7. Positioned(
  8. left: 0,
  9. bottom: 0, //居于最外层Container左侧和底部0
  10. child:
  11. Container(height: 100, width: 100, color: Colors.yellow)),
  12. const Positioned(
  13. right: 0,
  14. top: 190, //居于右侧中间
  15. child: Text("hello flutter"))
  16. ],
  17. ));

     

获取屏幕宽高:

final size = MediaQuery.of(context).size;

size.width 或size.height即可获取

实现一个导航悬浮在列表顶部

如上面右图

  1. Widget build(BuildContext context) {
  2. // 获取屏幕宽高
  3. final size = MediaQuery.of(context).size;
  4. return Stack(
  5. children: [
  6. ListView(
  7. padding: const EdgeInsets.only(top: 50), //距离顶部一定距离,从而不会遮挡第一个元素
  8. children: const [
  9. ListTile(title: Text("列表1"),),
  10. ListTile(title: Text("列表2"),),
  11. ListTile(title: Text("列表"),),
  12. ListTile(title: Text("列表"),),
  13. ListTile(title: Text("列表"),),
  14. ListTile(title: Text("列表"),),
  15. ListTile(title: Text("列表"),),
  16. ListTile(title: Text("列表"),),
  17. ],
  18. ),
  19. Positioned(
  20. left: 0,
  21. top: 0, //设置在顶部 也可以用bottom设置在底部
  22. width: size.width,
  23. height: 44,
  24. child: Container(
  25. alignment: Alignment.center,
  26. height: 44,
  27. color: Colors.black,
  28. child: const Text("二级导航", style: TextStyle(color: Colors.white)),
  29. ))
  30. ],
  31. );
  32. }

Align

Align组件可以调整子组件的位置,Stack组件中结合Align组件可以控制每个子元素的显示位置

 例如下面这三段代码实现的是同样的效果,均是实现一个子元素在父元素居中显示。

  

  1. Widget build(BuildContext context) {
  2. return Container(
  3. width: 300,
  4. height: 300,
  5. color: Colors.red,
  6. alignment: Alignment.center,
  7. child: const Text("你好 flutter"),
  8. );
  9. Widget build(BuildContext context) {
  10. return Container(
  11. width: 300,
  12. height: 300,
  13. color: Colors.red,
  14. child: const Align(
  15. alignment: Alignment.center,
  16. child: Text("你好 flutter"),
  17. ),
  18. );
  19. }
  20. Widget build(BuildContext context) {
  21. return Container(
  22. width: 300,
  23. height: 300,
  24. color: Colors.red,
  25. child: const Center(
  26. child: Text("你好 flutter"),
  27. ),
  28. );
  29. }

Alignment也可以使用构造函数const Alignment(this.x, this.y)来定位

 Align结合Stack实现容器里两个组件分别居左居右

首先我们会先想到用Row来实现, 结果是不行的,会挤在一起

  1. return Row(
  2. children: const [
  3. Align(
  4. alignment: Alignment.topLeft,
  5. child: Text("收藏"),
  6. ),
  7. Align(
  8. alignment: Alignment.topRight,
  9. child: Text("购买"),
  10. ),
  11. ],
  12. );

只需要将Row改成Stack即可。

接下来通过Column来实现,如果不在外面加容器Container,直接用Column是相对屏幕布局(Column并不知道容器宽高),因此我们在Stack外加个Container,并设置宽高

  1. Widget build(BuildContext context) {
  2. return Column(
  3. children: [
  4. SizedBox(
  5. width: double.infinity,
  6. height: 40,
  7. child: Stack(
  8. children: const [
  9. Positioned(child: Text("收藏"), left: 10,),
  10. Positioned(child: Text("购买"), right: 10,)
  11. ],
  12. ),
  13. )
  14. ],
  15. );
  16. }

页面布局 AspectRatio Card CircleAvatar

AspectRatio

 页面上显示一个容器 宽高是屏幕的宽度 高度是容器宽度的一半

  1. return AspectRatio(
  2. aspectRatio: 2 / 1,
  3. child: Container(
  4. color: Colors.red,
  5. ),
  6. );

Card

 Card实现一个通讯录/图文的卡片,如下

 vscode 保存时会自动格式化代码,可以修改settings.json中"editor.formatOnSave": false 即可。

最简单版本的卡片

  1. return ListView(
  2. children: [
  3. Card(
  4. child: Column(
  5. children: const [
  6. ListTile(title: Text("张三", style: TextStyle(fontSize: 28),),subtitle: Text("高级软件工程师")),
  7. Divider(),
  8. ListTile(title: Text("电话:123456789",)),
  9. ListTile(title: Text("地址:北京市海淀区",)),
  10. ],
  11. )),
  12. Card(
  13. child: Column(
  14. children: const [
  15. ListTile(title: Text("张三", style: TextStyle(fontSize: 28),),subtitle: Text("高级软件工程师")),
  16. Divider(),
  17. ListTile(title: Text("电话:123456789",)),
  18. ListTile(title: Text("地址:北京市海淀区",)),
  19. ],
  20. ))
  21. ],
  22. );

   

 接下来配置阴影的深度, elevation: 10, color则配置卡片背景颜色

shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), 可以配置阴影效果 如圆角。 margin可以配置外边距,shadowColor设置阴影颜色。

  1. Card(
  2. elevation: 10,
  3. margin: const EdgeInsets.all(10),
  4. shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
  5. child: Column(
  6. children: const [
  7. ListTile(title: Text("张三", style: TextStyle(fontSize: 28),),subtitle: Text("高级软件工程师")),
  8. Divider(),
  9. ListTile(title: Text("电话:123456789",)),
  10. ListTile(title: Text("地址:北京市海淀区",)),
  11. ],
  12. )),

实现图文卡片

  1. Widget build(BuildContext context) {
  2. return ListView( //ListView设置的列表项可以滑动
  3. children: [
  4. Card(
  5. shape: RoundedRectangleBorder( //实现圆角阴影
  6. borderRadius: BorderRadius.circular(20)
  7. ),
  8. margin: const EdgeInsets.all(10), //设置一些外边距
  9. child: Column( //通过列元素来实现
  10. children: [
  11. AspectRatio( //列元素第一个行就是一个大图片,AspectRatio可以设置子元素的宽高比
  12. aspectRatio: 16/9, //设置图片宽高比
  13. child: Image.network("https://www.itying.com/images/flutter/3.png", fit: BoxFit.cover,),
  14. ),
  15. ListTile( // 列元素第一行通过ListTile实现,包括主副标题以及左边的图片
  16. leading: ClipOval(
  17. child: Image.network("https://www.itying.com/images/flutter/3.png", fit: BoxFit.cover,height: 40,width: 40,),
  18. ),
  19. title: const Text("title"),
  20. subtitle: const Text("sub title"),
  21. )
  22. ],
  23. ),
  24. ),
  25. ],
  26. );
  27. }

   

这里我们使用ClipOval实现圆形图片,也可以用CircleAvatar实现圆形图片

  1. CircleAvatar(
  2. radius: 200,
  3. backgroundImage: NetworkImage("https://www.itying.com/images/flutter/3.png"),
  4. ),

 CircleAvatar实现圆形图片

效果如上面右图

  1. Widget build(BuildContext context) {
  2. return const CircleAvatar(
  3. radius: 110,
  4. backgroundColor: Color(0xffFDCF09),
  5. child: CircleAvatar(
  6. radius: 100,
  7. backgroundImage:
  8. NetworkImage("https://www.itying.com/images/flutter/3.png"),
  9. ),
  10. );
  11. }

接下来通过数据动态生成Card

  1. import "./res/listdata.dart";
  2. class HomePage extends StatelessWidget {
  3. const HomePage({super.key});
  4. List<Widget> _initCardData() {
  5. var tempList = listData.map((value) {
  6. return Card(
  7. shape: RoundedRectangleBorder(
  8. //实现圆角阴影
  9. borderRadius: BorderRadius.circular(20)),
  10. margin: const EdgeInsets.all(10), //设置一些外边距
  11. child: Column(
  12. //通过列元素来实现
  13. children: [
  14. AspectRatio(
  15. //列元素第一个行就是一个大图片,AspectRatio可以设置子元素的宽高比
  16. aspectRatio: 16 / 9, //设置图片宽高比
  17. child: Image.network(
  18. value["imageUrl"],
  19. fit: BoxFit.cover,
  20. ),
  21. ),
  22. ListTile(
  23. // 列元素第一行通过ListTile实现,包括主副标题以及左边的图片
  24. leading: ClipOval(
  25. child: Image.network(
  26. value["imageUrl"],
  27. fit: BoxFit.cover,
  28. height: 40,
  29. width: 40,
  30. ),
  31. ),
  32. title: Text(value["title"]),
  33. subtitle: Text(value["author"]),
  34. )
  35. ],
  36. ),
  37. );
  38. });
  39. return tempList.toList();
  40. }
  41. @override
  42. Widget build(BuildContext context) {
  43. return ListView(
  44. children: _initCardData(),
  45. );
  46. }
  47. }

Flutter按钮组件

按钮组件的属性

 ButtonStyle里面的常用参数

  1. Widget build(BuildContext context) {
  2. return ListView(children: [
  3. Row(
  4. mainAxisAlignment: MainAxisAlignment.spaceAround, //居中
  5. children: [
  6. ElevatedButton(
  7. onPressed: () { //设置点击时的回调函数
  8. print("ElevatedButton");
  9. },
  10. child: const Text("普通按钮")),
  11. TextButton(onPressed: () {}, child: const Text("文本按钮")),
  12. const OutlinedButton(onPressed: null, child: Text("带边框的按钮")),
  13. IconButton(onPressed: (){}, icon: const Icon(Icons.thumb_up)) //图标按钮,
  14. ],
  15. )
  16. ]);
  17. }

  

 带图标的按钮

ElevatedButton、TextButton、OutlinedButton都有一个icon构造函数,通过这个可以创建带图标的按钮(如上面右图)

  1. Row(
  2. children: [
  3. ElevatedButton.icon(onPressed: (){}, icon:const Icon(Icons.send), label:const Text("发送")),
  4. TextButton.icon(onPressed: null, icon: const Icon(Icons.info), label: const Text("消息")),
  5. OutlinedButton.icon(onPressed: null, icon: const Icon(Icons.add), label: const Text("增加"))
  6. ],
  7. )

修改按钮的背景颜色已经文字颜色,通过style参数来设置

  1. ElevatedButton(
  2. style: ButtonStyle(
  3. backgroundColor: MaterialStateProperty.all(Colors.red), //背景颜色
  4. foregroundColor: MaterialStateProperty.all(Colors.black) //文字和图标的颜色
  5. ),
  6. onPressed: () {}, child: const Text("修改颜色")
  7. ),

修改按钮的宽高

可以发现ElevatedButton组件里面并没有设置宽高的参数,因此我们需要在ElevatedButton外层加一个SizedBox或者Container来控制宽高

  1. SizedBox(
  2. height: 80,
  3. width: 200,
  4. child: ElevatedButton(
  5. style: ButtonStyle(
  6. backgroundColor: MaterialStateProperty.all(Colors.red), //背景颜色
  7. foregroundColor: MaterialStateProperty.all(Colors.black) //文字和图标的颜色
  8. ),
  9. onPressed: () {}, child: const Text("设置宽高")
  10. ),
  11. )

自适应按钮

在外层嵌套一个Expanded

  1. Row(
  2. mainAxisAlignment: MainAxisAlignment.center,
  3. children: [
  4. Expanded(child: Container(
  5. height: 60,
  6. margin: const EdgeInsets.all(10), //通过设置外边距实现
  7. child: ElevatedButton(
  8. child: const Text("自适应按钮"),
  9. onPressed: () {},
  10. ),
  11. ))
  12. ],
  13. )

圆角按钮

ElevatedButton style中的shape来设置圆角

  1. ElevatedButton(
  2. child: const Text("自适应按钮"),
  3. onPressed: () {},
  4. //设置圆角效果
  5. style: ButtonStyle(shape: MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)))),
  6. ),

圆形按钮

同样设置ElevatedButton style中的shape

  1. ElevatedButton(
  2. child: const Text("自适应按钮"),
  3. onPressed: () {},
  4. style: ButtonStyle(shape: MaterialStateProperty.all(CircleBorder(side: BorderSide(color: Colors.yellow)))),
  5. ),

修改OutlinedButton边框

这个按钮组件默认有边框,不带阴影且背景透明。按下后,边框颜色会变亮,同时出现背景和阴影。

  1. Row(
  2. mainAxisAlignment: MainAxisAlignment.center,
  3. children: [
  4. OutlinedButton(
  5. style: ButtonStyle(
  6. //设置边框粗细以及边框颜色
  7. side: MaterialStateProperty.all(const BorderSide(width: 1, color: Colors.red))
  8. ),
  9. onPressed: (){}, child: const Text("边框按钮")
  10. )
  11. ],
  12. )

Wrap组件

Wrap可以实现流布局,单行的Wrap跟Row几乎一致,单列的Wrap则跟Row几乎一致。但Row与Column都是单行单列,Wrap突破了这个限制,mainAxis上空间不足时,则向crossAxis上扩展显示。

例如实现下面的效果:

 

最简单的Wrap效果,如上面右图

  1. class HomePage extends StatelessWidget {
  2. const HomePage({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return Wrap(
  6. children: [
  7. Button("test1", onPressed: () {}),
  8. Button("test2", onPressed: () {}),
  9. Button("test3", onPressed: () {}),
  10. Button("test4", onPressed: () {}),
  11. Button("test5", onPressed: () {}),
  12. Button("test6", onPressed: () {}),
  13. Button("test7", onPressed: () {}),
  14. Button("test8", onPressed: () {}),
  15. Button("test9", onPressed: () {}),
  16. ],
  17. );
  18. }
  19. }
  20. class Button extends StatelessWidget {
  21. String text; //按钮文字
  22. void Function()? onPressed; //按钮回调函数
  23. Button(this.text, {super.key, required this.onPressed});
  24. @override
  25. Widget build(BuildContext context) {
  26. return ElevatedButton(
  27. style: ButtonStyle(
  28. foregroundColor: MaterialStateProperty.all(Colors.black45),
  29. backgroundColor:
  30. MaterialStateProperty.all(Color.fromARGB(240, 202, 199, 199))),
  31. onPressed: onPressed,
  32. child: Text(text));
  33. }
  34. }

 spacing 设置在主轴(x轴/横轴)上的间距;

runSpacing:设置在y轴 纵轴上的间距

如果要设置外边距 可以在Wrap外面嵌套一个Padding组件。

direction:设置主轴方向(默认是横轴),使用Axis.vertical可以改成纵轴为主轴;

alignment:设置对其方式,如居中 居左等;

完整实现搜索页布局

  1. class HomePage extends StatelessWidget {
  2. const HomePage({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return ListView(
  6. padding: const EdgeInsets.all(10),
  7. children: [
  8. Row(
  9. children: [
  10. Text("热搜", style: Theme.of(context).textTheme.titleLarge,)
  11. ],
  12. ),
  13. const Divider(),
  14. Wrap(
  15. spacing: 10,
  16. runSpacing: 10,
  17. children: [
  18. Button("test1", onPressed: () {}),
  19. Button("test2", onPressed: () {}),
  20. Button("test3", onPressed: () {}),
  21. Button("test4", onPressed: () {}),
  22. Button("test5", onPressed: () {}),
  23. Button("test6", onPressed: () {}),
  24. ],
  25. ),
  26. const SizedBox(height: 10,),
  27. Row(
  28. children: [
  29. Text("历史记录", style: Theme.of(context).textTheme.titleLarge,)
  30. ],
  31. ),
  32. Column(
  33. children: const [
  34. ListTile(title: Text("服装"),),
  35. Divider(),
  36. ListTile(title: Text("手机"),),
  37. Divider(),
  38. ListTile(title: Text("电脑"),),
  39. ],
  40. ),
  41. Padding(
  42. padding: const EdgeInsets.all(40),
  43. child: OutlinedButton.icon(
  44. style: ButtonStyle(
  45. foregroundColor: MaterialStateProperty.all(Colors.black45)
  46. ),
  47. onPressed: (){}, icon: const Icon(Icons.delete), label: const Text("清空记录")
  48. ),
  49. ),
  50. ],
  51. );
  52. }
  53. }
  54. class Button extends StatelessWidget {
  55. String text; //按钮文字
  56. void Function()? onPressed; //按钮回调函数
  57. Button(this.text, {super.key, required this.onPressed});
  58. @override
  59. Widget build(BuildContext context) {
  60. return ElevatedButton(
  61. style: ButtonStyle(
  62. foregroundColor: MaterialStateProperty.all(Colors.black45),
  63. backgroundColor:
  64. MaterialStateProperty.all(Color.fromARGB(240, 202, 199, 199))),
  65. onPressed: onPressed,
  66. child: Text(text));
  67. }
  68. }

StatefulWidget有状态组件

 StatefulWidget实现计数器功能

首先我们用传统的StatelessWidget 来实现这个功能

  1. import "./res/listdata.dart";
  2. import 'package:flutter/material.dart';
  3. void main(List<String> args) {
  4. runApp(const MyApp());
  5. }
  6. class MyApp extends StatelessWidget {
  7. const MyApp({super.key});
  8. @override
  9. Widget build(BuildContext context) {
  10. return MaterialApp(
  11. title: "Flutter Demo",
  12. theme: ThemeData(primarySwatch: Colors.blue),
  13. home: Scaffold(
  14. appBar: AppBar(
  15. title: const Text("Flutter App"),
  16. ),
  17. body: HomePage(),
  18. ),
  19. );
  20. }
  21. }
  22. class HomePage extends StatelessWidget {
  23. int countNum = 0; //定义变量
  24. HomePage({super.key}); //构造函数此时也不是常量的了
  25. @override
  26. Widget build(BuildContext context) {
  27. return Center(
  28. child: Column(
  29. mainAxisAlignment: MainAxisAlignment.center,
  30. children: [
  31. Text(
  32. "$countNum",
  33. style: Theme.of(context).textTheme.headline1,
  34. ),
  35. const SizedBox(
  36. height: 100,
  37. ),
  38. ElevatedButton(
  39. onPressed: () {
  40. countNum++;
  41. print(countNum);
  42. },
  43. child: const Text("增加"))
  44. ],
  45. ),
  46. );
  47. }
  48. }

  

 可以发现 虽然点击按钮countNum会增加,但是页面上并不会变化,永远显示的是最开始的0.

接下来用StatefulWidget来实现:

StatefulWidget(抽象类)里有一个createState抽象方法,因此要继承抽象类就必须实现其中的抽象方法。

createState返回State对象,因此我们还需要有一个类实现State类。

基本的结构

  1. class HomePage extends StatefulWidget {
  2. const HomePage({super.key});
  3. @override
  4. State<HomePage> createState() => _HomePageState();
  5. }
  6. class _HomePageState extends State<HomePage> {
  7. @override
  8. Widget build(BuildContext context) {
  9. return Container();
  10. }
  11. }

接下来在_HomePageState的build方法实现这个功能

  1. class _HomePageState extends State<HomePage> {
  2. int numCount = 0;
  3. @override
  4. Widget build(BuildContext context) {
  5. return Center(
  6. child: Column(
  7. mainAxisAlignment: MainAxisAlignment.center,
  8. children: [
  9. Text(
  10. "$numCount",
  11. style: Theme.of(context).textTheme.headline2,
  12. ),
  13. const SizedBox(
  14. height: 60,
  15. ),
  16. ElevatedButton(
  17. onPressed: () {
  18. setState(() { //这个只能在StatefulWidget使用,StatelessWidget是没有的
  19. numCount++;
  20. print(numCount);
  21. });
  22. },
  23. child: const Text("增加"))
  24. ],
  25. ),
  26. );
  27. }
  28. }

 可以发现每次点击都走了一次build()

Scaffold有一个floatingActionButton属性 可以在右下角设置一个浮动的按钮,我们通过这个也能实现增加的功能

StatefulWidget实现动态列表

基本结构

  1. import "./res/listdata.dart";
  2. import 'package:flutter/material.dart';
  3. void main(List<String> args) {
  4. runApp(const MyApp());
  5. }
  6. class MyApp extends StatelessWidget {
  7. const MyApp({super.key});
  8. @override
  9. Widget build(BuildContext context) {
  10. return MaterialApp(
  11. title: "Flutter Demo",
  12. theme: ThemeData(primarySwatch: Colors.blue),
  13. home: const HomePage());
  14. }
  15. }

    

final定于的List,在运行时可以多次add 往里面添加元素。

  1. class HomePage extends StatefulWidget {
  2. const HomePage({super.key});
  3. @override
  4. State<HomePage> createState() => _HomePageState();
  5. }
  6. class _HomePageState extends State<HomePage> {
  7. List<String> list = [];
  8. @override
  9. Widget build(BuildContext context) {
  10. return Scaffold(
  11. appBar: AppBar(
  12. title: const Text("Flutter App"),
  13. ),
  14. floatingActionButton: FloatingActionButton(
  15. child: const Icon(Icons.add),
  16. onPressed: () {
  17. // 改变数据必须在setState中
  18. setState(() {
  19. list.add("新增的列表");
  20. });
  21. },
  22. ),
  23. body: ListView(
  24. //遍历数据生成ListTile
  25. children: list.map((value) {
  26. return ListTile(
  27. title: Text(value),
  28. );
  29. }).toList()));
  30. }
  31. }

Scaffold组件

BottomNavigationBar自定义底部导航

 BottomNavigationBar常用属性

  1. class MyApp extends StatelessWidget {
  2. const MyApp({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return MaterialApp(
  6. title: "Flutter Demo",
  7. theme: ThemeData(primarySwatch: Colors.blue),
  8. home: Scaffold(
  9. appBar: AppBar(title: const Text("Flutter"),),
  10. body: const Text("Flutter1"),
  11. bottomNavigationBar: BottomNavigationBar(
  12. items: const [
  13. BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
  14. BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置")
  15. ],
  16. ),
  17. )
  18. );
  19. }
  20. }

   

currentIndex索引从0开始

iconSize:设置底部菜单大小

fixedColor:设置底部菜单选中时的颜色

onTap:(传入的参数value是当前选中的Tab页的索引)

onTap: (value) {print(value);},

因此要实现点击Tab项,切换到对应的Tab页,我们需要设置变量,onTap回调函数中设置currentIndex。(需要在StatefulWidget实现,它才是有状态的)

  1. class MyApp extends StatelessWidget {
  2. const MyApp({super.key});
  3. @override
  4. Widget build(BuildContext context) {
  5. return MaterialApp(
  6. title: "Flutter Demo",
  7. theme: ThemeData(primarySwatch: Colors.blue),
  8. home: const Tabs());
  9. }
  10. }
  11. class Tabs extends StatefulWidget {
  12. const Tabs({super.key});
  13. @override
  14. State<Tabs> createState() => _TabsState();
  15. }
  16. class _TabsState extends State<Tabs> {
  17. int _currentIndex = 0;
  18. @override
  19. Widget build(BuildContext context) {
  20. return Scaffold(
  21. appBar: AppBar(
  22. title: const Text("Flutter"),
  23. ),
  24. body: const Text("Flutter1"),
  25. bottomNavigationBar: BottomNavigationBar(
  26. currentIndex: _currentIndex,
  27. onTap: (value) {
  28. setState(() {
  29. _currentIndex = value;
  30. });
  31. print(value);
  32. },
  33. items: const [
  34. BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
  35. BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置")
  36. ],
  37. ),
  38. );
  39. }
  40. }

  

 接下来实现点击对应的Tab页展示对应的组件

 在lib新建文件夹pages,在pages中新建一个tabs文件夹,tabs中对应三个Tab页建立category.dart、home.dart、setting.dart三个文件。

目录结构

   

 main.dart

  1. import 'package:flutter/material.dart';
  2. import './pages/tabs.dart';
  3. void main(List<String> args) {
  4. runApp(const MyApp());
  5. }
  6. class MyApp extends StatelessWidget {
  7. const MyApp({super.key});
  8. @override
  9. Widget build(BuildContext context) {
  10. return MaterialApp(
  11. title: "Flutter Demo",
  12. theme: ThemeData(primarySwatch: Colors.blue),
  13. home: const Tabs());
  14. }
  15. }

tabs.dart

  1. import 'package:flutter/material.dart';
  2. import './tabs/category.dart';
  3. import './tabs/home.dart';
  4. import './tabs/setting.dart';
  5. class Tabs extends StatefulWidget {
  6. const Tabs({super.key});
  7. @override
  8. State<Tabs> createState() => _TabsState();
  9. }
  10. class _TabsState extends State<Tabs> {
  11. int _currentIndex = 0;
  12. final List<Widget> _pages = const [HomePage(), CategoryPage(), SettingPage()];
  13. @override
  14. Widget build(BuildContext context) {
  15. return Scaffold(
  16. appBar: AppBar(
  17. title: const Text("Flutter"),
  18. ),
  19. body: _pages[_currentIndex],
  20. bottomNavigationBar: BottomNavigationBar(
  21. currentIndex: _currentIndex,
  22. onTap: (value) {
  23. setState(() {
  24. _currentIndex = value;
  25. });
  26. print(value);
  27. },
  28. items: const [
  29. BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
  30. BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
  31. BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置")
  32. ],
  33. ),
  34. );
  35. }
  36. }

home.dart/category.dart/setting.dart

  1. import 'package:flutter/material.dart';
  2. class HomePage extends StatefulWidget {
  3. const HomePage({super.key});
  4. @override
  5. State<HomePage> createState() => _HomePageState();
  6. }
  7. class _HomePageState extends State<HomePage> {
  8. @override
  9. Widget build(BuildContext context) {
  10. return const Center(
  11. child: Text("首页"),
  12. );
  13. }
  14. }
type属性

当配置三个以上菜单时,需要设置type,否则会有菜单被挤掉

设置为type: BottomNavigationBarType.fixed

FloatingActionButton组件实现底部导航凸起按钮

设置Scaffold的floatingActionButton属性。

 

 基本代码与前面一直,加多了两个tab

FloatingActionButton可实现浮动按钮,常用属性如下:

简单设置,这个浮动按钮会在底部右侧:

 

我们需要将他放到消息这个tab中间。

Scaffold  floatingActionButtonLocation

接下来通过设置Scaffold的floatingActionButtonLocation属性可以设置这个按钮在哪个位置,如设置floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked 可以在底部中间。

要设置这个悬浮按钮的大小,我们需要在外层用一个Container,因为 FloatingActionButton无法指定宽高。

并且点击这个按钮还可以跳转到消息页面

tabs.dart

  1. import 'package:flutter/material.dart';
  2. import './tabs/category.dart';
  3. import './tabs/home.dart';
  4. import './tabs/setting.dart';
  5. import './tabs/message.dart';
  6. import './tabs/user.dart';
  7. class Tabs extends StatefulWidget {
  8. const Tabs({super.key});
  9. @override
  10. State<Tabs> createState() => _TabsState();
  11. }
  12. class _TabsState extends State<Tabs> {
  13. int _currentIndex = 0;
  14. final List<Widget> _pages = const [
  15. HomePage(),
  16. CategoryPage(),
  17. MessagePage(),
  18. SettingPage(),
  19. UserPage()
  20. ];
  21. @override
  22. Widget build(BuildContext context) {
  23. return Scaffold(
  24. appBar: AppBar(
  25. title: const Text("Flutter"),
  26. ),
  27. body: _pages[_currentIndex],
  28. bottomNavigationBar: BottomNavigationBar(
  29. type: BottomNavigationBarType.fixed,
  30. currentIndex: _currentIndex,
  31. onTap: (value) {
  32. setState(() {
  33. _currentIndex = value;
  34. });
  35. print(value);
  36. },
  37. items: const [
  38. BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"),
  39. BottomNavigationBarItem(icon: Icon(Icons.category), label: "分类"),
  40. BottomNavigationBarItem(icon: Icon(Icons.message), label: "消息"),
  41. BottomNavigationBarItem(icon: Icon(Icons.settings), label: "设置"),
  42. BottomNavigationBarItem(icon: Icon(Icons.person), label: "用户")
  43. ],
  44. ),
  45. floatingActionButton: Container(
  46. height: 60,
  47. width: 60,
  48. padding: const EdgeInsets.all(2),
  49. margin: const EdgeInsets.only(top: 6),
  50. decoration: BoxDecoration(
  51. color: Colors.yellow, borderRadius: BorderRadius.circular(30)),
  52. child: FloatingActionButton(
  53. child: const Icon(Icons.add),
  54. onPressed: () {
  55. setState(() {
  56. _currentIndex = 2;
  57. });
  58. },
  59. ),
  60. ),
  61. floatingActionButtonLocation:
  62. FloatingActionButtonLocation.centerDocked);
  63. }
  64. }

也可以设置点击悬浮按钮时修改背景颜色。

FloatingActionButton有个backgroundColor属性就用于设置按钮背景颜色

backgroundColor: _currentIndex==2? Colors.red: Colors.blue,

Scaffold 左右侧菜单Drawer

 可以发现左右出现了按钮 点击就可以弹出左右的菜单栏,也可以通过滑动引出。

DrawerHeader

 在Drawer的child元素中可以添加一个Column,里面的chidren设置DrawerHeader就可以设置左右侧菜单栏的头部了。

 UserAccountsDrawHeader

 使用内置的UserAccountsDrawHeader组件快速实现左侧菜单栏头部用户信息的效果

 

AppBar TabBar TabBarView实现顶部滑动导航

AppBar自定义顶部按钮图标、颜色

 首先在顶部左侧加一个按钮,通过leading来设置;

设置actions来在AppBar的右边加一些图标或者按钮;

 

 实现后右上角有一个debug图标,我们通过MaterialApp去掉这个图标

 类似头条的Appbar结合TabBar来实现顶部Tab切换

 1、混入SingleTickerProviderStateMixin

 2、定义TabController

3、配置TabBar和TabBarView

TabBar中的tabs与TabBarView中children的元素一一对应,点击tab的哪一个就对显示tabBarView中的哪一个。

  1. class _HomePageState extends State<HomePage>
  2. with SingleTickerProviderStateMixin {
  3. late TabController _tabController;
  4. //生命周期函数:当组件初始化时会触发
  5. @override
  6. void initState() {
  7. super.initState();
  8. //length为tab的数量
  9. _tabController = TabController(length: 3, vsync: this);
  10. }
  11. @override
  12. Widget build(BuildContext context) {
  13. return Scaffold(
  14. appBar: AppBar(
  15. backgroundColor: Colors.red,
  16. title: const Text("Flutter App!"),
  17. leading: IconButton(
  18. onPressed: (() {
  19. print("左侧按钮图标!");
  20. }),
  21. icon: const Icon(Icons.menu)),
  22. actions: [
  23. IconButton(
  24. onPressed: (() {
  25. print("右侧搜索图标!");
  26. }),
  27. icon: const Icon(Icons.search)),
  28. IconButton(
  29. onPressed: (() {
  30. print("更多");
  31. }),
  32. icon: const Icon(Icons.more_horiz)),
  33. ],
  34. bottom: TabBar(
  35. controller: _tabController,
  36. tabs: const [
  37. Tab(child: Text("关注"),),
  38. Tab(child: Text("热门"),),
  39. Tab(child: Text("视频"),),
  40. ],
  41. ),
  42. ),
  43. body: TabBarView(
  44. controller: _tabController,
  45. children: [
  46. ListView(
  47. children: const[
  48. ListTile(title: Text("关注列表"),),
  49. ],
  50. ),
  51. ListView(
  52. children: const[
  53. ListTile(title: Text("热门列表"),),
  54. ],
  55. ),
  56. ListView(
  57. children: const[
  58. ListTile(title: Text("视频列表"),),
  59. ],
  60. )
  61. ]
  62. )
  63. );
  64. }
  65. }
TabBar属性

 顶部导航bottomNavigationBar与TabBar一起使用

在底部导航的各个tab中都加上Scaffold并且实现tabbar,相当于Scaffold中嵌套Scaffold

home.dart

  1. import 'package:flutter/material.dart';
  2. class HomePage extends StatefulWidget {
  3. const HomePage({super.key});
  4. @override
  5. State<HomePage> createState() => _HomePageState();
  6. }
  7. class _HomePageState extends State<HomePage>
  8. with SingleTickerProviderStateMixin {
  9. late TabController _tabController;
  10. @override
  11. void initState() {
  12. super.initState();
  13. _tabController = TabController(length: 3, vsync: this);
  14. }
  15. @override
  16. Widget build(BuildContext context) {
  17. return Scaffold(
  18. appBar: PreferredSize(
  19. preferredSize: const Size.fromHeight(40), //配置AppBar的高度,改成40之后可能会太小 导致底部指示器被盖住,可以在AppBar外层在设置一个Container,指定一个更小的高度,从而让指示器显示出来.
  20. child: AppBar(
  21. elevation: 1, //设置阴影
  22. backgroundColor: Colors.white, //设置Appbar背景颜色
  23. title: TabBar(
  24. isScrollable: true,
  25. indicatorColor: Colors.red, //设置底部指示器颜色
  26. unselectedLabelColor: Colors.black, //label未选中时的颜色
  27. labelColor: Colors.red,
  28. indicatorSize: TabBarIndicatorSize.label, //底部指示器的宽度=label宽度
  29. controller: _tabController,
  30. tabs: const [
  31. Tab(child: Text("关注"),),
  32. Tab(child: Text("热门"),),
  33. Tab(child: Text("热门"),),
  34. ],
  35. ),
  36. ),
  37. ),
  38. body: TabBarView(
  39. controller: _tabController,
  40. children: const [
  41. Text("关注"),
  42. Text("热门"),
  43. Text("热门"),
  44. ],
  45. ),
  46. );
  47. }
  48. }

自定义KeepAliveWrapper缓存页面

比如从一个tab切换到另一个tab,不使用状态保存的话切换回去会从最开始的地方展示;而保存状态的话即使切换回去也是从之前的地方开始展示。

KeepAliveWrapper.dart

  1. import 'package:flutter/material.dart';
  2. class KeepAliveWrapper extends StatefulWidget {
  3. const KeepAliveWrapper(
  4. {super.key, required this.child, this.keepAlive = true});
  5. final Widget? child;
  6. final bool keepAlive;
  7. @override
  8. State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
  9. }
  10. class _KeepAliveWrapperState extends State<KeepAliveWrapper>
  11. with AutomaticKeepAliveClientMixin {
  12. @override
  13. Widget build(BuildContext context) {
  14. return widget.child!;
  15. }
  16. @override
  17. bool get wantKeepAlive => widget.keepAlive;
  18. @override
  19. void didUpdateWidget(covariant KeepAliveWrapper oldWidget) {
  20. if (oldWidget.keepAlive != widget.keepAlive) {
  21. //keepAlive状态需要更新,实现在AutomaticKeepAliveClientMixin中
  22. updateKeepAlive();
  23. }
  24. super.didUpdateWidget(oldWidget);
  25. }
  26. }

使用,如home.dart中

  1. import '../../tools/keepAliveWrapper.dart';
  2. 只需在需要保存状态的组件外层调用一个KeepAliveWrapper

 顶部导航监听事件

在initState函数中监听tab改变事件,此处既能监听点击也能监听滑动事件

这里要注意一下_tabController.animation是可空类型,因此我们需要用可空断言 !.

 或者再TabBar中也可以通过onTap设置监听函数(只能监听点击事件,不能监听滑动事件)

 能获取到索引就能动态变化数据了。

  1. @override
  2. void dispose() {
  3. // 销毁_tabController
  4. super.dispose();
  5. _tabController.dispose();
  6. }

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/289642
推荐阅读
相关标签
  

闽ICP备14008679号