Text
Text
用于显示简单样式文本,它包含一些控制文本显示样式的一些属性,一个简单的例子如下:
Text("Hello world",
textAlign: TextAlign.left,
);
Text("Hello world! I'm Jack. "*4,
maxLines: 1,
overflow: TextOverflow.ellipsis,
);
Text("Hello world",
textScaleFactor: 1.5,
);
下面是Text
的常用属性:
属性 | 功能 |
---|---|
textAlign | 文本对齐方式(center居中,left左对齐,right右对齐,justfy两端对齐) |
textDirection | 文本方向(ltr从左至右,rtl从右至左) |
overflow | 文字超出屏幕之后的处理方式(clip裁剪,fade渐隐,ellipsis省略号) |
textScaleFactor | 字体显示大小的缩放因子 |
maxLines | 文字显示最大行数 |
style | 字体的样式设置 |
注意, textAlign
对齐的参考系是Text
widget 本身。如果 Text
文本内容宽度不足一行,Text
的宽度和文本内容长度相等,那么这时指定对齐方式是没有意义的,只有 Text
宽度大于文本内容长度时指定此属性才有意义。下面我们指定一个较长的字符串:
Text("Hello world "*6, //字符串重复六次
textAlign: TextAlign.center,
);
运行效果:
字符串内容超过一行,Text
宽度等于屏幕宽度,第二行文本便会居中显示。
TextStyle
TextStyle
用于指定文本显示的样式如颜色、字体、粗细、背景等。下面是TextStyle
的常用属性:
属性 | 功能 |
---|---|
decoration | 文字装饰线(none没有线,lineThrough删除线,overline上划线, underline 下划线) |
decorationColor | 文字装饰线颜色 |
decorationStyle | 文字装饰线风格([dashed,dotted]虚线,double两根线,solid一根实线,wavy波浪线) |
wordSpacing | 单词间隙(如果是负值,会让单词变得更紧凑 |
letterSpacing | 字母间隙(如果是负值,会让字母变得更紧凑) |
fontFamily | 指定字体 |
fontStyle | 文字样式(italic斜体,normal正常体) |
fontSize | 文字大小 |
color | 文字颜色 |
fontWeight | 字体粗细(bold粗体,normal正常体) |
height | 指定行高,但它不是绝对值只是一个因子,具体的行高等于fontSize*height |
简单示例:
Text("Hello world",
style: TextStyle(
color: Colors.blue,
fontSize: 18.0,
height: 1.2,
fontFamily: "Courier",
background: Paint()..color=Colors.yellow,
decoration:TextDecoration.underline,
decorationStyle: TextDecorationStyle.dashed
),
);
效果:
fontStyle
和 textScaleFactor
都用于控制字体大小,但是有两个主要区别:
fontSize
可以精确指定字体大小,而textScaleFactor
只能通过缩放比例来控制。textScaleFactor
主要是用于系统字体大小设置改变时对 Flutter 应用字体进行全局调整,而fontSize
通常用于单个文本,字体大小不会跟随系统字体大小变化。
TextSpan
如果我们需要对一个 Text
内容的不同部分按照不同的样式显示,这时就可以使用TextSpan
,它代表文本的一个“片段”。我们看看 TextSpan
的定义:
const TextSpan({
TextStyle style,
Sting text,
List<TextSpan> children,
GestureRecognizer recognizer,
});
其中style
和 text
属性代表该文本片段的样式和内容。 children
是一个TextSpan
的数组,也就是说TextSpan
可以包括其他TextSpan
。而recognizer
用于对该文本片段上用于手势进行识别处理。下面我们看一个效果,然后用TextSpan
实现它:
源码:
Text.rich(TextSpan(
children: [
TextSpan(
text: "Home: "
),
TextSpan(
text: "https://flutterchina.club",
style: TextStyle(
color: Colors.blue
),
recognizer: _tapRecognizer
),
]
))
上面代码中,我们通过 TextSpan
实现了一个基础文本片段和一个链接片段,然后通过Text.rich
方法将TextSpan
添加到 Text
中,之所以可以这样做,是因为 Text
其实就是 RichText
的一个包装,而RichText
是可以显示多种样式(富文本)的 widget
。_tapRecognizer
,它是点击链接后的一个处理器(代码已省略)。
DefaultTextStyle
在 Widget
树中,文本的样式默认是可以被继承的(子类文本类组件未指定具体样式时可以使用 Widget
树中父级设置的默认样式),因此,如果在 Widget
树的某一个节点处设置一个默认的文本样式,那么该节点的子树中所有文本都会默认使用这个样式,而DefaultTextStyle
正是用于设置默认文本样式的。下面我们看一个例子:
DefaultTextStyle(
//1.设置文本默认样式
style: TextStyle(
color:Colors.red,
fontSize: 20.0,
),
textAlign: TextAlign.start,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("hello world"),
Text("I am Jack"),
Text("I am Jack",
style: TextStyle(
inherit: false, //2.不继承默认样式
color: Colors.grey
),
),
],
),
);
上面代码中,我们首先设置了一个默认的文本样式,即字体为20像素(逻辑像素)、颜色为红色。然后通过DefaultTextStyle
设置给了子树 Column
节点处,这样一来 Column
的所有子孙 Text
默认都会继承该样式,除非 Text
显示指定不继承样式,如代码中注释2。示例运行效果如图:
添加字体文件
可以在 Flutter 应用程序中使用不同的字体。例如,我们可能会使用设计人员创建的自定义字体,或者其他第三方的字体,如 iconfont 中的字体。
在 Flutter 中使用字体分两步完成。首先在pubspec.yaml
中声明它们,以确保它们会打包到应用程序中。然后通过TextStyle
属性使用字体。
1. 在asset中声明
要将字体文件打包到应用中,和使用其他资源一样,要先在pubspec.yaml
中声明它。然后将字体文件复制到在pubspec.yaml
中指定的位置,如:
flutter:
fonts:
- family: Raleway
fonts:
- asset: assets/fonts/Raleway-Regular.ttf
- asset: assets/fonts/Raleway-Medium.ttf
weight: 500
- asset: assets/fonts/Raleway-SemiBold.ttf
weight: 600
- family: AbrilFatface
fonts:
- asset: assets/fonts/abrilfatface/AbrilFatface-Regular.ttf
2. 使用字体
// 声明文本样式
const textStyle = const TextStyle(
fontFamily: 'Raleway',
);
// 使用文本样式
var buttonText = const Text(
"Use the font for this text",
style: textStyle,
);
3. Package中的字体
要使用 Package
中定义的字体,必须提供package
参数。例如,假设上面的字体声明位于my_package
包中。然后创建 TextStyle
的过程如下:
const textStyle = const TextStyle(
fontFamily: 'Raleway',
package: 'my_package', //指定包名
);
如果在 package
包内部使用它自己定义的字体,也应该在创建文本样式时指定package
参数,如上例所示。
一个包也可以只提供字体文件而不需要在 pubspec.yaml
中声明。 这些文件应该存放在包的lib
文件夹中。字体文件不会自动绑定到应用程序中,应用程序可以在声明字体时有选择地使用这些字体。假设一个名为my_package
的包中有一个字体文件:
lib/fonts/Raleway-Medium.ttf
然后,应用程序可以声明一个字体,如下面的示例所示:
flutter:
fonts:
- family: Raleway
fonts:
- asset: assets/fonts/Raleway-Regular.ttf
- asset: packages/my_package/fonts/Raleway-Medium.ttf
weight: 500
lib/
是隐含的,所以它不应该包含在 asset
路径中。
在这种情况下,由于应用程序本地定义了字体,所以在创建TextStyle
时可以不指定package
参数:
const textStyle = const TextStyle(
fontFamily: 'Raleway',
);
Button
Material 组件库中提供了多种按钮组件如ElevatedButton
、TextButton
、OutlineButton
等,它们都是直接或间接对RawMaterialButton
组件的包装和定制,所以他们大多数属性都和RawMaterialButton
一样。
所有 Material 库中的按钮都有如下相同点:
- 按下时都会有“水波动画”(又称“涟漪动画”,就是点击时按钮上会出现水波扩散的动画)。
- 有一个
onPressed
属性来设置点击回调,当按钮按下时会执行该回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。
Button
通用属性:
属性 | 说明 |
---|---|
onPressed | 必填参数,按下按钮时触发的回调,接收一个方法,传null表示按钮禁用,会显示禁用相关样式 |
child | 子组件 |
style | 通过ButtonStyle装饰 |
ButtonStyle
里面的常用的参数:
属性 | 说明 |
---|---|
foregroundColor | 文本颜色 |
backgroundColor | 按钮的颜色 |
shadowColor | 阴影颜色 |
elevation | 阴影的范围,值越大阴影范围越大 |
padding | 内边距 |
shape | 设置按钮的形状 |
side | 设置边框 |
ElevatedButton
"漂浮"按钮,它默认带有阴影和灰色背景。按下后,阴影会变大。
ElevatedButton(
child: Text("normal"),
onPressed: () {},
);
TextButton
文本按钮,默认背景透明并不带阴影。按下后,会有背景色。
TextButton(
child: Text("normal"),
onPressed: () {},
)
OutlineButton
OutlineButton 默认有一个边框,不带阴影且背景透明。按下后,边框颜色会变亮、同时出现背景和阴影(较弱)。
OutlineButton(
child: Text("normal"),
onPressed: () {},
)
IconButton
IconButton是一个可点击的Icon,不包括文字,默认没有背景,点击后会出现背景。
IconButton(
icon: Icon(Icons.thumb_up),
onPressed: () {},
)
带图标的按钮
ElevatedButton
、TextButton
、OutlineButton
都有一个icon
构造函数,通过它可以轻松创建带图标的按钮:
ElevatedButton.icon(
icon: Icon(Icons.send),
label: Text("发送"),
onPressed: _onPressed,
),
OutlineButton.icon(
icon: Icon(Icons.add),
label: Text("添加"),
onPressed: _onPressed,
),
TextButton.icon(
icon: Icon(Icons.info),
label: Text("详情"),
onPressed: _onPressed,
),
修改按钮的宽度高度
SizedBox(
height: 80,
width: 200,
child: ElevatedButton(
style:ButtonStyle(
backgroundColor:MaterialStateProperty.all(Colors.red),
foregroundColor: MaterialStateProperty.all(Colors.black)
) ,
onPressed: () {
},
child: const Text('宽度高度'),
),
)
自适应按钮
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Container(
height: 60,
margin: const EdgeInsets.all(10),
child: ElevatedButton(
child: const Text('自适应按钮'),
onPressed: () {
print("自适应按钮");
},
),
),
)
],
),
圆形和圆角按钮
圆角按钮:
ElevatedButton(
style: ButtonStyle(
backgroundColor:MaterialStateProperty.all(Colors.blue),
foregroundColor: MaterialStateProperty.all(Colors.white),
elevation: MaterialStateProperty.all(20),
shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))
),
),
onPressed: () {
print("圆角按钮");
},
child: const Text('圆角')
)
圆形按钮:
Container(
height: 80,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.blue),
foregroundColor:
MaterialStateProperty.all(Colors.white),
elevation: MaterialStateProperty.all(20),
shape: MaterialStateProperty.all(
const CircleBorder(side: BorderSide(color: Colors.white)),
)),
onPressed: () {
print("圆形按钮");
},
child: const Text('圆形按钮')),
)
修改OutlinedButton边框
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Expanded(
child: Container(
margin: const EdgeInsets.all(20),
height: 50,
child: OutlinedButton(
style: ButtonStyle(
foregroundColor:
MaterialStateProperty.all(Colors.black),
side: MaterialStateProperty.all(
const BorderSide(width: 1, color: Colors.red))),
onPressed: () {},
child: const Text("注册 配置边框")),
),
)
],
)
Image
Flutter 中,我们可以通过Image
组件来加载并显示图片,Image
的数据源可以是asset、文件、内存以及网络。
ImageProvider
ImageProvider
是一个抽象类,主要定义了图片数据获取的接口load()
,从不同的数据源获取图片需要实现不同的ImageProvider
,如AssetImage
是实现了从Asset
中加载图片的 ImageProvider
,而NetworkImage
实现了从网络加载图片的 ImageProvider
。
Image 的使用
Image
widget 有一个必选的image
参数,它对应一个 ImageProvider
。下面我们分别演示一下如何从 asset
和网络加载图片。
从asset中加载图片
-
在工程根目录下创建一个
images
目录,并将图片avatar.png
拷贝到该目录。 -
在
pubspec.yaml
中的flutter
部分添加如下内容:
assets:
- images/avatar.png
注意: 由于
yaml
文件对缩进严格,所以必须严格按照每一层两个空格的方式进行缩进,此处assets
前面应有两个空格。
- 加载该图片
Image(image: AssetImage("images/avatar.png"), width: 100.0);
Image
也提供了一个快捷的构造函数Image.asset
用于从asset
中加载、显示图片:
Image.asset("images/avatar.png", width: 100.0,)
加载分辨率相关的本地图片
-
在工程根目录下创建一个
images
目录,并在images
中新建2.0x
、3.0x
对应的目录
-
在
pubspec.yaml
声明添加的图片文件, 注意空格
-
使用
Image.asset(
"images/a.jpeg",
width: 150.0,
height: 150.0,
fit: BoxFit.cover,
),
从网络加载图片
NetworkImage
可以加载网络图片,例如:
Image(
image: NetworkImage("https://avatars2.githubusercontent.com/u/20411648?s=460&v=4"),
width: 100.0,
)
Image
也提供了一个快捷的构造函数Image.network
用于从网络加载、显示图片:
Image.network(
"https://avatars2.githubusercontent.com/u/20411648?s=460&v=4",
width: 100.0,
)
运行上面两个示例,图片加载成功后如图3-11所示:
Image 的参数
Image
在显示图片时定义了一系列参数,通过这些参数我们可以控制图片的显示外观、大小、混合效果等。
Image
常用属性:
名称 | 类型 | 说明 |
---|---|---|
alignment | Alignment | 图片的对齐方式 |
color 和 colorBlendMode | 在图片绘制时可以对每一个像素进行颜色混合处理,color指定混合色,而colorBlendMode指定混合模式 | |
fit | BoxFit | fit 属性用来控制图片的拉伸和挤压,这都是根据父容器来的。 |
repeat | ImageRepeat | 当图片本身大小小于显示空间时,指定图片的重复规则 |
width | 宽度 一般结合ClipOval才能看到效果 | |
height | 高度 一般结合ClipOval才能看到效果 |
-
width、height
:用于设置图片的宽、高,当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小,如果只设置width、height
的其中一个,那么另一个属性默认会按比例缩放,但可以通过fit
属性来指定适应规则。 -
fit
:该属性用于在图片的显示空间和图片本身大小不同时指定图片的适应模式。适应模式是在BoxFit
中定义,它是一个枚举类型,有如下值:BoxFit.fill
:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。BoxFit.cover
:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。BoxFit.contain
:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。BoxFit.fitWidth
:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。BoxFit.fitHeight
:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。BoxFit.none
:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。
一图胜万言! 我们对一个宽高相同的头像图片应用不同的fit
值,效果如图所示:
color
和 colorBlendMode
的使用示例:
Image(
image: AssetImage("images/avatar.png"),
width: 100.0,
color: Colors.blue,
colorBlendMode: BlendMode.difference,
);
运行效果如图:
-
repeat
:当图片本身大小小于显示空间时,指定图片的重复规则。取值为ImageRepeat
枚举值:ImageRepeat.repeat
:横向和纵向都进行重复,直到铺满整个画布。ImageRepeat.repeatX
:横向重复,纵向不重复。ImageRepeat.repeatY
:纵向重复,横向不重复。ImageRepeat.noRepeat
:不重复。
简单示例如下:
Image(
image: AssetImage("images/avatar.png"),
width: 100.0,
height: 200.0,
repeat: ImageRepeat.repeatY ,
)
运行后效果如图所示:
完整的示例代码如下:
import 'package:flutter/material.dart';
class ImageAndIconRoute extends StatelessWidget {
Widget build(BuildContext context) {
var img=AssetImage("imgs/avatar.png");
return SingleChildScrollView(
child: Column(
children: <Image>[
Image(
image: img,
height: 50.0,
width: 100.0,
fit: BoxFit.fill,
),
Image(
image: img,
height: 50,
width: 50.0,
fit: BoxFit.contain,
),
Image(
image: img,
width: 100.0,
height: 50.0,
fit: BoxFit.cover,
),
Image(
image: img,
width: 100.0,
height: 50.0,
fit: BoxFit.fitWidth,
),
Image(
image: img,
width: 100.0,
height: 50.0,
fit: BoxFit.fitHeight,
),
Image(
image: img,
width: 100.0,
height: 50.0,
fit: BoxFit.scaleDown,
),
Image(
image: img,
height: 50.0,
width: 100.0,
fit: BoxFit.none,
),
Image(
image: img,
width: 100.0,
color: Colors.blue,
colorBlendMode: BlendMode.difference,
fit: BoxFit.fill,
),
Image(
image: img,
width: 100.0,
height: 200.0,
repeat: ImageRepeat.repeatY ,
)
].map((e){
return Row(
children: <Widget>[
Padding(
padding: EdgeInsets.all(16.0),
child: SizedBox(
width: 100,
child: e,
),
),
Text(e.fit.toString())
],
);
}).toList()
),
);
}
}
Container实现圆形图片
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Center(
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.yellow,
borderRadius: BorderRadius.circular(75),
image: const DecorationImage(
image: NetworkImage(
"https://www.itying.com/themes/itying/images/ionic4.png",
),
fit: BoxFit.cover)
),
),
);
}
}
ClipOval实现圆形图片
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return Center(
child: ClipOval(
child: Image.network(
"https://www.itying.com/themes/itying/images/ionic4.png",
width: 150.0,
height: 150.0,
fit: BoxFit.cover),
),
);
}
}
CircleAvatar实现圆形图片
const CircleAvatar(
radius: 200,
backgroundImage: NetworkImage("https://www.itying.com/images/flutter/3.png"),
)
基本上,CircleAvatar
不提供设置边框的属性。但是,可以将其包裹在具有更大半径和不同背景颜色的不同 CircleAvatar
中,以创建类似于边框的内容。
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
Widget build(BuildContext context) {
return const CircleAvatar(
radius: 110,
backgroundColor: Color(0xffFDCF09),
child: CircleAvatar(
radius: 100,
backgroundImage:
NetworkImage("https://www.itying.com/images/flutter/3.png"),
)
);
}
}
Icon
Flutter 中,可以像Web开发一样使用 iconfont,iconfont 即“字体图标”,它是将图标做成字体文件,然后通过指定不同的字符而显示不同的图片。
在字体文件中,每一个字符都对应一个位码,而每一个位码对应一个显示字形,不同的字体就是指字形不同,即字符对应的字形是不同的。而在iconfont中,只是将位码对应的字形做成了图标,所以不同的字符最终就会渲染成不同的图标。
在Flutter开发中,iconfont和图片相比有如下优势:
- 体积小:可以减小安装包大小。
- 矢量的:iconfont都是矢量图标,放大不会影响其清晰度。
- 可以应用文本样式:可以像文本一样改变字体图标的颜色、大小对齐等。
- 可以通过TextSpan和文本混用。
使用Material Design字体图标
Flutter默认包含了一套Material Design的字体图标,在pubspec.yaml
文件中的配置如下
flutter:
uses-material-design: true
Material Design所有图标可以在其官网查看:https://fonts.google.com/icons/
我们看一个简单的例子:
String icons = "";
// accessible: 0xe03e
icons += "\uE03e";
// error: 0xe237
icons += " \uE237";
// fingerprint: 0xe287
icons += " \uE287";
Text(
icons,
style: TextStyle(
fontFamily: "MaterialIcons",
fontSize: 24.0,
color: Colors.green,
),
);
运行效果如图所示:
通过这个示例可以看到,使用图标就像使用文本一样,但是这种方式需要我们提供每个图标的码点,这并对开发者不友好,所以,Flutter封装了IconData
和Icon
来专门显示字体图标,上面的例子也可以用如下方式实现:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(Icons.accessible,color: Colors.green),
Icon(Icons.error,color: Colors.green),
Icon(Icons.fingerprint,color: Colors.green),
],
)
Icons
类中包含了所有Material Design图标的IconData
静态变量定义。
使用自定义字体图标
我们也可以使用自定义字体图标。iconfont.cn
上有很多字体图标素材,我们可以选择自己需要的图标打包下载后,会生成一些不同格式的字体文件,在Flutter中,我们使用ttf
格式即可。
假设我们项目中需要使用一个书籍图标和微信图标,我们打包下载后导入:
1. 导入字体图标文件;这一步和导入字体文件相同,假设我们的字体图标文件保存在项目根目录下,路径为"fonts/iconfont.ttf
":
fonts:
- family: myIcon #指定一个字体名
fonts:
- asset: fonts/iconfont.ttf
也可以配置多个字体文件:
fonts:
- family: myIcon #指定一个字体名
fonts:
- asset: fonts/iconfont.ttf
- family: alipayIcon #指定一个字体名
fonts:
- asset: fonts/iconfont2.ttf
2. 为了使用方便,我们定义一个MyIcons
类,功能和Icons
类一样:将字体文件中的所有图标都定义成静态变量:
class MyIcons{
// book 图标
static const IconData book = const IconData(
0xe614,
fontFamily: 'myIcon',
matchTextDirection: true
);
// 微信图标
static const IconData wechat = const IconData(
0xec7d,
fontFamily: 'myIcon',
matchTextDirection: true
);
}
3. 使用:
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(MyIcons.book,color: Colors.purple),
Icon(MyIcons.wechat,color: Colors.green),
],
)
运行后效果如图所示:
CheckBox 和 Switch
Material 组件库中提供了 Material 风格的单选开关Switch
和复选框Checkbox
,虽然它们都是继承自StatefulWidget
,但它们本身不会保存当前选中状态,选中状态都是由父组件来管理的。当Switch
或Checkbox
被点击时,会触发它们的onChanged
回调,我们可以在此回调中处理选中状态改变逻辑。
下面是一个简单的例子:
class SwitchAndCheckBoxTestRoute extends StatefulWidget {
_SwitchAndCheckBoxTestRouteState createState() => _SwitchAndCheckBoxTestRouteState();
}
class _SwitchAndCheckBoxTestRouteState extends State<SwitchAndCheckBoxTestRoute> {
bool _switchSelected=true; //维护单选开关状态
bool _checkboxSelected=true;//维护复选框状态
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Switch(
value: _switchSelected,//当前状态
onChanged:(value){
//重新构建页面
setState(() {
_switchSelected=value;
});
},
),
Checkbox(
value: _checkboxSelected,
activeColor: Colors.red, //选中时的颜色
onChanged:(value){
setState(() {
_checkboxSelected=value;
});
} ,
)
],
);
}
}
Switch
和Checkbox
属性比较简单,可自行查阅API文档,它们都有一个activeColor
属性,用于设置激活态的颜色。至于大小,到目前为止,Checkbox
的大小是固定的,无法自定义,而Switch
只能定义宽度,高度也是固定的。
值得一提的是Checkbox
有一个属性tristate
,表示是否为三态,其默认值为false
,这时 Checkbox
有两种状态即“选中”和“不选中”;如果tristate
值为true
时,Checkbox
的 value
的值会有三种:true
、false
、null
。
通过Switch和Checkbox我们可以看到,虽然它们本身是与状态(是否选中)关联的,但它们却不是自己来维护状态,而是需要父组件来管理状态,然后当用户点击时,再通过事件通知给父组件,这样是合理的,因为Switch和Checkbox是否选中本就和用户数据关联,而这些用户数据也不可能是它们的私有状态。我们在自定义组件时也应该思考一下哪种状态的管理方式最为合理。
CheckboxListTile
CheckboxListTile
主要用于方便的实现一组多选按钮组,例如:
class CheckboxListTilePage extends StatefulWidget {
const CheckboxListTilePage({super.key});
State<CheckboxListTilePage> createState() => _CheckboxListTilePageState();
}
class _CheckboxListTilePageState extends State<CheckboxListTilePage> {
final List _hobby = [
{"checked": true, "title": "吃饭"},
{"checked": false, "title": "睡觉"},
{"checked": true, "title": "写代码"}
];
List<Widget> _initHobby() {
List<Widget> tempList = [];
for (var i = 0; i < _hobby.length; i++) {
tempList.add(
CheckboxListTile(
title: Text(_hobby[i]["title"]),
value: _hobby[i]["checked"],
onChanged: (value) {setState(() => _hobby[i]["checked"] = value);}
)
);
}
return tempList;
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Title'),
),
body: ListView(
children: [
Text(
"爱好:",
style: Theme.of(context).textTheme.headline6,
),
Column(
children: _initHobby(),
),
const SizedBox(height: 40),
ElevatedButton(
onPressed: () {
print(_hobby);
},
child: const Text("获取值"))
],
),
);
}
}
Radio
Radio
可以用来实现单选按钮组
import 'package:flutter/material.dart';
class RadioPage extends StatefulWidget {
const RadioPage({super.key});
State<RadioPage> createState() => _RadioPageState();
}
class _RadioPageState extends State<RadioPage> {
int sex = 1;
int flag=1;
_onChanged(value) {
setState(() {
sex = value;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Radio"),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text("男:"),
Radio(value: 1, groupValue: sex, onChanged: _onChanged),
const SizedBox(width: 20),
const Text("女:"),
Radio(value: 2, groupValue: sex, onChanged: _onChanged)
],
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(sex==1?"男":"女")
],
),
],
),
);
}
}
RadioListTile
与CheckboxListTile
类似,RadioListTile
可以方便的实现一组单选按钮组:
RadioListTile(
title: Text("男"),
value: 1,
groupValue: sex,
onChanged: _radioChange),
RadioListTile(
title: Text("女"),
value: 2,
groupValue: sex,
onChanged: _radioChange),
TextField
TextField
表单常见属性:
属性 | 描述 |
---|---|
maxLines | 设置输入框的最大行数,默认为1 ;如果为null ,则无行数限制 |
onChanged | 输入框内容改变时的回调函数;注:内容改变事件也可以通过controller来监听 |
decoration | 用于控制TextField的外观显示,如提示文本、背景颜色、边框等。hintText 类似html中的placeholder border 配置文本框边框 OutlineInputBorder配合使用 labelText lable的名称 labelStyle 配置lable的样式 |
obscureText | 把文本框框改为密码框 |
controller | 结合TextEditingController() 可以配置表单默认显示的内容,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。 大多数情况下我们都需要显式提供一个controller来与文本框交互。 如果没有提供controller,则TextField内部会自动创建一个。 |
focusNode | 用于控制TextField是否占有当前键盘的输入焦点。 它是我们和键盘交互的一个句柄(handle) |
keyboardType | 用于设置该输入框默认的键盘输入类型 |
textInputAction | 键盘动作按钮图标(即回车键位图标),它是一个枚举值,有多个可选值, 如 TextInputAction.search 会在键盘右下角显示一个放大镜图标 |
style | 正在编辑的文本样式 |
textAlign | 输入框内编辑文本在水平方向的对齐方式 |
autofocus | 是否自动获取焦点 |
obscureText | 是否隐藏正在编辑的文本,如用于输入密码的场景等,文本内容会用“•”替换 |
maxLength | 输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数 |
maxLengthEnforcement | 决定当输入文本长度超过maxLength 时如何处理,如截断、超出等 |
toolbarOptions | 长按或鼠标右击时出现的菜单,包括 copy、cut、paste 以及 selectAll |
onEditingComplete 和onSubmitted | 这两个回调都是在输入框输入完成时触发,比如按了键盘的完成键(对号图标)或搜索键(🔍图标)。 不同的是两个回调签名不同, onSubmitted 回调是ValueChanged<String> 类型,它接收当前输入内容做为参数,而 onEditingComplete 不接收参数。 |
inputFormatters | 用于指定输入格式;当用户输入内容改变时,会根据指定的格式来校验 |
enable | 设置输入框是否禁用,禁用状态不能响应输入和事件,同时显示禁用态样式 |
cursorWidth 、cursorRadius 和cursorColor | 这三个属性是用于自定义输入框光标宽度、圆角和颜色的 |
其中,keyboardType
的取值如下:
TextInputType枚举值 | 含义 |
---|---|
text | 文本输入键盘 |
multiline | 多行文本,需和maxLines配合使用(设为null或大于1) |
number | 数字;会弹出数字键盘 |
phone | 优化后的电话号码输入键盘;会弹出数字键盘并显示“* # ” |
datetime | 优化后的日期输入键盘;Android上会显示“: -” |
emailAddress | 优化后的电子邮件地址;会显示“@ .” |
url | 优化后的url输入键盘; 会显示“/ .” |
示例:实现一个登录表单
1)布局
Column(
children: <Widget>[
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)
),
),
TextField(
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
prefixIcon: Icon(Icons.lock)
),
obscureText: true,
),
],
);
运行效果:
2)获取输入内容
获取输入内容有两种方式:
- 定义两个变量,用于保存用户名和密码,然后在
onChange
触发时,各自保存一下输入内容。 - 通过
controller
直接获取。
第一种方式比较简单,不在举例,我们来重点看一下第二种方式,我们以用户名输入框举例,首先定义一个controller
:
TextEditingController _unameController = TextEditingController();
然后设置输入框controller
:
TextField(
autofocus: true,
controller: _unameController, //设置controller
...
)
通过controller
获取输入框内容:
print(_unameController.text)
3)监听文本变化
监听文本变化也有两种方式:
- 通过设置
onChange
回调监听,如:
TextField(
autofocus: true,
onChanged: (v) {
print("onChange: $v");
}
)
- 通过
controller
监听,如:
void initState() {
//监听输入改变
_unameController.addListener((){
print(_unameController.text);
});
}
两种方式相比,onChanged
是专门用于监听文本变化,而controller
的功能却多一些,除了能监听文本变化外,它还可以设置默认值、选择文本,下面我们看一个例子:
创建一个controller
:
TextEditingController _selectionController = TextEditingController();
设置默认值,并从第三个字符开始选中后面的字符:
_selectionController.text="hello world!";
_selectionController.selection=TextSelection(
baseOffset: 2,
extentOffset: _selectionController.text.length
);
设置controller
:
TextField(
controller: _selectionController,
)
运行效果:
4)控制焦点
焦点可以通过FocusNode
和FocusScopeNode
来控制,默认情况下,焦点由FocusScope
来管理,它代表焦点控制范围,可以在这个范围内可以通过FocusScopeNode
在输入框之间移动焦点、设置默认焦点等。
我们可以通过FocusScope.of(context)
来获取Widget
树中默认的FocusScopeNode
。
下面看一个示例,在此示例中创建两个TextField
,第一个自动获取焦点,然后创建两个按钮:
- 点击第一个按钮可以将焦点从第一个
TextField
挪到第二个TextField
。 - 点击第二个按钮可以关闭键盘。
要实现的效果如图所示:
代码如下:
class FocusTestRoute extends StatefulWidget {
_FocusTestRouteState createState() => _FocusTestRouteState();
}
class _FocusTestRouteState extends State<FocusTestRoute> {
FocusNode focusNode1 = FocusNode();
FocusNode focusNode2 = FocusNode();
FocusScopeNode? focusScopeNode;
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(16.0),
child: Column(
children: <Widget>[
TextField(
autofocus: true,
focusNode: focusNode1,//关联focusNode1
decoration: InputDecoration(
labelText: "input1"
),
),
TextField(
focusNode: focusNode2,//关联focusNode2
decoration: InputDecoration(
labelText: "input2"
),
),
Builder(builder: (ctx) {
return Column(
children: <Widget>[
ElevatedButton(
child: Text("移动焦点"),
onPressed: () {
//将焦点从第一个TextField移到第二个TextField
// 这是一种写法 FocusScope.of(context).requestFocus(focusNode2);
// 这是第二种写法
if(null == focusScopeNode){
focusScopeNode = FocusScope.of(context);
}
focusScopeNode.requestFocus(focusNode2);
},
),
ElevatedButton(
child: Text("隐藏键盘"),
onPressed: () {
// 当所有编辑框都失去焦点时键盘就会收起
focusNode1.unfocus();
focusNode2.unfocus();
},
),
],
);
},
),
],
),
);
}
}
FocusNode
和FocusScopeNode
还有一些其他的方法,详情可以查看API文档。
5)监听焦点状态改变事件
FocusNode
继承自ChangeNotifier
,通过FocusNode
可以监听焦点的改变事件,如:
...
// 创建 focusNode
FocusNode focusNode = FocusNode();
...
// focusNode绑定输入框
TextField(focusNode: focusNode);
...
// 监听焦点变化
focusNode.addListener((){
print(focusNode.hasFocus);
});
获得焦点时focusNode.hasFocus
值为true
,失去焦点时为false
。
6)自定义样式
我们可以通过decoration
属性来定义输入框样式,下面以自定义输入框下划线颜色为例来介绍一下:
TextField(
decoration: InputDecoration(
labelText: "请输入用户名",
prefixIcon: Icon(Icons.person),
// 未获得焦点下划线设为灰色
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.grey),
),
//获得焦点下划线设为蓝色
focusedBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.blue),
),
),
),
上面代码我们直接通过InputDecoration
的enabledBorder
和focusedBorder
来分别设置了输入框在未获取焦点和获得焦点后的下划线颜色。
另外,我们也可以通过主题来自定义输入框的样式,下面我们探索一下如何在不使用enabledBorder
和focusedBorder
的情况下来自定义下滑线颜色。
由于TextField
在绘制下划线时使用的颜色是主题色里面的hintColor
,但提示文本颜色也是用的hintColor
, 如果我们直接修改hintColor
,那么下划线和提示文本的颜色都会变。值得高兴的是decoration
中可以设置hintStyle
,它可以覆盖hintColor
,并且主题中可以通过inputDecorationTheme
来设置输入框默认的decoration
。所以我们可以通过主题来自定义,代码如下:
Theme(
data: Theme.of(context).copyWith(
hintColor: Colors.grey[200], //定义下划线颜色
inputDecorationTheme: InputDecorationTheme(
labelStyle: TextStyle(color: Colors.grey),//定义label字体样式
hintStyle: TextStyle(color: Colors.grey, fontSize: 14.0)//定义提示文本样式
)
),
child: Column(
children: <Widget>[
TextField(
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
prefixIcon: Icon(Icons.person)
),
),
TextField(
decoration: InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: "密码",
hintText: "您的登录密码",
hintStyle: TextStyle(color: Colors.grey, fontSize: 13.0)
),
obscureText: true,
)
],
)
)
运行效果:
我们成功的自定义了下划线颜色和提示文字样式,但是通过这种方式自定义后,输入框在获取焦点时,labelText
不会高亮显示了,正如上图中的"用户名"本应该显示蓝色,但现在却显示为灰色,并且我们还是无法定义下划线宽度。
另一种灵活的方式是直接隐藏掉TextField
本身的下划线,然后通过Container
去嵌套定义样式,如:
Container(
child: TextField(
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: "Email",
hintText: "电子邮件地址",
prefixIcon: Icon(Icons.email),
border: InputBorder.none //隐藏下划线
)
),
decoration: BoxDecoration(
// 下滑线浅灰色,宽度1像素
border: Border(bottom: BorderSide(color: Colors.grey[200], width: 1.0))
),
)
运行效果:
通过这种组件组合的方式,也可以定义背景圆角等。一般来说,优先通过decoration
来自定义样式,如果decoration
实现不了,再用widget
组合的方式。
在这个示例中,下划线颜色是固定的,所以获得焦点后颜色仍然为灰色,可以结合前面的
FocusNode
通过监听焦点的改变事件,即可轻松实现点击后下滑线也变色,可自行尝试。
Form
Flutter提供了一个Form
组件,它可以对输入框进行分组,然后进行一些统一操作,如输入内容校验、输入框重置以及输入内容保存。
Form
继承自StatefulWidget
对象,它对应的状态类为FormState
。我们先看看Form
类的定义:
Form({
required Widget child,
bool autovalidate = false,
WillPopCallback onWillPop,
VoidCallback onChanged,
})
属性 | 含义 |
---|---|
autovalidate | 是否自动校验输入内容; 当为 true 时,每一个子 FormField 内容发生变化时都会自动校验合法性,并直接显示错误信息。当为 false 时,需要通过调用FormState.validate() 来手动校验 |
onWillPop | 决定Form 所在的路由是否可以直接返回(如点击返回按钮),该回调返回一个Future 对象,如果 Future 的最终结果是false ,则当前路由不会返回;如果为true ,则会返回到上一个路由。此属性通常用于拦截返回按钮 |
onChanged | Form 的任意一个子FormField 内容发生变化时会触发此回调 |
FormField
Form
的子孙元素必须是FormField
类型,FormField
是一个抽象类,定义几个属性,FormState
内部通过它们来完成操作,FormField
部分定义如下:
const FormField({
...
FormFieldSetter<T> onSaved, //保存回调
FormFieldValidator<T> validator, //验证回调
T initialValue, //初始值
bool autovalidate = false, //是否自动校验。
})
为了方便使用,Flutter 提供了一个TextFormField
组件,它继承自FormField
类,也是TextField
的一个包装类,所以除了FormField
定义的属性之外,它还包括TextField
的属性。
FormState
FormState
为Form
的State
类,可以通过Form.of()
或GlobalKey
获得。我们可以通过它来对Form
的子孙FormField
进行统一操作。我们看看其常用的三个方法:
-
FormState.validate()
:调用此方法后,会调用Form
子孙FormField
的validate
回调,如果有一个校验失败,则返回false
,所有校验失败项都会返回用户返回的错误提示。 -
FormState.save()
:调用此方法后,会调用Form
子孙FormField
的save
回调,用于保存表单内容 -
FormState.reset()
:调用此方法后,会将子孙FormField
的内容清空。
示例:修改上面用户登录的示例,在提交之前进行校验
- 用户名不能为空,如果为空则提示“用户名不能为空”。
- 密码不能小于 6 位,如果小于 6 为则提示“密码不能少于 6 位”。
完整代码:
import 'package:flutter/material.dart';
class FormTestRoute extends StatefulWidget {
_FormTestRouteState createState() => _FormTestRouteState();
}
class _FormTestRouteState extends State<FormTestRoute> {
TextEditingController _unameController = TextEditingController();
TextEditingController _pwdController = TextEditingController();
GlobalKey _formKey = GlobalKey<FormState>();
Widget build(BuildContext context) {
return Form(
key: _formKey, //设置globalKey,用于后面获取FormState
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: <Widget>[
TextFormField(
autofocus: true,
controller: _unameController,
decoration: InputDecoration(
labelText: "用户名",
hintText: "用户名或邮箱",
icon: Icon(Icons.person),
),
// 校验用户名
validator: (v) {
return v!.trim().isNotEmpty ? null : "用户名不能为空";
},
),
TextFormField(
controller: _pwdController,
decoration: InputDecoration(
labelText: "密码",
hintText: "您的登录密码",
icon: Icon(Icons.lock),
),
obscureText: true,
//校验密码
validator: (v) {
return v!.trim().length > 5 ? null : "密码不能少于6位";
},
),
// 登录按钮
Padding(
padding: const EdgeInsets.only(top: 28.0),
child: Row(
children: <Widget>[
Expanded(
child: ElevatedButton(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text("登录"),
),
onPressed: () {
// 通过_formKey.currentState 获取FormState后,
// 调用validate()方法校验用户名密码是否合法,校验
// 通过后再提交数据。
if ((_formKey.currentState as FormState).validate()) {
//验证通过提交数据
}
},
),
),
],
),
)
],
),
);
}
}
注意,登录按钮的onPressed
方法中不能通过Form.of(context)
来获取FormState
,原因是,此处的context
为FormTestRoute
的context
,而Form.of(context)
是根据所指定context
向根去查找,而FormState
是在FormTestRoute
的子树中,所以不行。正确的做法是通过Builder
来构建登录按钮,Builder
会将widget
节点的context
作为回调参数:
Expanded(
// 通过Builder来获取ElevatedButton所在widget树的真正context(Element)
child:Builder(builder: (context){
return ElevatedButton(
...
onPressed: () {
//由于本widget也是Form的子代widget,所以可以通过下面方式获取FormState
if(Form.of(context).validate()){
//验证通过提交数据
}
},
);
})
)
其实
context
正是操作Widget
所对应的Element
的一个接口,由于Widget
树对应的Element
都是不同的,所以context
也都是不同的。Flutter中有很多“of(context)
”这种方法,在使用时一定要注意context
是否正确。
ProgressIndicator
Material 组件库中提供了两种进度指示器:LinearProgressIndicator
和CircularProgressIndicator
,它们都可以同时用于精确的进度指示和模糊的进度指示。精确进度通常用于任务进度可以计算和预估的情况,比如文件下载;而模糊进度则用户任务进度无法准确获得的情况,如下拉刷新,数据提交等。
LinearProgressIndicator
LinearProgressIndicator
是一个线性、条状的进度条,定义如下:
LinearProgressIndicator({
double value,
Color backgroundColor,
Animation<Color> valueColor,
...
})
属性 | 含义 |
---|---|
value | 表示当前的进度,取值范围为[0,1] ;如果 value 为null ,则指示器会执行一个循环动画(模糊进度);当 value 不为null 时,指示器为一个具体进度的进度条 |
backgroundColor | 指示器的背景色 |
valueColor | 指示器的进度条颜色;值得注意的是,该值类型是Animation<Color> ,这允许我们对进度条的颜色也可以指定动画。如果我们不需要对进度条颜色执行动画,换言之,我们想对进度条应用一种固定的颜色,此时我们可以通过AlwaysStoppedAnimation 来指定 |
示例代码:
// 模糊进度条(会执行一个动画)
LinearProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
),
//进度条显示50%
LinearProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
value: .5,
)
效果:
第一个进度条在执行循环动画:蓝色条一直在移动,而第二个进度条是静止的,停在50%的位置。
CircularProgressIndicator
CircularProgressIndicator
是一个圆形进度条,定义如下:
CircularProgressIndicator({
double value,
Color backgroundColor,
Animation<Color> valueColor,
this.strokeWidth = 4.0,
...
})
前三个参数和LinearProgressIndicator
相同,不再赘述。strokeWidth
表示圆形进度条的粗细。示例如下:
// 模糊进度条(会执行一个旋转动画)
CircularProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
),
//进度条显示50%,会显示一个半圆
CircularProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
value: .5,
),
效果:
第一个进度条会执行旋转动画,而第二个进度条是静止的,它停在50%的位置。
自定义尺寸
我们可以发现LinearProgressIndicator
和CircularProgressIndicator
,并没有提供设置圆形进度条尺寸的参数;如果我们希望LinearProgressIndicator
的线细一些,或者希望CircularProgressIndicator
的圆大一些该怎么做?
其实LinearProgressIndicator
和CircularProgressIndicator
都是取父容器的尺寸作为绘制的边界的。知道了这点,我们便可以通过尺寸限制类Widget,如ConstrainedBox
、SizedBox
来指定尺寸,如:
// 线性进度条高度指定为3
SizedBox(
height: 3,
child: LinearProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
value: .5,
),
),
// 圆形进度条直径指定为100
SizedBox(
height: 100,
width: 100,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
value: .7,
),
),
效果:
注意,如果CircularProgressIndicator
显示空间的宽高不同,则会显示为椭圆。如:
// 宽高不等
SizedBox(
height: 100,
width: 130,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation(Colors.blue),
value: .7,
),
),
效果:
进度色动画
下面代码实现一个进度条在3秒内从灰色变成蓝色的动画:
import 'package:flutter/material.dart';
class ProgressRoute extends StatefulWidget {
_ProgressRouteState createState() => _ProgressRouteState();
}
class _ProgressRouteState extends State<ProgressRoute>
with SingleTickerProviderStateMixin {
AnimationController _animationController;
void initState() {
//动画执行时间3秒
_animationController = AnimationController(
vsync: this, //注意State类需要混入SingleTickerProviderStateMixin(提供动画帧计时/触发器)
duration: Duration(seconds: 3),
);
_animationController.forward();
_animationController.addListener(() => setState(() => {}));
super.initState();
}
void dispose() {
_animationController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
children: <Widget>[
Padding(
padding: EdgeInsets.all(16),
child: LinearProgressIndicator(
backgroundColor: Colors.grey[200],
valueColor: ColorTween(begin: Colors.grey, end: Colors.blue)
.animate(_animationController), // 从灰色变成蓝色
value: _animationController.value,
),
);
],
),
);
}
}
自定义进度指示器样式
定制进度指示器风格样式,可以通过CustomPainter
Widget 来自定义绘制逻辑,实际上LinearProgressIndicator
和CircularProgressIndicator
也正是通过CustomPainter
来实现外观绘制的。
flutter_spinkit 包提供了多种风格的模糊进度指示器,可以参考。
参考:《Flutter实战·第二版》