1. 概述
上一篇说到,Basics Widget 并不是 Flutter 的一个专门的Widget类别,而是 Flutter 官方挑选一些开发常用的 Widget 构成的,希望我们掌握到一些最基本的开发能力。
包括:
- 文本 Text
- 按钮 Button
- 图片 Image
- 单选框、复选框
- 输入框、表单
- 指示器
- Container
- …
2. 常用组件
2.1 Text
Text 用于显示简单样式文本,然后可以填充一些文本显示样式的属性,如下例子:
Text("Hello World",
textAlign: TextAlign.left,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textScaleFactor: 1.5);
textAlign 文本对齐方式maxLines 、 overflow maxLines 指定文本显示的最大行数。 当文本内容超过最大行数时, overflow 指定了阶段方式, 例如 ellipsis 就是将多余的文本用 “…” 表示textScaleFactor 代表文本相对于当前字体大小的缩放因子,想你对于去设置文本的样式 style 属性的 fontSize, 它是调整字体大小的一个快捷方式, 该属性的默认值可以通过 MediaQueryData.textScaleFactor 获得, 如果没有 MediaQuery ,那么会默认值为 1.0
2.1.1 TextStyle
TextStyle 用于指定文本样式,例如颜色、字体、粗细、背景等,如下:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: "Flutter",
home: Scaffold(
appBar: AppBar(
title: const Text("Basics Widget"),
),
body: Text(
"Hello World",
style: TextStyle(
color: Colors.blue,
fontSize: 19.0,
height: 2,
fontFamily: "Courier",
background: Paint()..color = Colors.yellow,
decoration: TextDecoration.underline,
decorationStyle: TextDecorationStyle.dashed),
)));
}
效果如图: 一些属性:
height 行高,它不是一个绝定的值,因为具体的行高为 height*fontSize ,同理行宽也是fontFamily 由于不同平台默认支持的字体集不同,所以在手动指定字体时一定要先在不同平台测试一下fontSize 改属性和 Text 的 textScaleFactor 都用于控制字体大小,但是有两个区别, ①:fontSize 可以精确指定字体大小, 而 textScaleFactor 只能缩放比例 ②: textScaleFactor 主要是用于系统字体大小设置改变时,对Flutter 应用字体进行全局调整,而 fontSzie通常用于单个文本,字体大小不会跟随系统字体大小变化
2.1.2 TextSpan
如果我们需要对Text内容不同部分按照不同的样式显示,就可以使用 TextSpan,代表文本的一个“片段”,看看 TextSpan的定义:
const TextSpan({
this.text,
this.children,
TextStyle? style,
this.recognizer,
MouseCursor? mouseCursor,
this.onEnter,
this.onExit,
this.semanticsLabel,
this.locale,
this.spellOut,
})
其中 style 和 text 代表样式和文本内容, children是 List<InlineSpan>? 类型,也就说 TextSpan 可以包含其他 Span reconizer 用于表示该文本片段上用于手势进行识别处理,下面我们看一个效果图,然后用 TextSpan 来实现:
body: const Text.rich(TextSpan(children: [
TextSpan(text: "Home: "),
TextSpan(
text: "https://flutterchina.club",
style: TextStyle(color: Colors.blue),
recognizer: _recognizer
),
]))));
这里的代码,用 TextSpan实现了一个基础文本和一个链接片段
Text.rich 方法将 TextSpan 添加到 Text 中,之所以可以这样做,是因为 Text 其实就是 RichText 的一个包装,而 RichText 是可以显示多种多样的 widget_reconizer 是点击链接的处理器
2.1.3 DefaultTextStyle
在 Widget 树中, 文本的样式默认是可以被继承的,因此如果 Widget树的某一个节点处设置一个默认的文本样式,那么该节点的子树所有的文本都会默认使用这个样式,而 DefaultTextStyle 正是用于设置默认文本样式的,看下面例子:
DefaultTextStyle(
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,
color: Colors.grey
),
),
],
),
);
这里的代码首先设置了一个默认的样式,字体大小为20,、颜色为红色,然后将 DefaultTextStyle 设置给了子树,这样一来 Column 所有子孙 Text 默认都会继承该样式, 除非 Text 设置 inherit: false ,如下所示:
2.1.4 使用字体
在 Flutter 中可以使用自定义的字体,或者其他第三方字体, 这里就不介绍配置了,具体可以看官方文档:字体
2.2 Button
Material 组件库提供了多种多样的按钮,他们都是直接或间接对 RawMaterialButton 的包装定制,所以大部分属性都一样。另外 Marterial 库中的按钮都有以下共同点:
- 按下时都有水波纹动画
- 统一用
onPressed 属性来设置回调,当按钮按下时会执行该回调,如果不提供回调则按钮会处于禁用状态,不会响应用户点击
2.2.1 ElevatedButton
即 带阴影的按钮, 默认带有阴影和灰色背景,按下后阴影会变大,如下所示: 代码如下:
child: ElevatedButton(
child: const Text("i am ElevatedButton"),
onPressed: () {},
),
),
2.2.2 TextButton
文本按钮,按下后会有背景色,如下图所示:
2.2.3 OutlinedButton
默认有一个边框,不带阴影且背景透明,按下后,边框颜色会变亮、同时出现背景和阴影,如下图所示:
2.2.4 IconButton
可以点击的 Icon, 不包含文字,点击后会出现背景,如下所示: 代码设置为:
IconButton(
icon: Icon(Icons.eleven_mp),
onPressed: () {},
),
2.2.5 带图标的按钮
上面学到的 ElevatedButton 、 TextButton 、 OutlinedButton 都有一个 icon() 的构造函数,这样就可以代入一个图片进去,例如设置:
ElevatedButton.icon(
icon: const Icon(Icons.send),
label: const Text("发送"),
onPressed: () {},
),
效果为(这里有编码问题,可以无视):
2.3 图片及Icon
2.3.1 图片
可以通过 Image 组件来加载并显示布局, Image 的数据源可以是
2.3.1.1 ImageProvider
ImageProvider 是抽象类,主要定义了图片的获取接口 load() ,从不同的数据源获取图片需要实现不同的 ImageProvider ,如 AssetImage 是实现了从 Asset 中加载图片, NetworkImage 则实现了从网络中加载图片。
2.3.1.2 Image Widget
Image 组件在构建时有一个必选的 image 参数,它对应一个 ImageProvier ,下面分别演示一下如何从 asset 和 网络中加载图片。
1.从 asset 中加载图片 在工程根目录下创建一个 images 目录,并将图片拷贝到该目录。 接下来在 pubspec.yaml 文件的 flutter部分 中,写入(注意缩进):
flutter:
..
assets:
- assets/images/bobo.jpg
最后在代码中使用:
Image(
image: AssetImage("images/bobo.jpg"),
width: 100.0,
)
就能展示图片。
(不过我这里遇到一个问题,使用手机运行Flutter应用能正常展示图片,但是使用 Chrome 模拟器会报错,不知道是什么原因造成的
2.从网络URL中加载图片 直接使用代码:
Image(
image: NetworkImage("https://www.wahaotu.com/uploads/allimg/201904/1554901831804910.jpg"),
width: 100.0,
)
可以正常展示图片。
(不过这里出现了很上面一样的问题,但是使用官方使用的url又能正常展示图片
2.3.1.3 Image 参数
我们可以来看下 Image 的参数,通过这些参数可以控制图片外观、大小、混合效果等。
const Image({
Key? key,
required this.image,
this.frameBuilder,
this.loadingBuilder,
this.errorBuilder,
this.semanticLabel,
this.excludeFromSemantics = false,
this.width,
this.height,
this.color,
this.opacity,
this.colorBlendMode,
this.fit,
this.alignment = Alignment.center,
this.repeat = ImageRepeat.noRepeat,
this.centerSlice,
this.matchTextDirection = false,
this.gaplessPlayback = false,
this.isAntiAlias = false,
this.filterQuality = FilterQuality.low,
})
width 、height 设置图片宽高,当不指定宽高时,会根据当前父容器的限制尽可能的显示其原始大小,如果只设置其中一个,那么另一个属性默认会按比例缩放fit 该属性用于用于在图片的显示空间和图片本身大小不同时指定图片的适应模式。适应模式是在 BoxFit 中定义的,它是一个枚举类型,有这些值: ①fill :拉伸填充满显示空间 ,图片会便是 ②cover :会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示部分会被剪裁 ③contain :图片默认适应规则,图片会保证图片本身长宽比不变的情况下缩放以适应当前的显示空间 ④fitWidth :图片宽度会缩放到显示空间的宽度,高度会按比例缩放,居中显示,图片不会变形 ⑤fitHeight :和上面的反着来 ⑥none :图片没有适应策略,会在显示空间内显示图片 color 和 colorBlendMode :在图片绘制时可以对每一个像素进行颜色混合处理,color 指定混合色,而 colorBlendMode 指定混合模式下,因为用的比较少,这里就不做实例repeat 当图片本身大小小于显示空间时,指定图片的重复规则,这里也不做展示
2.3.2 Icon
Android中有 svg 矢量图, 而 Flutter 中的也有,就是 Icon ,它有下面这些优点:
- 体积小
- 因为是矢量图,所以拉伸不会影响清晰程度
- 可以通过 TextSpan 和 文本混用
- 可以引用到文本样式
Flutter 默认实现了一套Icon,在 pubspec.yaml 的配置文件可以看到:
flutter:
uses-material-design: true
来看下官方的示例代码:
String icons = "";
icons += "\uE03e";
icons += " \uE237";
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),
],
)
我们也可以使用自定义的字体图标,这里就不赘述了,可以看看官方示例:Icon自定义字体图标
2.4 单选开关和复选框
Flutter 提供了 Material 风格的 开关Switch 和 复选框Checkbox ,它们都继承自 StatfulWidget ,但是它们不会保存选中的状态,选中状态是由父组件来管理的。 当 Switch 或者 Checkbox 被点击时,会触发 onChanged 回调,我们可以在此回调中处理选中状态改变逻辑,下面看官方例子:
class SwitchAndCheckBoxTestRoute extends StatefulWidget {
@override
_SwitchAndCheckBoxTestRouteState createState() => _SwitchAndCheckBoxTestRouteState();
}
class _SwitchAndCheckBoxTestRouteState extends State<SwitchAndCheckBoxTestRoute> {
bool _switchSelected=true;
bool _checkboxSelected=true;
@override
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 的选中状态,所以 Widget 继承自 StatefulWidget 。 在其 build 方法中分别状态了 Switch 和 Checkbox, 并且用两个 bool 值来维护分别的选中状态。 当按钮被点击时,会回调 onChanged 回调选中状态出去,此时我们需要调用 setState() 方法来触发 Flutter 重绘。
为什么要这样子设计,我的理解是:
- 将开关、复选框的状态抛给父组件,可以更加灵活,比如在勾选时候做一些网络请求,即异步的操作
- 一般来说,这些item是否选中,是和用户数据关联的,用户数据也不可能是他们的私有状态,所以放在一起管理更好
2.4.1 属性
它们的属性比较简单,常用的有:
activeColor :设置激活状态的颜色tristate : 是否为三态,仅 Checbox有,一般情况下只有 “true” 和 “false”,表示选中和非选中,如果设置了 tristate 后,还会增加一个 “null” 状态
此外, Checkbox 不可设置宽高,其大小是自定义的,而 Switch 也仅能设置宽度而已。
2.5 输入框以及表单
Flutter Material组件提供了 输入款TextField 和 表单Form
2.5.1 输入框 TextField
2.5.1.1 属性
来看下 TextField 提供的属性:
const TextField({
...
this.controller,
this.focusNode,
this.decoration = const InputDecoration(),
TextInputType? keyboardType,
this.textInputAction,
this.textCapitalization = TextCapitalization.none,
this.style,
this.strutStyle,
this.textAlign = TextAlign.start,
this.textAlignVertical,
this.textDirection,
this.readOnly = false,
ToolbarOptions? toolbarOptions,
this.showCursor,
this.autofocus = false,
this.obscuringCharacter = '?',
this.obscureText = false,
this.autocorrect = true,
SmartDashesType? smartDashesType,
SmartQuotesType? smartQuotesType,
this.enableSuggestions = true,
this.maxLines = 1,
this.minLines,
this.expands = false,
this.maxLength,
this.maxLengthEnforcement,
this.onChanged,
this.onEditingComplete,
this.onSubmitted,
this.onAppPrivateCommand,
this.inputFormatters,
this.enabled,
this.cursorWidth = 2.0,
this.cursorHeight,
this.cursorRadius,
this.cursorColor,
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
this.keyboardAppearance,
this.scrollPadding = const EdgeInsets.all(20.0),
this.dragStartBehavior = DragStartBehavior.start,
this.enableInteractiveSelection = true,
this.selectionControls,
this.onTap,
this.mouseCursor,
this.buildCounter,
this.scrollController,
this.scrollPhysics,
this.autofillHints,
this.restorationId,
this.enableIMEPersonalizedLearning = true,
})
属性比较多,列几个关键的讲解:
controller 编辑框的控制器,通过它可以设置/获取编辑框的内容、选择编辑内容、监听编辑文本改变事件。大多数情况下我们都需要显示提供一个 controller 来与文本框交互,如果设置的话, TextField 内部会创建一个focusNode 用于控制 TextField 是否占有当前键盘的输入焦点InputDecoration 用于控制 TextField 的外观显示,如提示文本、背景颜色、边框等。keyboardType 用于设置该输入框默认的键盘输入类型, 有文本、电话、email等格式textInputAction 键盘动作按钮图标,就是右下角的那个图标设置style 文本的样式(正在编辑中的)textAlign 输入框内编辑文本在水平方向的对齐方式autofocus 是否自动获取焦点obscureText 是否隐藏正在编辑的文本, 比如输入密码的场景,文本内容会用 “?” 来代替maxLines 最大行数maxLenth 和 maxLengthEnforcement maxLenth 代表输入框文本的最大长度,设置后输入框右下角会显示输入的文本计数 maxLengthEnforcement 决定输入文本长度超过 maxLength 时如何处理,如截断toolbarOptions 长按时出现的菜单,可以选择 copy、cut、paste 、selectAll- onChange
输入框内容改变的回调, 当然 controller 也可以做到监听 onEditingComplete 、onSubmitted 作用一样,都是在输入完成时触发,比如点击了键盘的 完成键、搜索键不同的是两个回调签名不同inputFormatters 指定输入格式,当用户输入内容改变时,会根据指定格式来校验enable 如果为false, 则输入框会被禁用cursorWidth 、 cursorRadius 、 cursorColor 分别表示自定义输入框光标宽度、圆角和颜色
一个简单的设置代码如下:
Column(children: const <Widget>[
TextField(
autofocus: true,
decoration: InputDecoration(
labelText: "用户名",
hintText: "请输入用户名或密码",
prefixIcon: Icon(Icons.person)
),
),
TextField(
decoration: InputDecoration(
labelText: "密码",
hintText: "请输入密码",
prefixIcon: Icon(Icons.lock)
),
obscureText: true,
)
]),
2.5.1.2 通过 controller 获取输入内容
我们可以通过 onChange 拿到内容。 当然也可以使用 controller 来获取
步骤为:
- 定义一个
controller :
final TextEditingController _tfController = TextEditingController();
- 然后在 TextFiled 中传入这个 controller
TextField(
controller: _tfController,
...
)
最后就可以通过 : print(_tfController.text) 来获得输入框的内容
2.5.1.3 通过 controller 监听文本内容变化
可以通过 onChange 来监听文本, controller 可以通过设置监听器来监听文本,如下:
@override
void initState() {
super.initState();
_tfController.addListener(() {
print(_tfController.text);
});
}
controller 的功能更多,除了监听文本,还可以设置默认值、选择文本等,这里就不多赘述。
2.5.1.4 控制焦点
可以使用 FocusNode 和 FocusScopeNode 来控制焦点。默认情况下是由 FocusScope 来管理,可以在这个范围内通过 FocusScopeNode 在输入框之间移动焦点、设置默认焦点。
我们可以通过下面代码来获取当前 Widget 树中默认的 FocusScopeNode:
focusScopeNode = FocusScope.of(context)
拿到句柄后,可以使用下面代码来获取焦点:
focusScopeNode.requestFocus(focusNode);
其中 focucsNode 是为 TextField 创建的 FocusNode, 这个操作可以让该 TextField 获取焦点。 调用 focusNode.unfocus() 可以取消焦点。
2.5.1.5 监听焦点状态改变事件
通过 FocusNode 可以监听焦点改变的事件:
focusNode.addListener((){
print(focusNode.hasFocus);
})
true为获取焦点,false为失去焦点
2.5.2 表单
表单Form 对输入框进行分组和统一操作。 就像 Android 的原生组件 RadioGroup 之于 RadioButton 一样, Form 可以管理内容校验、输入框重置等。
Form 继承自 StatefulWidget ,其状态管理在 FormState 里面,来看看 From 的定义:
class Form extends StatefulWidget {
const Form({
Key? key,
required this.child,
@Deprecated(
'Use autovalidateMode parameter which provides more specific '
'behavior related to auto validation. '
'This feature was deprecated after v1.19.0.',
)
this.autovalidate = false,
this.onWillPop,
this.onChanged,
AutovalidateMode? autovalidateMode,
})
...
autovalidate 是否自动校验输入内容,当为true时,每一个 FormField 内容发生变化时都会校验合法性,并直接显示错误信息,否则就需要通过调用 FormState.validate() 来手动校验 v1.19 已经废弃了,改成使用 AutovalidateMode autovalidateMode 自动校验模式,是上面的替换,它有三个枚举值: ①disable :当 FormField 内容改变时不做校验 ②always :即使用户没有用户交互也要校验合法性 ③onUserInteraction :只有在用户交互时才会去校验合法性onWillPop 决定 Form 所在的路由是否可以直接返回。该回调返回一个 Future 对象,如果 Future 的最终结果是 false,则当前路由不会返回,如果为 true,则会返回到上一个路由。 这个属性通常是用于拦截返回按钮的onChanged Form 的任意一个 FormField 内容发生改变时就会调用该方法
2.5.2.1 FormField
Form 的子孙元素是 FormField 类型,FormField 是一个抽象类,定义了几个属性, FormState 内部通过他们来完成操作, FormField 部分定义如下:
const FormField({
Key? key,
required this.builder,
this.onSaved,
this.validator,
this.initialValue,
@Deprecated(
'Use autovalidateMode parameter which provides more specific '
'behavior related to auto validation. '
'This feature was deprecated after v1.19.0.',
)
this.autovalidate = false,
this.enabled = true,
AutovalidateMode? autovalidateMode,
this.restorationId,
})
onSaved 保存时的回调validator 验证合法性的回调initValue 初始值
为了方便使用, Flutter 提供了一个 TextFormFild 组件,继承自 FormField 类,还包装了 TextFileld ,可以直接当成 Form 的 FormField 来使用, 相当于用 Form 来管理 TextField
2.5.2.2 FormState
Form 表单的状态类就是 FormState , 可以通过 Form.of 或者 GlobalKey 获得,通过获得它来对 Form 的子孙 FormField 进行统一操作。
FormState 常用的三个方法:
FormState.validate() :调用此方法后, 会调用 Form 子孙 FormField.validate() 回调,如果有一个检验失败,那么会返回 false,这样所有校验失败的 Widget 都会给出错误提示FormState.save() :调用此方法后,会调用 子孙的 FormFild.save() 回调,用于保存表单内容FormState.reset() : 会将子孙 FormField 的内容清空
2.5.2.3 示例
我们做一个用户登录的程序,再点击登录前需要做到输入检查:
- 用户名不能为空,如果为空则提示“用户名不能为空”
- 密码不能小于6位,如果小于6位则提示 “密码不能少于6位”
代码如下:
import 'package:flutter/material.dart';
class FormTestRoute extends StatefulWidget {
const FormTestRoute({Key? key}) : super(key: key);
@override
State<StatefulWidget> createState() => _FormTestRouteState();
}
class _FormTestRouteState extends State<FormTestRoute> {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final GlobalKey _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Form demo'),
),
body: Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(
children: [
TextFormField(
autofocus: true,
controller: _usernameController,
decoration: const InputDecoration(
labelText: "username",
hintText: "username or email",
icon: Icon(Icons.person)),
validator: (username) {
return username!.trim().isNotEmpty
? null
: "username cannot empty";
},
),
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(
labelText: "password",
hintText: "please input your password",
icon: Icon(Icons.lock)),
obscureText: true,
validator: (pwd) {
return pwd!.trim().length >= 6
? null
: "password digit cannot less than 6!";
},
),
Padding(
padding: const EdgeInsets.only(top: 28.0),
child: Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
if ((_formKey.currentState as FormState).validate()) {
print("Loing success");
}
},
child: const Padding(
padding: EdgeInsets.all(16.0),
child: Text("Login"),
),
))
],
),
)
],
)));
}
}
效果如下图所示:
参考文章
官方学习文档 Basics Widgets
|