Flutter入门到入土[总]
Flutter
Flutter是谷歌基于Dart语言开发的一款开源、免费且跨平台的App开发框架,所以建议先学习Dart语言的基本语法,很像JavaScript和Java,学起来很快。这篇文章打算直接总结如何使用Flutter框架,所以比较长,但是讲解的东西比较简短
环境配置
安装JDK
-
前往Java Downloads - Oracle下载所需要的jdk,JDK17版本够用了
-
配置系统环境
- 新建系统变量,第一行输入
JAVA_HOME
,第二行输入安装jdk的目录
-
在系统变量中找到Path双击点开,点击新建,并输入
%JAVA_HOME%\bin
-
一直点确定,然后再cmd中输入
java -version
,一般来说出现的就是版本号,若不行,重启下电脑,若还不行,重新安装吧!
- 新建系统变量,第一行输入
安装Android Studio
-
前往下载 Android Studio ,并安装
-
安装完毕之后,点击左边的第三项,插件,我这里汉化了,汉化教程
-
搜索并安装这两个插件,然后重启Android Studio
安装FlutterSDK
-
前往Flutter官网下载FlutterSDK,Flutter SDK archive - Flutter
-
将下载好的压缩包解压,然后配置环境,双击系统变量的Path,新建并输入刚刚解压到的文件夹到bin目录的路径
-
在cmd中输入
flutter doctor
,等待一下,出现这些前三项和倒数三项不是x一般就能用,如果第三项出现问题,先安装Android Studio
-
常见问题
若第三项出现问题,先安装Android Studio
Android-SDK的系统环境,未配置,在系统变量中新建并第一行输入
ANDROID_HOME
,第二行输入你安装Android Studio时,安装Android-SDK的路径,然后重启电脑,一般就行了如果
flutter doctor
时出现cmdline-tools component is missing
根据以下步骤:
-
打开Android Studio,关闭项目,到开始页面,点最右边的竖着的三个点,选择SDK Manager
-
点第二项SDK Tools,在下面找到Android SDK Command-line Tools并勾选,点击确定
-
点击确定[我这里因为已经最新了,所以下载的东西与你们出问题的不一样]
-
等待这个界面下载,下载完成就点完成
如果还不行,推荐重新安装Android SDK!
-
创建Flutter项目并运行
创建
-
一切准备好之后,会看到Android Studio右上角有个
New Flutter Projec
t的按钮,点这个按钮是为了创建Flutter项目 -
点进去之后,右边点击Flutter,然后下一步
-
这里就是项目的基本信息
- 项目名称:随便你想怎么起怎么起
- 项目路径:看你想放哪
- 项目描述:这一条无所谓
- 项目类型:想做app就选Application
- 所属组织:随你
- 安卓系统的编译语言:一般选第二项
- IOS系统的编译语言:一般选第二项
- 所在平台:一般全选
然后填好之后,点左下角创建
最后,我们需要编写的文件,就在lib里
运行
先点击右上角的手机图标,然后选择Edge,再点击运行,如果没有这种运行,左边的项目找到项目的目录,然后点开lib,再双击.dart文件,再编辑器右键点击小三角运行
flutter的基本要素
-
创建第一个flutter,需要导入一个包
package:flutter/material.dart
-
使用main函数,让他可以运行runApp()方法
-
runApp()需要填写容器,这里先使用Center
-
Center里面需要填写child
-
Text()即是创建一个文本到Center里
-
Text里可以有textDirection为让文字从左到右[ltr]还是从右到左[rtl]
-
Text里的style可以改变文字样式
import 'package:flutter/material.dart'; //demo1-Center void main() { runApp(const Center( child: Text( "hello world!", textDirection: TextDirection.ltr, style: TextStyle( fontSize: 20.0, color: Colors.brown ), ) )); }
MaterialApp and Scaffold
MaterialApp常用的属性
属性名 | 用处 |
---|---|
home | 主页 |
title | 标题 |
color | 颜色 |
theme | 主题 |
routes | 路由 |
MaterialApp-home
填写容器,一般以Scaffold为主,Scaffold是与MaterialApp的另一种组件
import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
runApp(MaterialApp(
home: Scaffold()
));
}
MaterialApp-title|color
MaterialApp的标题和颜色,但是一般不用
MaterialApp-theme
整个软件的主题,填写的参数有要求
ThemeData.light()
:白色主题ThemeData.dark()
:黑色主题
import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
runApp(MaterialApp(
theme:ThemeData.dark()
));
}
MaterialApp-routes
none
Scaffold的常用属性
- appBar - 用于显示顶部部分
- body - 用于显示主要内容
- drawer - 左边抽屉菜单
Scaffold-appBar
如何使用appBar
import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
runApp(MaterialApp(
home:Scaffold(
appBar:AppBar(
title:Text("demo2")
)
));
}
其实appBar还有一些属性
- backgroundColor - 用于设置背景颜色
- centerTitle - 是否居中文字
import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
runApp(MaterialApp(
home:Scaffold(
appBar:AppBar(
title:Text("demo2")
),
backgroundColor: Colors.lightGreen,
centerTitle: true,
)
));
}
Scaffold-body
主体部分,填写容器就行了,这里用了Center
import 'package:flutter/material.dart';
// demo2-MaterialApp and Scaffold
void main(){
runApp(MaterialApp(
home: Scaffold(
body: const Center(
child: Text(
"Hello world!",
textDirection: TextDirection.ltr,
style: TextStyle(
color: Colors.greenAccent,
fontSize: 30.0
),
),
),
)
));
}
Scaffold-drawer
-
编写一个简单的界面
import 'package:flutter/material.dart'; //demo4-Drawer void main(){ runApp(MaterialApp( home: Scaffold( appBar: AppBar( title: const Text( "demo4-Drawer", textDirection: TextDirection.ltr ), backgroundColor: Colors.blue, titleTextStyle:const TextStyle( color: Colors.amber ), ), drawer: ), )); }
-
darwer需要填写与之相应的Darwer()
里面常用的两个属性
- backgroundColor - 抽屉菜单的背景颜色
- child - 抽屉菜单的内容
import 'package:flutter/material.dart'; //demo4-Drawer void main(){ runApp(MaterialApp( home: Scaffold( appBar: AppBar( title: const Text( "demo4-Drawer", textDirection: TextDirection.ltr ), backgroundColor: Colors.blue, titleTextStyle:const TextStyle( color: Colors.amber ), ), drawer:const Drawer( backgroundColor: Colors.lightBlue, child:Text( "DrawerTitle", textDirection: TextDirection.ltr, style: TextStyle( color: Colors.greenAccent, fontSize: 30.0, ), ), ), ), )); }
-
总体代码
import 'package:flutter/material.dart'; //demo4-Drawer void main(){ runApp(MaterialApp( home: Scaffold( appBar: AppBar( title: const Text( "demo4-Drawer", textDirection: TextDirection.ltr ), backgroundColor: Colors.blue, titleTextStyle:const TextStyle( color: Colors.amber ), ), body: HomeComp(), drawer:const Drawer( backgroundColor: Colors.lightBlue, child:Text( "DrawerTitle", textDirection: TextDirection.ltr, style: TextStyle( color: Colors.greenAccent, fontSize: 30.0, ), ), ), ), )); } class HomeComp extends StatelessWidget{ // const HomeComp ({Key ? key}) : super(key: key); @override Widget build(BuildContext context) { // TODO: implement build return const Center( child: Text( "Hello world", textDirection: TextDirection.ltr, style: TextStyle( color: Colors.green, ), ), ); } }
分离组件
我们在编写软件的时候,不会把所有的东西功能和界面全部写在一起,那样会让我们看起来很烦躁,因为非常的乱
-
先编写一个普通的界面,现在,代码是报错的,因为body还没有填写东西
import 'package:flutter/material.dart'; //demo3-分离组件 void main(){ runApp(MaterialApp( home: Scaffold( appBar: AppBar( title: const Text("demo3-分离组件"), backgroundColor: Colors.blue, titleTextStyle:const TextStyle( color: Colors.amber ), ), body: , ), )); }
-
在main方法下面,编写一个组件类,让这个类继承StatelessWidget,此时会报错,将鼠标放上去,让系统自动帮你把方法写出来
const HomeComp({super.key});
这一行一般都是这样的形式,照写就行class HomeComp extends StatelessWidget{ const HomeComp({super.key}); @override Widget build(BuildContext context) { } }
-
在继承的方法返回容器即可
class HomeComp extends StatelessWidget{ const HomeComp({super.key}); @override Widget build(BuildContext context) { // TODO: implement build return const Center( child: Text( "Hello world", style: TextStyle( color: Colors.green, ), ), ); } }
-
总体代码就是这样
import 'package:flutter/material.dart'; //demo3-分离组件 void main(){ runApp(MaterialApp( home: Scaffold( appBar: AppBar( title: const Text("demo3-分离组件"), backgroundColor: Colors.blue, titleTextStyle:const TextStyle( color: Colors.amber ), ), body: const HomeComp(), ), )); } class HomeComp extends StatelessWidget{ const HomeComp({super.key}); @override Widget build(BuildContext context) { // TODO: implement build return const Center( child: Text( "Hello world", style: TextStyle( color: Colors.green, ), ), ); } }
基础组件
Container容器组件
alignment的参数
参数 | 说明 |
---|---|
Alignment.topCenter |
顶部居中对齐 |
Alignment.topLeft |
顶部左对齐 |
Alignment.topRight |
顶部右对齐 |
Alignment.topRight |
顶部右对齐 |
Alignment.center |
水平垂直居中对齐 |
Alignment.centerLeft |
垂直居中水平居左对齐 |
Alignment.centerRight |
垂直居中水平居右对齐 |
Alignment.bottomCenter |
底部居中对齐 |
Alignment.bottomLeft |
底部居左对齐 |
Alignment.bottomRight |
底部居右对齐 |
decoration的属性
先使用BoxDecoration(),里面的参数为
属性 | 说明 |
---|---|
gradient |
渐变 LinearGradient()为背景线性渐变 RadialGradient()为径向渐变,这两个方法里面需要color参数,可以书数组 |
boxShadow |
阴影 需要定义一个常量数组,数组里面需要BoxShadow(),里面有color[颜色]、offset[位移,参数为Offset(x, y)]、blurRadius[虚化程度] |
border |
边框 需要使用Border.all(),里面有color[颜色]、width[线条大小] |
borderRadius |
盒子圆角 需要使用BorderRadius.circular(),参数就是double类型的数字 |
其他属性
属性 | 说明 |
---|---|
margin |
容器与外部的距离,参数使用EdgeInsets.all(double) 或者EdgeInsets.fromLTRB(double,double,double,double) |
padding |
容器边框与child的距离,同上 |
transform |
让容器可以位移旋转 |
`height | width` |
child |
子元素 |
Text组件
textAlign参数
文字对齐方式
参数 | 说明 |
---|---|
TextAlign.center |
文字居中 |
TextAlign.left |
文字左对齐 |
TextAlign.right |
文字右对齐 |
TextAlign.justfy |
文字两端对齐 |
textDirection参数
文字方向
参数 | 说明 |
---|---|
TextDirection.ltr |
从左至右 |
TextDirection.rtl |
从右至左 |
overflow参数
文字超出屏幕之后的处理方式
参数 | 说明 |
---|---|
TextOverflow.visible |
强制显示 |
TextOverflow.clip |
裁剪隐藏 |
TextOverflow.fade |
渐隐 |
TextOverflow.ellipsis |
省略号 |
textScaler参数
textScaleFactor已弃用,用textScaler代替,用于字体显示倍率
参数 | 说明 |
---|---|
TextScaler.linear(double) |
字体显示倍率,1为正常 |
maxLines参数
显示最大行数
- 填写整数即可
style属性
需要搭配TextStyle()使用,以下是TextStyle()的属性
-
decoration:文字的装饰线
参数 说明 TextDecoration.none
无线条 TextDecoration.lineThrough
删除线 TextDecoration.overline
上划线 TextDecoration.underline
下划线 -
decorationColor:装饰线的颜色
-
decorationStyle:装饰线样式
参数 说明 TextDecorationStyle.dashed
横杆虚线 TextDecorationStyle.dotted
点虚线 TextDecorationStyle.double
双线 TextDecorationStyle.solid
实线 TextDecorationStyle.wavy
波浪线 -
wordSpacing:单词间隙,负值会让单词变得更紧凑
-
letterSpacing:字母间隙,负值,会让字母变得更紧凑
-
fontStyle:文字样式
参数 说明 FontStyle.normal
正常字体 FontStyle.italic
斜体 -
fontSize:文字大小
-
color:文字颜色
-
fontWeight:文字粗细
参数 说明 FontWeight.normal
正常粗细 FontWeight.normal
粗体 FontWeight.wx00
按照整百粗细(100-900)
图片组件
图片组件分为两种
- Image.asset
- Image.network
属性
两种的参数都是一样的,就是引用图片的方式不一样
alignment:图片对齐方式
参数 | 说明 |
---|---|
Alignment.topCenter |
顶部居中对齐 |
Alignment.topLeft |
顶部左对齐 |
Alignment.topRight |
顶部右对齐 |
Alignment.center |
水平垂直居中对齐 |
Alignment.centerLeft |
垂直居中水平居左对齐 |
Alignment.centerRight |
垂直居中水平居右对齐 |
Alignment.bottomCenter |
底部居中对齐 |
Alignment.bottomLeft |
底部居左对齐 |
Alignment.bottomRight |
底部居右对齐 |
fit:图片的填充
参数 | 说明 |
---|---|
BoxFit.fill |
拉伸填满 |
BoxFit.contain |
按原始比例,可能会有空隙 |
BoxFit.cover |
图片填满整个容器,不变形,但是会被剪裁 |
BoxFit.fitWidth |
宽度充满 |
BoxFit.fitHeight |
高度充满 |
BoxFit.scaleDown |
效果和contain差不多,但是不允许显示超过原图片的大小,可小不可大 |
color:图片背景颜色
colorBlendMode:图片混合颜色选项
-
参数比较多,都是比较基本的图片颜色混合
repeat:图片平铺
ImageRepeat.noRepeat
:不重复ImageRepeat.repeat
:横向和总线都重复ImageRepeat.repeatX
:横向重复,纵向不重复ImageRepeat.repeatY
:纵向重复,横向不重复
width 和 height:图片宽高
Image.asset
-
在目录新建储存图片的文件夹
-
在pubspec.yaml文件中声明图片文件[注意格式和空格],在这个地方添加⬇
flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true assets: - "img/head.jpg" #在这个地方添加,认准这个地方,下面这里的这四行注释下面也行 # To add assets to your application, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg
-
在child中使用Image.asset()
// 这只是个组件 class Demo6 extends StatelessWidget{ const Demo6({super.key}); @override Widget build(BuildContext context) { // TODO: implement build return Center( child: Image.asset( "img/head.jpg", ) ); } }
Image.network
这个也同理,就是不用配置什么东西
// 这只是个组件
class Demo6 extends StatelessWidget{
const Demo6({super.key});
@override
Widget build(BuildContext context) {
// TODO: implement build
return Center(
child: Image.network(
"https://www.baidu.com/img/flexible/logo/pc/result.png",
)
);
}
}
图标组件
使用Icon()来使用官方给的图标
class Demo7 extends StatelessWidget{
const Demo7({super.key});
@override
Widget build(BuildContext context) {
// TODO: implement build
return const Center(
child: Column(
children: [
Icon(
Icons.add,
),
Icon(
Icons.add_box_rounded,
color: Colors.lightBlueAccent,
),
Icon(
Icons.accessibility_rounded,
color: Colors.green,
size: 30,
)
],
),
);
}}
属性
属性 | 说明 |
---|---|
Icons.图标名 | 使用官方的图标 |
color | 图标颜色 |
size | 图标大小,默认20 |
列表组件
通过使用组件ListView()
创建列表
属性
属性 | 说明 |
---|---|
scrollDirection |
设置列表类型,参数为 Axis.vertical 垂直列表[默认]、Axis.horizontal 水平列表 |
padding |
内边距,与其他组件一样 |
resolve |
是否反向排序,参数:true 和false |
children |
列表的元素,参数为数组 |
最简单的列表
通过使用ListTile()
创建列表项,利用ListTile里的参数title可以将其他元素放进去列表项
// 垂直列表-title
class List1 extends StatelessWidget{
const List1({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: const <Widget>[
ListTile(
title: Text("标题1"),
),
ListTile(
title: Text("标题1"),
),
ListTile(
title: Text("标题1"),
),
ListTile(
title: Text("标题1"),
),
ListTile(
title: Text("标题1"),
),
],
);
}
}
列表项
即ListTile()
,有必要说一下里面的参数
属性
属性 | 说明 |
---|---|
leading |
列表项的最左边[前导],一般放图标或者图片 |
trailing |
列表项的最右边[尾部],一般放图标或者图片 |
title |
列表项的标题 |
subtitle |
列表项的副标题 |
style |
列表项的文本样式 |
iconColor |
图标颜色 |
tileColor |
列表项背景颜色 |
titleTextStyle |
标题文本颜色 |
subtitleTextStyle |
副标题文本颜色 |
leadingAndTrailingTextStyle |
前导和尾部的文本样式 |
contentPadding |
内容的内边距 |
titleAlignment |
标题文本的对齐方式 |
isThreeLine |
额外文本行[三行列表项] |
布局
Grid布局
常用属性
属性 | 说明 |
---|---|
scrollDirection |
组件滚动方向,Axis.horizontal 水平,Axis.vertical 垂直 |
reverse |
组件元素是否反向排序 |
controller |
滚动监听 |
primary |
内容不足时是否可以滑动 |
shrinkWrap |
内容适配,默认false |
padding |
内边距 |
crossAxisCount |
一行或一列的元素数量 |
mainAxisSpacing |
主轴之间间距 |
crossAxisSpacing |
横轴之间间距 |
childAspectRatio |
元素的宽高比例 |
children |
组件元素 |
使用GridView.count创建网络布局
使用这个方法创建网络布局必须要填写一个值crossAxisCount,也就是一行限制多少个元素
class GridTest1 extends StatelessWidget{
const GridTest1({super.key});
@override
Widget build(BuildContext context) {
return GridView.count(
scrollDirection: Axis.horizontal,
crossAxisCount: 3,
childAspectRatio: 1.0,
children: const <Widget>[
Icon(Icons.account_balance_wallet,color: Colors.green,),
Icon(Icons.balance,color: Colors.brown,),
Icon(Icons.camera,color: Colors.lightBlueAccent,),
Icon(Icons.data_object,color: Colors.orange,),
Icon(Icons.egg,color: Colors.cyan,),
Icon(Icons.facebook_outlined,color: Colors.greenAccent,),
Icon(Icons.gamepad,color: Colors.deepPurple,),
Icon(Icons.handshake_outlined,color: Colors.lightGreen,),
Icon(Icons.image_rounded,color: Colors.deepOrange,),
],
);
}
}
使用GridView.count创建网络布局
使用此方法创建网络布局必须要填写一个值maxCrossAxisExtent,也就是固定横轴的最大长度
class GridTest2 extends StatelessWidget{
const GridTest2({super.key});
@override
Widget build(BuildContext context) {
return GridView.extent(
maxCrossAxisExtent: 100,
childAspectRatio: 1.0,
children: const <Widget>[
Icon(Icons.account_balance_wallet,color: Colors.green,),
Icon(Icons.balance,color: Colors.brown,),
Icon(Icons.camera,color: Colors.lightBlueAccent,),
Icon(Icons.data_object,color: Colors.orange,),
Icon(Icons.egg,color: Colors.cyan,),
Icon(Icons.facebook_outlined,color: Colors.greenAccent,),
Icon(Icons.gamepad,color: Colors.deepPurple,),
Icon(Icons.handshake_outlined,color: Colors.lightGreen,),
Icon(Icons.image_rounded,color: Colors.deepOrange,),
],
);
}
}
线性布局
属性
属性 | 说明 | 参数 |
---|---|---|
mainAxisAlignment |
主轴的排序方式 | `MainAxisAlignment.center |
crossAxisAlignment |
次轴的排序方式 | `CrossAxisAlignment.center |
children |
组件元素 | 略 |
Row布局
class RowTest extends StatelessWidget{
const RowTest({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: double.infinity,
width: double.infinity,
child: const Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.account_balance_wallet,color: Colors.green,size: 50,),
Icon(Icons.balance,color: Colors.brown,size: 50,),
Icon(Icons.camera,color: Colors.lightBlueAccent,size: 50,),
Icon(Icons.data_object,color: Colors.orange,size: 50,),
Icon(Icons.egg,color: Colors.cyan,size: 50,),
Icon(Icons.facebook_outlined,color: Colors.greenAccent,size: 50,),
Icon(Icons.gamepad,color: Colors.deepPurple,size: 50,),
Icon(Icons.handshake_outlined,color: Colors.lightGreen,size: 50,),
Icon(Icons.image_rounded,color: Colors.deepOrange,size: 50,),
],
),
);
}
}
Colum布局
class ColumnTest extends StatelessWidget{
const ColumnTest({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: double.infinity,
child:const Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(Icons.account_balance_wallet,color: Colors.green,size: 50,),
Icon(Icons.balance,color: Colors.brown,size: 50,),
Icon(Icons.camera,color: Colors.lightBlueAccent,size: 50,),
Icon(Icons.data_object,color: Colors.orange,size: 50,),
Icon(Icons.egg,color: Colors.cyan,size: 50,),
Icon(Icons.facebook_outlined,color: Colors.greenAccent,size: 50,),
Icon(Icons.gamepad,color: Colors.deepPurple,size: 50,),
Icon(Icons.handshake_outlined,color: Colors.lightGreen,size: 50,),
Icon(Icons.image_rounded,color: Colors.deepOrange,size: 50,),
],
),
);
}
}
Flex布局
使用flex布局一般需要搭配Expanded组件一起使用
Flex属性和Expanded属性
Flex属性 | 说明 | Expanded属性 | 说明 |
---|---|---|---|
direction |
flex布局方向,Axis.horizontal 水平,Axis.vertical 垂直 |
flex |
该组件占总flex的多少,总flex=处于该组件内的Expanded组件的flex值之和 |
children |
flex的元素 | child |
Expanded的元素 |
水平布局
class FlexHor extends StatelessWidget{
const FlexHor({super.key});
@override
Widget build(BuildContext context) {
return Flex(
direction: Axis.horizontal,
children: [
Expanded(flex: 1,child: Container(color: Colors.brown,child: Icon(Icons.egg,color: Colors.teal,size: 50,),)),
Expanded(flex: 2,child: Container(color: Colors.orange,child: Icon(Icons.facebook_outlined,color: Colors.teal,size: 50,),)),
Expanded(flex: 3,child: Container(color: Colors.cyan,child: Icon(Icons.data_object,color: Colors.teal,size: 50,),)),
],
);
}
}
垂直布局
class FlexVer extends StatelessWidget{
const FlexVer({super.key});
@override
Widget build(BuildContext context) {
return Flex(
direction: Axis.vertical,
children: [
Expanded(flex: 1,child: Container(color: Colors.brown,child: Icon(Icons.egg,color: Colors.teal,size: 50,),)),
Expanded(flex: 2,child: Container(color: Colors.orange,child: Icon(Icons.facebook_outlined,color: Colors.teal,size: 50,),)),
Expanded(flex: 3,child: Container(color: Colors.cyan,child: Icon(Icons.data_object,color: Colors.teal,size: 50,),)),
],
);
}
}
水平与垂直布局的混合使用
class RowAndColumn extends StatelessWidget{
const RowAndColumn({super.key});
@override
Widget build(BuildContext context) {
return Container(
height: 300,
margin:const EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 1,
child: Container(color: Colors.teal,),
),
Container(height: 10,),//只是组件之间的分割线
Expanded(
flex: 1,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(flex: 2,child: Container(color: Colors.deepPurple,),),
Container(width: 10,),//只是组件之间的分割线
Expanded(flex: 1,child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(flex: 1,child: Container(color: Colors.tealAccent,)),
Container(height: 10,),//只是组件之间的分割线
Expanded(flex: 1,child: Container(color: Colors.lightBlue,))
],
))
],
),
),
],
),
);
}
}
Wrap布局
属性
属性 | 说明 |
---|---|
direction |
组件主轴的方向,参数为Axis.horizontal 水平,Axis.vertical 垂直 |
spacing |
主轴元素之间的间距 |
runSpacing |
新行元素之间的间距 |
alignment |
主轴元素的对齐方式 |
runAlignment |
新行元素的对齐方式 |
verticalDirection |
定义children 摆放顺序,默认是VerticalDirection.down ,参数还有VerticalDirection.up |
children |
组件元素 |
class WrapTest extends StatelessWidget{
const WrapTest({super.key});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.topCenter,
child: Container(
width: 300,
alignment: Alignment.topCenter,
child: Wrap(
spacing: 5,
runSpacing: 5,
direction: Axis.horizontal,
alignment: WrapAlignment.start,
runAlignment: WrapAlignment.center,
verticalDirection: VerticalDirection.down,
children: [
TextButton(onPressed: (){}, child: const Text("Btn1")),
TextButton(onPressed: (){}, child: const Text("Btn22")),
TextButton(onPressed: (){}, child: const Text("Btn333")),
TextButton(onPressed: (){}, child: const Text("Btn4444")),
TextButton(onPressed: (){}, child: const Text("Btn55555")),
TextButton(onPressed: (){}, child: const Text("Btn666666")),
TextButton(onPressed: (){}, child: const Text("Btn7777777")),
TextButton(onPressed: (){}, child: const Text("Btn88888888")),
TextButton(onPressed: (){}, child: const Text("Btn999999999")),
TextButton(onPressed: (){}, child: const Text("Btn1000000000")),
TextButton(onPressed: (){}, child: const Text("Btn11")),
],
),
),
);
}
}
Stack布局
一般来说,会使用其他组件来与Stack布局一起使用
属性
属性 | 说明 |
---|---|
alignment |
对齐方式,参数Alignment.* |
textDirection |
文本方向,参数TextDirection.* |
children |
子组件 |
使用stack布局,会让它的子组件都堆叠在一起,如下
class StackTest extends StatelessWidget{
const StackTest({super.key});
@override
Widget build(BuildContext context) {
return Container(
child: const Stack(
alignment: Alignment.topCenter,
children: [
Text("test1"),
Text("test2"),
Text("test3"),
Text("test4"),
Text("test5"),
Text("test6"),
Text("test7"),
],
),
);
}
}
结合Align使用
Align组件一般来说,只用通过alignment属性来控制组件的位置
Align属性
属性 | 说明 |
---|---|
alignment |
对齐方式,参数Alignment.* |
child |
子组件 |
如果位置不够,Stack布局里的组件依然会堆叠
class StackAndAlign extends StatelessWidget{
const StackAndAlign({super.key});
@override
Widget build(BuildContext context) {
// TODO: implement build
return Container(
child: const Stack(
textDirection: TextDirection.ltr,
alignment: Alignment.topCenter,
children: [
Align(
alignment: Alignment.topLeft,
child: Text("test1"),
),
Align(
alignment: Alignment.topCenter,
child: Text("test2"),
),
Align(
alignment: Alignment.topRight,
child: Text("test3"),
),
Align(
alignment: Alignment.centerLeft,
child: Text("test4"),
),
Align(
alignment: Alignment.center,
child: Text("test5"),
),
Align(
alignment: Alignment.centerRight,
child: Text("test6"),
),
Align(
alignment: Alignment.bottomLeft,
child: Text("test7"),
),
Align(
alignment: Alignment.bottomCenter,
child: Text("test8"),
),
Align(
alignment: Alignment.bottomRight,
child: Text("test9"),
),
],
),
);
}
}
创建一个底部导航栏
通过使用Align的对齐方式定位,将导航栏固定在底部
class TabBarSelf extends StatelessWidget{
const TabBarSelf({super.key});
List<Widget> getLists(){
List<Widget> lists = [];
for(int i = 0;i<100;i++){
lists.add(
ListTile(
leading: Text("[${i+1}]"),
title: Text("标题${i+1}"),
subtitle: Text("数据${i+1}"),
trailing: Text("|${i+1}|"),
tileColor: Color.fromARGB(Random().nextInt(255), Random().nextInt(255), Random().nextInt(255), 1),
)
);
}
return lists;
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
ListView(
children: getLists(),
),
Align(
alignment: Alignment.bottomRight,
child: Container(
width: double.infinity,
height: 40,
color: Colors.lightBlue,
child: const Center(
child: Text("我是个导航栏"),
),
),
),
],
);
}
}
结合Positioned使用
使用Positioned组件,可以让组件位于哪个地方,就位于哪个地方,相当于绝对定位
Positioned属性
属性 | 说明 |
---|---|
`left | top |
`width | height` |
child |
子组件 |
class StackAndPositioned extends StatelessWidget{
const StackAndPositioned({super.key});
@override
Widget build(BuildContext context) {
return Container(
child: const Stack(
alignment: Alignment.topCenter,
children: [
Positioned(
child: Text("top0,left0"),
top: 0,
left: 0,
),
Positioned(
child: Text("top100,left0"),
top: 100,
left: 0,
),
Positioned(
child: Text("top0,left100"),
top: 0,
left: 100,
),
Positioned(
child: Text("top100,left100"),
top: 100,
left: 100,
),
Positioned(
child: Text("top0,right0"),
top: 0,
right: 0,
),
Positioned(
child: Text("bottom0,left0"),
bottom: 0,
left: 0,
),
Positioned(
child: Text("bottom0,right0"),
bottom: 0,
right: 0,
),
],
),
);
}
}
创建一个悬浮按钮
可以使用Positioned组件的绝对定位的特点,让按钮随时固定在窗口的一个位置
class SpecialBtn extends StatelessWidget{
const SpecialBtn({super.key});
List<Widget> getLists(){
List<Widget> lists = [];
for(int i = 0;i<100;i++){
lists.add(
ListTile(
leading: Text("[${i+1}]"),
title: Text("标题${i+1}"),
subtitle: Text("数据${i+1}"),
trailing: Text("|${i+1}|"),
tileColor: Color.fromARGB(Random().nextInt(255), Random().nextInt(255), Random().nextInt(255), 1),
)
);
}
return lists;
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
ListView(
children: getLists(),
),
Positioned(
right: 10,
bottom: 10,
child: Container(
width: 40,
height: 40,
alignment: Alignment.center,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateColor.resolveWith((states) => Colors.limeAccent),
padding: MaterialStateProperty.all(const EdgeInsets.all(0)),
),
onPressed: () {
print("你点击了按钮");
},
child: const Text("Btn"),
),
)
),
],
);
}
}
AspectRatio布局
此布局的理解比较的复杂,布局的宽高由aspectRatio
参数去决定,即宽高比,它会在布局限制条件允许的范围内尽可能的扩展,按照固定比率去尽量占满区域
属性
属性 | 说明 |
---|---|
aspectRatio |
宽高比,可能会忽略这个数值 |
child |
子组件 |
class AspectRatioTest extends StatelessWidget{
const AspectRatioTest({super.key});
@override
Widget build(BuildContext context) {
return AspectRatio(
aspectRatio: 3/2,
child: Container(
color: Colors.limeAccent,
),
);
}
按钮
flutter内置了一些比较好的按钮,虽然样式设置起来有些许麻烦
属性
属性 | 说明 |
---|---|
onPressed |
点击事件 [必填] |
style |
按钮样式,需要使用ButtonStyle来调整样式 |
child |
子组件 [必填] |
Flutter内置的按钮
按钮 | 说明 |
---|---|
ElevatedButton |
最普通的按钮,有背景颜色,有少许阴影,点击有颜色变化,无边框 |
TextButton |
文本按钮,无背景颜色,无阴影,点击有颜色变化,无边框 |
OutlinedButton |
边框按钮,无背景颜色,无阴影,点击有颜色变化,有边框 |
IconButton |
图标按钮,无背景颜色,无阴影,点击有颜色变化,无边框 |
ElevatedButton.icon |
带图标的普通按钮,样式如上,icon 必填 |
TextButton.icon |
带图标的文本按钮,样式如上,icon 必填 |
OutlinedButton.icon |
带图标的边框按钮,样式如上,icon 必填 |
class BtnTest extends StatelessWidget{
const BtnTest({super.key});
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 4,
children: [
ElevatedButton(onPressed: (){}, child: const Text("普通按钮")),
TextButton(onPressed: (){}, child: const Text("文本按钮")),
OutlinedButton(onPressed: (){}, child: const Text("边框按钮")),
IconButton(onPressed: (){}, icon: const Icon(Icons.gamepad_outlined)),
ElevatedButton.icon(onPressed: (){}, icon: const Icon(Icons.tag_faces), label: const Text("普通按钮[有图标]")),
TextButton.icon(onPressed: (){}, icon: const Icon(Icons.tag_faces), label: const Text("文本按钮[有图标]")),
OutlinedButton.icon(onPressed: (){}, icon: const Icon(Icons.tag_faces), label: const Text("边框按钮[有图标]")),
],
);
}
}
修改按钮宽高
通过使用SizeBox来调整按钮,SizeBox中的width和height调整盒子的大小,进而调整按钮宽高
class BtnTest1 extends StatelessWidget{
const BtnTest1({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 150,
height: 50,
child: ElevatedButton(
onPressed: (){},
child: const Text("修改宽高"),
),
);
}
}
修改按钮样式
想要修改按钮的样式,需要通过ButtonStyle
去调整,而里面的参数比较的复杂
ButtonStyle的属性
属性 | 说明 | 参数 |
---|---|---|
backgroundColor |
背景颜色 | MaterialStateProperty.all(Colors.*) |
foregroundColor |
组件内容颜色 | MaterialStateProperty.all(Colors.*) |
shadowColor |
阴影颜色 | MaterialStateProperty.all(Colors.*) |
shape |
按钮形状 | MaterialStateProperty.all(*) 这个比较特殊 |
padding |
内边距 | MaterialStateProperty.all(const EdgeInsets.*) |
elevation |
阴影范围 | MaterialStateProperty.all(num) |
side |
边框 | MaterialStateProperty.all(const BorderSide(width: num,color: Colors.*)) |
alignment |
组件对齐方式 | Alignment.* |
设置按钮圆角
shape
这个属性的参数比较多变
若要设置圆角,参数为:MaterialStateProperty.all(RoundedRectangleBorder(borderRadius: BorderRadius.circular(num)))
class BtnTest2 extends StatelessWidget{
const BtnTest2({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 150,
height: 50,
child: ElevatedButton(
onPressed: (){},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.lightBlueAccent),
foregroundColor: MaterialStateProperty.all(Colors.purple),
elevation: MaterialStateProperty.all(10),
alignment:Alignment.center,
shadowColor:MaterialStateProperty.all(Colors.black12),
padding:MaterialStateProperty.all(const EdgeInsets.all(10)),
shape: MaterialStateProperty.all(RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)
)),
),
child: const Text("修改各种属性"),
),
);
}
}
若要设置圆形,参数为:MaterialStateProperty.*all*(const CircleBorder(side: BorderSide(color: Color.fromARGB(0, 0, 0, 0))))
class BtnTest3 extends StatelessWidget{
const BtnTest3({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 120,
height: 120,
child: ElevatedButton(
onPressed: (){},
style: ButtonStyle(
shape: MaterialStateProperty.all(
const CircleBorder(side: BorderSide(color: Color.fromARGB(0, 0, 0, 0))
)),
),
child: const Text("圆形按钮"),
),
);
}
}
设置按钮边框
class BtnTest4 extends StatelessWidget{
const BtnTest4({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
width: 120,
height: 120,
child: OutlinedButton(
onPressed: (){},
style: ButtonStyle(
side: MaterialStateProperty.all(
const BorderSide(width: 1,color: Colors.brown)
),
),
child: const Text("修改边框"),
),
);
}
}
事件
PointerEvents
最简单的事件监听,通过Listener()去监听子组件的行为 tips:以下的e
和event
都是event
,只是个变量而已
属性
属性 | 说明 |
---|---|
onPointerDown |
按下组件触发,参数:(e)=>{} |
onPointerMove |
按下并在组件内移动触发,参数:(e)=>{} |
onPointerHover |
移动到组件触发,参数:(e)=>{} |
onPointerUp |
按下后松开才触发,参数:(e)=>{} |
class Test1 extends StatelessWidget{
const Test1({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: [
ListTile(
title: Listener(
onPointerDown: (event) => print("按下组件触发:$event"),
child: const Text("按下组件触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title: Listener(
onPointerMove: (event) => print("按下并在组件内移动触发:$event"),
child: const Text("按下并在组件内移动触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title: Listener(
onPointerHover: (event) => print("移动到组件触发:$event"),
child: const Text("移动到组件触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title: Listener(
onPointerUp: (event) => print("按下后松开才触发:$event"),
child: const Text("按下后松开才触发"),
),
),
],
);
}
}
GestureDetector
GestureDetector封装了非常多的触发类型,所以直接使用GestureDetector()组件可以直接使用这些监听类型
Tap单击 属性
属性 | 说明 |
---|---|
onTapDown |
按下触发,参数(e)=>{} |
onTapUp |
抬起触发,参数(e)=>{} |
onTap |
按下后抬起[与抬起触发不同,这个两个条件都得有],参数()=>{} |
onTapCancel |
按下后取消[可移出组件触发],参数()=>{} |
class Test2 extends StatelessWidget{
const Test2({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: [
ListTile(
title:GestureDetector(
onTapDown: (e) => print("按下触发:$e"),
child: const Text("按下触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title:GestureDetector(
onTapUp: (e) => print("抬起触发:$e"),
child: const Text("抬起触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title:GestureDetector(
onTap: () => print("按下后抬起[与抬起触发不同,这个两个条件都得有]:======="),
child: const Text("按下后抬起[与抬起触发不同,这个两个条件都得有]"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title:GestureDetector(
onTapCancel: () => print("按下后取消[可移出组件触发]:======="),
child: const Text("按下后取消[可移出组件触发]"),
),
),
],
);
}
}
DoubleTap双击 属性
属性 | 说明 |
---|---|
onDoubleTap |
双击触发[必须按下抬起各两次],参数()=>{} |
onDoubleTapCancel |
双击时取消[可移出组件触发],参数()=>{} |
onDoubleTapDown |
双击时第二次点击触发,参数(e)=>{} |
class Test3 extends StatelessWidget{
const Test3({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: [
ListTile(
title:GestureDetector(
onDoubleTap: () => print("双击触发:======="),
child: const Text("双击触发[必须按下抬起各两次]"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title:GestureDetector(
onDoubleTapCancel: () => print("双击时取消[可移出组件触发]:======="),
child: const Text("双击时取消[可移出组件触发]"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title:GestureDetector(
onDoubleTapDown: (e) => print("双击时第二次点击触发:$e"),
child: const Text("双击时第二次点击触发"),
),
),
],
);
}
}
LongPress长按 属性
属性 | 说明 |
---|---|
onLongPress |
长按触发,参数()=>{} |
onLongPressStart |
长按开始时[长按触发时]触发,参数(e)=>{} |
onLongPressEnd |
长按结束时[长按抬起]触发,参数(e)=>{} |
onLongPressDown |
长按按下时[单点也可]触发,参数(e)=>{} |
onLongPressUp |
长按抬起时触发,参数()=>{} |
onLongPressMoveUpdate |
长按后移动更新时触发,参数(e)=>{} |
onLongPressCancel |
长按取消时[也可移出组件]触发,参数()=>{} |
class Test4 extends StatelessWidget{
const Test4({super.key});
@override
Widget build(BuildContext context) {
return ListView(
children: [
ListTile(
title: GestureDetector(
onLongPress: ()=>print("长按触发:====="),
child: const Text("长按触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title: GestureDetector(
onLongPressStart: (e)=>print("长按开始时[长按触发时]触发:$e"),
child: const Text("长按开始时触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title: GestureDetector(
onLongPressEnd: (e)=>print("长按结束时[长按抬起]触发:$e"),
child: const Text("长按结束时触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title: GestureDetector(
onLongPressDown: (e)=>print("长按按下时[单点也可]触发:$e"),
child: const Text("长按按下时触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title: GestureDetector(
onLongPressUp: ()=>print("长按抬起时触发:===="),
child: const Text("长按抬起时触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title: GestureDetector(
onLongPressMoveUpdate: (e)=>print("长按后移动更新时触发:$e"),
child: const Text("长按后移动更新时触发"),
),
),
Container(height: 1,color: Colors.black87,),
ListTile(
title: GestureDetector(
onLongPressCancel: ()=>print("长按取消时触发:===="),
child: const Text("长按取消时触发"),
),
),
],
);
}
}
Drag拖动 属性
属性 | 说明 |
---|---|
onVerticalDragStart |
开始垂直移动,参数(e)=>{} |
onVerticalDragUpdate |
持续垂直移动,参数(e)=>{} |
onVerticalDragEnd |
结束垂直移动,参数(e)=>{} |
onHorizontalDragStart |
开始水平移动,参数(e)=>{} |
onHorizontalDragUpdate |
持续水平移动,参数(e)=>{} |
onHorizontalDragEnd |
结束水平移动,参数(e)=>{} |
class Test5 extends StatelessWidget{
const Test5({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: GestureDetector(
onVerticalDragStart: (e)=>print("开始垂直移动,$e"),
onVerticalDragUpdate: (e)=>print("持续垂直移动,$e"),
onVerticalDragEnd: (e)=>print("结束垂直移动,$e"),
onHorizontalDragStart: (e)=>print("开始水平移动,$e"),
onHorizontalDragUpdate: (e)=>print("持续水平移动,$e"),
onHorizontalDragEnd: (e)=>print("结束水平移动,$e"),
child: Container(
width: 150,
height: 50,
color: Colors.green,
child: const Center(
child: Text("按住我移动一下"),
),
)
),
);
}
}
事件隔离
因为通过Listener()
可以拿到子组件Widget的信息,使用AbsorbPointer()
组件和IgnorePointer()
组件可以使得这些信息不允许打印出来,而两者的用法一样,只是属性不一样
组件 | 属性 | 说明 |
---|---|---|
AbsorbPointer() |
absorbing |
true :不会打印,false :会打印 |
IgnorePointer() |
ignoring |
true :不会打印,false :会打印 |
class Test6 extends StatelessWidget{
const Test6({super.key});
@override
Widget build(BuildContext context) {
return Flex(
direction: Axis.horizontal,
children: [
Container(width: 5),
Expanded(
flex: 1,
child: IgnorePointer(
ignoring: true,
child: Listener(
onPointerDown: (e)=>print("点了第一个ignoring: true,$e"),
child: Container(
height: 50,
color: Colors.green,
child: const Text("ignoring: true"),
),
),
)),
Container(width: 5),
Expanded(
flex: 1,
child: AbsorbPointer(
absorbing: false,
child: Listener(
onPointerDown: (e)=>print("点了第二个,absorbing: false,$e"),
child: Container(
height: 50,
color: Colors.lightBlueAccent,
child: const Text("absorbing: false"),
),
),
)),
Container(width: 5),
],
);
}
}
输入框
使用TextField()可以创建出一个输入框,Flutter为输入框提供了非常多的属性,供开发人员自定义输入框
属性
属性 | 说明 | 参数 |
---|---|---|
controller |
控制器,文本的交互需要它 | TextEditingController() |
decoration |
输入框的装饰选项 | InputDecoration() |
textInputAction |
键盘左下角的按钮类型 | TextInputAction.* |
textCapitalization |
键盘默认大小写 | TextCapitalization.* |
autofocus |
是否自动聚焦到输入框 | bool ,默认false |
obscureText |
是否将文本变成点,即隐藏文本 | bool ,默认false |
autocorrect |
是否自动更正 | bool ,默认true |
maxLength |
最大输入字符数 | int |
cursorWidth |
光标宽度 | int ,默认2.0 |
cursorColor |
光标颜色 | color |
cursorRadius |
光标圆角 | Radius.circular(*) |
cursorHeight |
光标高度 | int |
cursorOpacityAnimates |
光标是否开启闪烁动画 | bool |
inputFormatters |
限制输入文字/数字 | [] |
onTap |
点击输入框事件 | (){事件} |
onTapOutside |
点击输入框外事件 | (){事件} |
onChanged |
输入框变更事件 | (e){事件} |
onEditingComplete |
输入框完成输入事件 | (){事件} |
onSubmitted |
输入框提交事件 | (){事件} |
enabled |
是否启用输入框 | bool |
Controller
一般来说,使用控制器直接使用该对象就好了
TextEditingController controller = TextEditingController();
方法 | 用途 |
---|---|
clear() |
清除使用该控制器的输入框内容 |
text() |
设置使用该控制器的输入框内容 |
但是,使用TextEditingController.fromValue(*)
可以自定义输入框的一些属性,*为TextPosition()
而TextPosition()
有以下两个属性
属性 | 说明 |
---|---|
text |
设置使用该控制器的输入框内容,参数为String |
selection |
设置输入框内边距,参数请继续看↓ |
selection的参数为TextSelection.fromPosition(*),而里面又需要TextPosition(),里面只有两个属性
属性 | 说明 |
---|---|
affinity |
一般填TextAffinity.downstream |
offset |
输入框的光标距离最左边的距离,参数为int |
decoration
可以让输入框自定义成想要的样式,参数为InputDecoration()
,以下为InputDecoration()
的属性
属性 | 说明 | 参数 |
---|---|---|
icon |
最左侧的图标,图标下无横线 | Icon() |
labelText |
悬浮文字 | String |
labelStyle |
悬浮文字样式 | TextStyle() |
helperText |
帮助文字 | String |
helperStyle |
帮助文字样式 | TextStyle() |
hintText |
提示文字 | String |
hintStyle |
提示文字样式 | TextStyle() |
errorText |
错误文字 | String |
errorStyle |
错误文字样式 | TextStyle() |
contentPadding |
内容内边距 | EdgeInsets.all(*) |
prefixIcon |
最左边的图标,包含在横线内 | Icon() |
prefix |
最左边自定义Widget | Widget |
prefixText |
最左边文字,只有当输入框聚焦才显示 | String |
prefixStyle |
最左边文字样式 | TextStyle() |
suffixIcon |
最右边的图标,包含在横线内 | Icon() |
suffix |
最右边自定义Widget | Widget |
suffixText |
最右边文字,只有当输入框聚焦才显示 | String |
suffixStyle |
最右边文字样式 | TextStyle() |
counter |
自定义计数器Widget | Widget |
counterText |
计数器文本 | String |
counterStyle |
计数器文本样式 | TextStyle() |
filled |
是否填充颜色 | bool ,默认false |
fillColor |
填充颜色 | Color |
border |
输入框整体边框,看下面教程 | InputBorder.none 或OutlineInputBorder() |
enabledBorder |
输入框可用时边框,看下面教程 | InputBorder.none 或不填OutlineInputBorder() |
disabledBorder |
输入框禁用时边框,看下面教程 | InputBorder.none 或不填OutlineInputBorder() |
errorBorder |
输入框错误时边框,看下面教程 | InputBorder.none 或不填OutlineInputBorder() |
focusedBorder |
输入框聚焦时边框,看下面教程 | InputBorder.none 或不填OutlineInputBorder() |
border参数详解
参数为InputBorder.none
时,输入框不会显示下横线
参数为OutlineInputBorder()
,需要通过属性borderRadius
设置圆角,borderRadius
的参数为BorderRadius.all(Radius.circular(*))
参数为UnderlineInputBorder()
,输入框只会在下面显示一条线
最后两个参数的属性borderSide
可以设置线条颜色,参数为BorderSide(color: Colors.*)
放demo
class AllInput extends StatelessWidget{
AllInput({super.key});
TextEditingController controller = TextEditingController();
void clearInput(){
controller.clear();
}
TextEditingController userInputController = TextEditingController();
void clearUserInput(){
userInputController.clear();
}
@override
Widget build(BuildContext context) {
controller = TextEditingController.fromValue(
TextEditingValue(
text: "初始化文本",
selection: TextSelection.fromPosition(
const TextPosition(
affinity: TextAffinity.downstream,
offset: 5
)
)
)
);
return ListView(
children: [
const ListTile(
title: TextField(),//最简单的输入框
),
ListTile(
title: TextField(
controller: controller,
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.characters,
autofocus: true,
obscureText: true,
autocorrect: true,
onTap: ()=>print("你点击了输入框"),
onChanged: (e){
print("你输入了${e.characters}/共${e.length}字");
},
cursorWidth: 10,
cursorColor: Colors.green,
cursorRadius: const Radius.circular(5),
cursorHeight: 50,
cursorOpacityAnimates: true,
inputFormatters: [],
),
),
ListTile(
title: TextField(
controller: controller,
enabled: false,
),
),
const ListTile(
title: TextField(
decoration: InputDecoration(
hintText: "灰色提示文字",
icon: Icon(Icons.account_box)
),
),
),
const ListTile(
title: TextField(
decoration: InputDecoration(
hintText: "灰色提示文字",
prefixIcon: Icon(Icons.facebook_outlined),
suffixIcon: Icon(Icons.clear),
),
),
),
const ListTile(
title: TextField(
decoration: InputDecoration(
hintText: "灰色提示文字",
prefix: Text("user"),
suffix: Text("后缀"),
),
),
),
const ListTile(
title: TextField(
decoration: InputDecoration(
hintText: "灰色提示文字",
label: Text("label"),
helperText: "帮助文字",
errorText: "错误提示",
contentPadding: EdgeInsets.all(10),
counterText: "计数器",
filled: true,
fillColor: Colors.green
),
),
),
ListTile(
title: TextField(
controller: userInputController,
textInputAction: TextInputAction.next,
decoration: const InputDecoration(
hintText: "请输入用户名",
labelText: "用户名",
prefixIcon: Icon(Icons.account_box),
filled: true,
fillColor: Colors.white70,
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(
color: Colors.lightBlue
),
borderRadius: BorderRadius.all(Radius.circular(10))
)
),
),
)
],
);
}
}
Dialog
简单的Dialog
通过AlertDialog()
,我们可以很快的创建出一个确认窗口
但是在使用AlertDialog()
时,需要使用showDialog()
,才能显示
-
新建一个方法,并携带
context
变量,定义一个result
变量和打印resultvoid alertDialog(context){ var result; print("返回值:$result"); }
-
使用
showDialog()
,以及将方法变为异步方法,这里来看一下showDialog的属性属性 说明 context
不理,就填 context
builder
返回的Dialog类型 barrierDismissible
是否点击蒙版关闭Dialog useSafeArea
是否预留安全区域 void alertDialog(context)async{ var result = await showDialog( barrierDismissible: true, useSafeArea: true, context: context, builder: builder ); print("返回值:$result"); }
-
让
builder
返回AlertDialog()
void alertDialog(context)async{ var result = await showDialog( barrierDismissible: true, useSafeArea: true, context: context, builder: (context)=>AlertDialog() ); print("返回值:$result"); }
-
编辑
AlertDialog()
的title,content,action
void alertDialog(context)async{ var result = await showDialog( barrierDismissible: true, useSafeArea: true, context: context, builder: (context)=>AlertDialog( title: const Text("标题"), content: const Text("内容"), actions: [ TextButton( onPressed: (){ print("确定"); Navigator.pop(context,"confirm"); }, child: const Text("确定") ), TextButton( onPressed: (){ print("取消"); Navigator.pop(context,"cancel"); }, child: const Text("取消") ), ], ) ); print("返回值:$result"); }
-
最后创建一个按钮使用这个方法
onPressed: (){ selectDialog(context); },
-
效果
多选项Dialog
使用showDialog()
可以创建出多选项Dialog
与普通的Dialog创建方法相同,只是里面的children
用了SimpleDialogOption()
,相当于按钮
void selectDialog(context)async{
var result = await showDialog(
barrierDismissible: false,
context: context,
builder: (context)=>SimpleDialog(
title: const Text("标题"),
children: [
SimpleDialogOption(
onPressed: (){
print("选项1");
Navigator.pop(context,"选项1");
},
child: const Text("选项1"),
),
const Divider(),
SimpleDialogOption(
onPressed: (){
print("选项2");
Navigator.pop(context,"选项2");
},
child: const Text("选项2"),
),
const Divider(),
SimpleDialogOption(
onPressed: (){
print("选项3");
Navigator.pop(context,"选项3");
},
child: const Text("选项3"),
),
],
)
);
print("返回值:$result");
}
自定义Dialog
通过继承Dialog这个类,可以创建出高自定义的Dialog
class CustomDialog extends Dialog{
String title;
TextStyle titleTextStyle;
String content;
TextStyle contentTextStyle;
Color barrierColor;
Color backgroundColor;
double height;
double width;
Function()? onFinish;
CustomDialog({
super.key,
required this.title,
required this.onFinish,
this.content = "",
this.barrierColor = Colors.black12,
this.backgroundColor = Colors.brown,
this.width = 300,
this.height = 300,
this.titleTextStyle = const TextStyle(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.white
),
this.contentTextStyle = const TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
color: Colors.white
),
});
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Stack(
children: [
Container(height: double.infinity,width: double.infinity,color: barrierColor,),
Center(
child: Container(
width: width,
height: height,
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20)
),
child: Stack(
children: [
Align(
alignment: Alignment.topCenter,
child: Text(
title,
style: titleTextStyle
),
),
Align(
alignment: Alignment.centerLeft,
child: Text(
content,
style: contentTextStyle,
),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
height: 50,
child: Column(
children: [
Container(height: 2,color: Colors.white,),
Container(
height: 48,
child: Flex(
direction: Axis.horizontal,
children: [
Expanded(
flex: 1,
child: TextButton(
onPressed: (){
Navigator.pop(context,"confirm");
onFinish;
},
child: const Text(
"确定",
style: TextStyle(
color: Colors.white
),
)
)
),
Container(width: 2,color: Colors.white,),
Expanded(
flex: 1,
child: TextButton(
onPressed: (){
Navigator.pop(context,"cancel");
},
child: const Text(
"取消",
style: TextStyle(
color: Colors.white
),
)
)
)
],
),
)
],
),
)
)
],
),
),
)
],
),
);
}
}
路由
在使用MaterialApp()
时,可以使用路由,来实现页面之间的切换,而这里面的路由实现方式是通过将页面加入栈与移出栈的思想
最简单的页面切换
通过使用Navigator.push
来将新页面加入栈
Navigator.push(context, MaterialPageRoute(
builder: (context){
return const NextPage();
}
));
传参数入页面
-
新建一个页面,和一个实体类,并在页面入口方法中添加属性
class Person{ String name; int age; Person(this.name,this.age); @override String toString() { return "{name:$name,age:$age}"; } } class AttrPage extends StatelessWidget{ final String text; final Person person; const AttrPage({ super.key, required this.text, required this.person }); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("AttrPage"), backgroundColor: Colors.cyanAccent, ), body: Center( child: Column( children: [ Text("text:$text"), Text("person:${person.toString()}") ], ), ), ); } }
-
在主页面的按钮添加点击事件,用
Navigator.push
将页面加入栈,并在返回页面的对象里写入属性的参数onPressed: (){ Navigator.push(context, MaterialPageRoute( builder: (context){ return AttrPage(text: "TestText", person: Person("jack",10)); } )); }
-
效果
退出页面返回值
通过Navigator.push
加入的页面,左上角都会有一个返回的按钮,但是这个按钮返回值为null
,所以需要Navigator.pop
在退出页面时,返回一个值
-
新建一个页面,并添加一个按钮,添加点击事件,使用
Navigator.pop
返回页面class BackAttrPage extends StatelessWidget{ const BackAttrPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("BackAttrPage"), backgroundColor: Colors.green, ), body: Center( child: ElevatedButton( onPressed: (){ Navigator.pop(context,"hello world"); }, child: const Text("返回"), ), ), ); } }
-
在主页添加按钮进入这个界面,但是这里的方法需要在
()
后加async
表示这个方法为异步方法,并在Navigator.push
前加await
表示等待这个方法运行完,在进行下一步,然后用一个变量给Navigator.push
赋值,为了接收页面的返回值onPressed: ()async{ var result = await Navigator.push(context, MaterialPageRoute( builder: (context){ return const BackAttrPage(); } )); print("返回值:$result"); }
-
点击默认的返回按钮和点击自己的返回按钮的效果
使用MaterialApp()的routes
MaterialApp()
有个routes属性,这个属性是为了方便添加页面,形式为"路由名":(context) => const 页面()
routes: {
"route1":(context) => const Router1Page(),
"route2":(context) => const Router2Page()
}
然后我们就可以通过Navigator.pushNamed(context,"路由名")
去进入页面
传参问题
因为通过routes命名路由进入页面的方式无法直接传输参数,但是Navigator.pushNamed里的arguments可以将参数传输到页面里
-
在按钮处添加事件,并在Navigator.pushNamed里添加arguments属性,将要传的参数写在
{}
里,以,
隔开onPressed: (){ Navigator.pushNamed( context, "route1", arguments: [ "TestText", 123 ] ); }
-
在返回页面的方法里,通过
ModalRoute.of(context)?.settings.arguments
获取传入的数据,如果只传一个参数,那就直接将变量转成String类型即可,但是如果是其他的形式,例如:我传的是一个List,我需要将变量转成字符串,然后再将它转换成List才能拿出来使用class Router1Page extends StatelessWidget{ const Router1Page({super.key}); @override Widget build(BuildContext context) { var args = ModalRoute.of(context)?.settings.arguments; List<String> argList = args.toString().substring(1,args.toString().length-1).split(","); return Scaffold( appBar: AppBar( title: const Text("Page1111"), backgroundColor: Colors.cyan, ), body: Center( child: Column( children: [ Text("text:${argList[0]}"), Text("num:${argList[1]}"), ], ), ), ); } }
-
效果
返回值[与普通的一样]
返回值的方法与普通进入页面的方法一样,没有问题
效果:
route的钩子函数
顾名思义就是访问routes时,执行的函数
函数 | 说明 |
---|---|
onGenerateRoute |
当页面没有Route命名但依旧使用Navigator.pushNamed 会执行[需要单独出来讲] |
onUnknownRoute |
当页面没有Route命名但依旧使用Navigator.pushNamed 会执行,防止访问未知页面用 |
navigatorObservers |
监控页面的入栈和出栈 |
onGenerateRoute与onUnknownRoute
onGenerateRoute
这个函数的使用方法是为了:
- 使用路由命名方式传参的问题
- 防止没有权限的用户访问页面
- 访问空页面[不适用]
在使用这个函数的时候,需要一个参数RouteSettings settings
,通过settings.name
可以获取当前使用Navigator.pushNamed
时,访问的路由名
onGenerateRoute: (RouteSettings settings){
var routeName = settings.name;
return null;
}
可以通过给访问特定的路由名加个权限,或者传参
//MaterialApp里的onGenerateRoute属性
onGenerateRoute: (RouteSettings settings){
var routeName = settings.name;
if(routeName == "noReg"){
print("如果没有注册在route里,但是依旧要Navigator.pushNamed访问,会显示这一行,route name:$routeName");
return MaterialPageRoute(builder: (context){
return const Route2Page(text: "testText",);
}
);
}
return null;
}
//页面
class Route2Page extends StatelessWidget{
const Route2Page({super.key,required this.text});
final String text;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("route2"),
backgroundColor: Colors.amberAccent,
),
body: Center(
child: Text("没在routes注册,也能通过Navigator.pushNamed访问,传入值:$text"),
),
);
}
}
onUnknownRoute
这个函数的执行时机在于当页面没有Route命名但依旧使用Navigator.pushNamed
且onGenerateRoute
没有返回页面
也就是当页面不存在的时候,这个函数会执行,且可以自定义不存在页面
//MaterialApp里的onUnknownRoute属性
onUnknownRoute: (RouteSettings settings){
print("访问了不存在的页面,");
return MaterialPageRoute(builder: (context){
return const NoExitPage();
});
}
//不存在页面提示页面
class NoExitPage extends StatelessWidget{
const NoExitPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("页面不存在"),
backgroundColor: Colors.amberAccent,
),
body: const Center(
child: Text("访问了不存在的页面!"),
),
);
}
}
navigatorObservers
这个钩子函数,会全程监控页面的入栈和出栈,只要栈堆有变化,就会执行
import 'package:flutter/material.dart';
// demo21 监控路由
void main(){
runApp(MaterialApp(
navigatorObservers: [DemoNO()],
home: const HomePage(),
routes: {
"test1": (context) => const Test1Page(),
"test2": (context) => const Test2Page(),
},
));
}
class DemoNO extends NavigatorObserver{
@override
void didPush(Route route, Route? previousRoute) {
var name = route.settings.name;
print("$name页面被加入栈");
super.didPush(route, previousRoute);
}
@override
void didPop(Route route, Route? previousRoute) {
var name = route.settings.name;
print("$name页面被移出栈");
super.didPop(route, previousRoute);
}
}
demo
Turing158/flutter-demo (github.com)
Turing_ICE/flutter-demo (gitee.com)
待更新 ...