处理用户输入
现在您已经了解了如何在 Flutter 应用中管理状态,那么如何让用户与您的应用交互并更改其状态呢?
处理用户输入简介
#作为一个多平台 UI 框架,用户与 Flutter 应用交互的方式有很多种。本节中的资源将向您介绍一些常用的小部件,这些小部件用于启用应用内的用户交互。
参考:
接下来,我们将介绍一些支持在 Flutter 应用中处理用户输入的常见用例的 Material 小部件。
按钮
#按钮允许用户通过点击或轻触来启动 UI 中的操作。Material 库提供了各种类型的按钮,它们的功能相似,但样式不同,适用于各种用例,包括:
ElevatedButton
:带有一定深度的按钮。使用凸起按钮可以为原本大部分平面的布局添加维度。FilledButton
:填充按钮,应用于重要的最终操作,以完成流程,例如 保存 、立即加入 或 确认。Tonal Button
:FilledButton
和OutlinedButton
之间的中间地带按钮。它们在需要比轮廓更高的优先级按钮的上下文中非常有用,例如 下一步 。OutlinedButton
:带有文本和可见边框的按钮。这些按钮包含重要的操作,但不是应用中的主要操作。TextButton
:可点击的文本,没有边框。由于文本按钮没有可见的边框,因此它们必须依赖于相对于其他内容的位置来确定上下文。IconButton
:带有图标的按钮。FloatingActionButton
:悬停在内容上以促进主要操作的图标按钮。
视频:
构建按钮通常有三个主要方面:样式、回调及其子项,如下面的 ElevatedButton
代码示例所示:
按钮的回调函数
onPressed
决定点击按钮时会发生什么,因此,此函数是您更新应用状态的地方。如果回调为null
,则按钮被禁用,并且用户按下按钮时不会发生任何事情。按钮的
child
显示在按钮的内容区域内,通常是文本或图标,指示按钮的目的。最后,按钮的
style
控制其外观:颜色、边框等等。
int count = 0;
@override
Widget build(BuildContext context) {
return ElevatedButton(
style: ElevatedButton.styleFrom(
textStyle: const TextStyle(fontSize: 20),
),
onPressed: () {
setState(() {
count += 1;
});
},
child: const Text('Enabled'),
);
}
![带有文本“启用”的 ElevatedButton 的 GIF](/assets/images/docs/fwe/user-input/ElevatedButton.gif)
检查点: 完成本教程,学习如何构建“收藏”按钮:
ElevatedButton
• FilledButton
• OutlinedButton
• TextButton
• IconButton
• FloatingActionButton
文本
#几个小部件支持文本输入。
SelectableText
#Flutter 的 Text
小部件在屏幕上显示文本,但不允许用户突出显示或复制文本。SelectableText
显示用户可以选择的一系列文本。
@override
Widget build(BuildContext context) {
return const SelectableText('''
两个家庭,地位相同,
在美丽的维罗纳,我们设置场景,
从古老的怨恨爆发为新的叛乱,
内战的鲜血使公民的手变得不洁。
从这两个敌人的致命腰部开始''');
}
![一个光标突出显示段落中两行文本的 GIF。](/assets/images/docs/fwe/user-input/SelectableText.gif)
视频:
RichText
#RichText
允许您在应用中显示富文本字符串。TextSpan
与 RichText
类似,允许您以不同的文本样式显示文本的部分。它不用于处理用户输入,但如果您允许用户编辑和格式化文本,则非常有用。
@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
text: 'Hello ',
style: DefaultTextStyle.of(context).style,
children: const <TextSpan>[
TextSpan(text: 'bold', style: TextStyle(fontWeight: FontWeight.bold)),
TextSpan(text: ' world!'),
],
),
);
}
![“Hello bold world!”文本的屏幕截图,“bold”一词以粗体显示。](/assets/images/docs/fwe/user-input/RichText.png)
视频:
演示:
代码:
TextField
#TextField
允许用户使用硬件或屏幕键盘在文本框中输入文本。
TextField
具有许多不同的属性和配置。一些亮点:
InputDecoration
确定文本字段的外观,例如颜色和边框。controller
:TextEditingController
控制正在编辑的文本。为什么您可能需要一个控制器?默认情况下,您的应用用户可以在文本字段中键入文本,但如果您想以编程方式控制TextField
并清除其值(例如),则需要TextEditingController
。onChanged
:当用户更改文本字段的值时(例如插入或删除文本时),此回调函数会触发。onSubmitted
:当用户指示他们已完成对字段中文本的编辑时,此回调将被触发;例如,当文本字段处于焦点时,点击“Enter”键。
此类支持其他可配置属性,例如 obscureText
,它会将其输入的每个字母都转换为 readOnly
圆圈,以及 readOnly
,它可以防止用户更改文本。
final TextEditingController _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: 'Mascot Name',
),
);
}
![一个带有标签“吉祥物名称”的文本字段的 GIF,紫色焦点边框和正在输入的短语“Dash the hummingbird”。](/assets/images/docs/fwe/user-input/TextField.gif)
检查点: 完成这个包含 4 个部分的 cookbook 系列教程,该教程将引导您逐步创建文本字段、检索其值并更新您的应用状态:
表单
#Form
是一个可选容器,用于将多个表单字段小部件(例如 TextField
)组合在一起。
每个单独的表单字段都应包装在一个 FormField
小部件中,并以 Form
小部件作为公共祖先。存在预先将表单字段小部件包装在 FormField
中的便捷小部件。 例如,TextField
的 Form
小部件版本是 TextFormField
。
使用 Form
可以访问 FormState
,它允许您保存、重置和验证从此 Form
派生的每个 FormField
。您还可以提供 GlobalKey
来标识特定表单,如下面的代码所示:
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
TextFormField(
decoration: const InputDecoration(
hintText: 'Enter your email',
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
return null;
},
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: ElevatedButton(
onPressed: () {
// Validate returns true if the form is valid, or false otherwise.
if (_formKey.currentState!.validate()) {
// Process data.
}
},
child: const Text('Submit'),
),
),
],
),
);
}
检查点: 完成本教程,学习如何[构建带有验证的表单][]。
演示:
代码:
TextField
• RichText
• SelectableText
• Form
从一组选项中选择一个值
#为用户提供从多个选项中选择的方法。
SegmentedButton
#SegmentedButton
允许用户从最少 2-5 个项目组中进行选择。
数据类型 <T>
可以是内置类型,例如 int
、String
、bool
或枚举。SegmentedButton
有一些相关的属性:
segments
,一个ButtonSegment
列表,其中每个表示用户可以选择的一个“段”或选项。在视觉上,每个ButtonSegment
可以具有图标、文本标签或两者兼有。multiSelectionEnabled
指示用户是否可以选中多个选项。此属性默认为 false。selected
标识当前选定的值。注意:selected
的类型为Set<T>
,因此,如果您只允许用户选择一个值,则必须将该值作为包含单个元素的Set
提供。当用户选择任何段时,
onSelectionChanged
回调将被触发。它提供所选段的列表,以便您可以更新应用程序状态。其他样式参数允许您修改按钮的外观。例如,
style
获取ButtonStyle
,提供了一种配置selectedIcon
的方法。
enum Calendar { day, week, month, year }
// StatefulWidget...
Calendar calendarView = Calendar.day;
@override
Widget build(BuildContext context) {
return SegmentedButton<Calendar>(
segments: const <ButtonSegment<Calendar>>[
ButtonSegment<Calendar>(
value: Calendar.day,
label: Text('Day'),
icon: Icon(Icons.calendar_view_day)),
ButtonSegment<Calendar>(
value: Calendar.week,
label: Text('Week'),
icon: Icon(Icons.calendar_view_week)),
ButtonSegment<Calendar>(
value: Calendar.month,
label: Text('Month'),
icon: Icon(Icons.calendar_view_month)),
ButtonSegment<Calendar>(
value: Calendar.year,
label: Text('Year'),
icon: Icon(Icons.calendar_today)),
],
selected: <Calendar>{calendarView},
onSelectionChanged: (Set<Calendar> newSelection) {
setState(() {
// 默认情况下,一次只能选择一个段,因此其值始终是第一个
calendarView = newSelection.first;
});
},
);
}
![一个 SegmentedButton 的 GIF,它有 4 个段:日、周、月和年。每个都有一个日历图标来表示其值和文本标签。首先选中日,然后是周和月,最后是年。](/assets/images/docs/fwe/user-input/segmented-button.gif)
Chip
#Chip
是以简洁的方式表示特定上下文中的属性、文本、实体或操作的一种方法。针对特定用例存在专门的 Chip
小部件:
- InputChip 以简洁的形式表示复杂的信息,例如实体(人、地点或事物)或会话文本。
- ChoiceChip 允许从一组选项中进行单选。Choice chip 包含相关的描述性文本或类别。
- FilterChip 使用标签或描述性词语来过滤内容。
- ActionChip 表示与主要内容相关的操作。
每个 Chip
小部件都需要一个 label
。它可以选择具有 avatar
(例如图标或用户的个人资料图片)和 onDeleted
回调,该回调显示一个删除图标,当触发时,会删除该 chip。Chip
小部件的外观也可以通过设置许多可选参数(例如 shape
、color
和 iconTheme
)来自定义。
您通常会使用 Wrap
(一个以多行水平或垂直方式显示其子项的小部件)来确保您的 chip 包装起来并且不会被应用的边缘切断。
@override
Widget build(BuildContext context) {
return const SizedBox(
width: 500,
child: Wrap(
alignment: WrapAlignment.center,
spacing: 8,
runSpacing: 4,
children: [
Chip(
avatar: CircleAvatar(
backgroundImage: AssetImage('assets/images/dash_chef.png')),
label: Text('Chef Dash'),
),
Chip(
avatar: CircleAvatar(
backgroundImage:
AssetImage('assets/images/dash_firefighter.png')),
label: Text('Firefighter Dash'),
),
Chip(
avatar: CircleAvatar(
backgroundImage: AssetImage('assets/images/dash_musician.png')),
label: Text('Musician Dash'),
),
Chip(
avatar: CircleAvatar(
backgroundImage: AssetImage('assets/images/dash_artist.png')),
label: Text('Artist Dash'),
),
],
),
);
}
![一个屏幕截图,显示 4 个 Chip 分成两行,每个 Chip 都有一个圆形的引导个人资料图像和内容文本。](/assets/images/docs/fwe/user-input/chip.png)
DropdownMenu
#DropdownMenu
允许用户从选项菜单中选择一个选项并将所选文本放入 TextField
中。它还允许用户根据文本输入过滤菜单项。
配置参数包括以下内容:
dropdownMenuEntries
提供一个DropdownMenuEntry
列表,用于描述每个菜单项。菜单可能包含文本标签以及引导或尾随图标等信息。(这也是唯一必需的参数。)TextEditingController
允许以编程方式控制TextField
。- 当用户选择一个选项时,
onSelected
回调将被触发。 initialSelection
允许您配置默认值。- 还提供其他参数来定制小部件的外观和行为。
enum ColorLabel {
blue('Blue', Colors.blue),
pink('Pink', Colors.pink),
green('Green', Colors.green),
yellow('Orange', Colors.orange),
grey('Grey', Colors.grey);
const ColorLabel(this.label, this.color);
final String label;
final Color color;
}
// StatefulWidget...
@override
Widget build(BuildContext context) {
return DropdownMenu<ColorLabel>(
initialSelection: ColorLabel.green,
controller: colorController,
// requestFocusOnTap is enabled/disabled by platforms when it is null.
// On mobile platforms, this is false by default. Setting this to true will
// trigger focus request on the text field and virtual keyboard will appear
// afterward. On desktop platforms however, this defaults to true.
requestFocusOnTap: true,
label: const Text('Color'),
onSelected: (ColorLabel? color) {
setState(() {
selectedColor = color;
});
},
dropdownMenuEntries: ColorLabel.values
.map<DropdownMenuEntry<ColorLabel>>(
(ColorLabel color) {
return DropdownMenuEntry<ColorLabel>(
value: color,
label: color.label,
enabled: color.label != 'Grey',
style: MenuItemButton.styleFrom(
foregroundColor: color.color,
),
);
}).toList(),
);
}
![一个选定的 DropdownMenu 小部件的 GIF,它显示 5 个选项:蓝色、粉色、绿色、橙色和灰色。选项文本以其值的颜色显示。](/assets/images/docs/fwe/user-input/dropdownmenu.gif)
视频:
Slider
#Slider
小部件允许用户通过移动指示器(例如音量条)来调整值。
Slider
小部件的配置参数:
value
表示滑块的当前值onChanged
是在移动滑块手柄时触发的回调min
和max
建立滑块允许的最小值和最大值divisions
建立用户可以沿轨道移动手柄的离散间隔。
double _currentVolume = 1;
@override
Widget build(BuildContext context) {
return Slider(
value: _currentVolume,
max: 5,
divisions: 5,
label: _currentVolume.toString(),
onChanged: (double value) {
setState(() {
_currentVolume = value;
});
},
);
}
![一个滑块的 GIF,其旋钮以 1 为增量从左向右拖动,从 0.0 到 5.0](/assets/images/docs/fwe/user-input/slider.gif)
视频:
SegmentedButton
• DropdownMenu
• Slider
• Chip
在值之间切换
#您的 UI 可以通过多种方式允许在值之间切换。
复选框、开关和单选按钮
#提供一个选项来打开和关闭单个值。这些小部件背后的功能逻辑相同,因为所有三个都是基于 ToggleableStateMixin
构建的,尽管每个都提供了细微的演示差异:
Checkbox
是一个容器,当为 false 时为空,当为 true 时填充复选标记。Switch
有一个手柄,当为 false 时在左边,当为 true 时滑动到右边。Radio
与Checkbox
类似,它是一个容器,当为 false 时为空,但当为 true 时填充。
Checkbox
和 Switch
的配置包含:
- 一个值为
true
或false
的value
- 一个
onChanged
回调,当用户切换小部件时触发
复选框
#bool isChecked = false;
@override
Widget build(BuildContext context) {
return Checkbox(
checkColor: Colors.white,
value: isChecked,
onChanged: (bool? value) {
setState(() {
isChecked = value!;
});
},
);
}
![一个 GIF,显示一个指针单击一个复选框,然后再次单击以取消选中它。](/assets/images/docs/fwe/user-input/checkbox.gif)
开关
#bool light = true;
@override
Widget build(BuildContext context) {
return Switch(
// 此布尔值切换开关。
value: light,
activeColor: Colors.red,
onChanged: (bool value) {
// 当用户切换开关时调用此函数。
setState(() {
light = value;
});
},
);
}
![一个 Switch 小部件的 GIF,它被打开和关闭。在其关闭状态下,它是灰色的,带有深灰色边框。在其打开状态下,它是红色的,带有浅红色边框。](/assets/images/docs/fwe/user-input/Switch.gif)
单选按钮
#一组 Radio
按钮,允许用户在互斥值之间进行选择。当用户选择组中的一个单选按钮时,其他单选按钮将被取消选择。
- 特定
Radio
按钮的value
表示该按钮的值, - 一组单选按钮的选定值由
groupValue
参数标识。 Radio
还具有一个onChanged
回调,当用户单击它时会触发,就像Switch
和Checkbox
一样
enum Character { musician, chef, firefighter, artist }
class RadioExample extends StatefulWidget {
const RadioExample({super.key});
@override
State<RadioExample> createState() => _RadioExampleState();
}
class _RadioExampleState extends State<RadioExample> {
Character? _character = Character.musician;
void setCharacter(Character? value) {
setState(() {
_character = value;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ListTile(
title: const Text('Musician'),
leading: Radio<Character>(
value: Character.musician,
groupValue: _character,
onChanged: setCharacter,
),
),
ListTile(
title: const Text('Chef'),
leading: Radio<Character>(
value: Character.chef,
groupValue: _character,
onChanged: setCharacter,
),
),
ListTile(
title: const Text('Firefighter'),
leading: Radio<Character>(
value: Character.firefighter,
groupValue: _character,
onChanged: setCharacter,
),
),
ListTile(
title: const Text('Artist'),
leading: Radio<Character>(
value: Character.artist,
groupValue: _character,
onChanged: setCharacter,
),
),
],
);
}
}
![一个 GIF,显示一列 4 个 ListTile,每个 ListTile 包含一个引导单选按钮和标题文本。单选按钮从上到下按顺序选择。](/assets/images/docs/fwe/user-input/Radio.gif)
附加内容:CheckboxListTile & SwitchListTile
#这些便捷小部件与复选框和小部件相同,但支持标签(作为 ListTile
)。
double timeDilation = 1.0;
bool _lights = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
CheckboxListTile(
title: const Text('Animate Slowly'),
value: timeDilation != 1.0,
onChanged: (bool? value) {
setState(() {
timeDilation = value! ? 10.0 : 1.0;
});
},
secondary: const Icon(Icons.hourglass_empty),
),
SwitchListTile(
title: const Text('Lights'),
value: _lights,
onChanged: (bool value) {
setState(() {
_lights = value;
});
},
secondary: const Icon(Icons.lightbulb_outline),
),
],
);
}
![一个 ListTile,包含一个引导图标、标题文本和一个正在选中和取消选中的尾随复选框。它还显示一个 ListTile,包含一个引导图标、标题文本和一个正在打开和关闭的开关。](/assets/images/docs/fwe/user-input/SpecialListTiles.gif)
视频:
视频:
Checkbox
• CheckboxListTile
• Switch
• SwitchListTile
• Radio
选择日期或时间
#提供小部件,以便用户可以选择日期和时间。
有一组对话框允许用户选择日期或时间,您将在以下部分看到。除了不同的日期类型——日期为 DateTime
,时间为 TimeOfDay
之外——这些对话框的功能相似,您可以通过提供以下内容来配置它们:
- 默认的
initialDate
或initialTime
- 或确定显示的选取器 UI 的
initialEntryMode
。
DatePickerDialog
#此对话框允许用户选择一个日期或一系列日期。通过调用 showDatePicker
函数激活,该函数返回 Future<DateTime>
,因此不要忘记等待异步函数调用!
DateTime? selectedDate;
@override
Widget build(BuildContext context) {
var date = selectedDate;
return Column(children: [
Text(
date == null
? "You haven't picked a date yet."
: DateFormat('MM-dd-yyyy').format(date),
),
ElevatedButton.icon(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
var pickedDate = await showDatePicker(
context: context,
initialEntryMode: DatePickerEntryMode.calendarOnly,
initialDate: DateTime.now(),
firstDate: DateTime(2019),
lastDate: DateTime(2050),
);
setState(() {
selectedDate = pickedDate;
});
},
label: const Text('Pick a date'),
)
]);
}
![一个指针单击一个显示“选择日期”的按钮的 GIF,然后显示一个日期选择器。选中 8 月 30 日(星期五)的日期,然后单击“确定”按钮。](/assets/images/docs/fwe/user-input/DatePicker.gif)
TimePickerDialog
#TimePickerDialog
是一个显示时间选择器的对话框。可以通过调用 showTimePicker()
函数激活它。showTimePicker
返回 Future<TimeOfDay>
,而不是返回 Future<DateTime>
。同样,不要忘记等待函数调用!
TimeOfDay? selectedTime;
@override
Widget build(BuildContext context) {
var time = selectedTime;
return Column(children: [
Text(
time == null ? "You haven't picked a time yet." : time.format(context),
),
ElevatedButton.icon(
icon: const Icon(Icons.calendar_today),
onPressed: () async {
var pickedTime = await showTimePicker(
context: context,
initialEntryMode: TimePickerEntryMode.dial,
initialTime: TimeOfDay.now(),
);
setState(() {
selectedTime = pickedTime;
});
},
label: const Text('Pick a date'),
)
]);
}
![一个指针单击一个显示“选择时间”的按钮的 GIF,然后显示一个时间选择器。时间选择器显示一个圆形时钟,当光标移动时针、分针、选择 PM,然后单击“确定”按钮。](/assets/images/docs/fwe/user-input/TimePicker.gif)
showDatePicker
• showTimePicker
滑动和滑动
#Dismissible
是一个小部件,它允许用户通过滑动来将其关闭。
它有很多配置参数,包括:
- 一个
child
小部件 - 当用户滑动时触发的
onDismissed
回调 - 样式参数,例如
background
- 同样重要的是要包含一个
key
对象,以便可以从窗口小部件树中同级Dismissible
小部件唯一地识别它们。
List<int> items = List<int>.generate(100, (int index) => index);
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: items.length,
padding: const EdgeInsets.symmetric(vertical: 16),
itemBuilder: (BuildContext context, int index) {
return Dismissible(
background: Container(
color: Colors.green,
),
key: ValueKey<int>(items[index]),
onDismissed: (DismissDirection direction) {
setState(() {
items.removeAt(index);
});
},
child: ListTile(
title: Text(
'Item ${items[index]}',
),
),
);
},
);
}
![三个小部件的屏幕截图,彼此均匀间隔。](/assets/images/docs/fwe/user-input/Dismissible.gif)
视频:
检查点: 完成本教程,学习如何使用 dismissible 小部件[实现滑动以关闭][]。
API 文档:
寻找更多小部件?
#此页面仅介绍了可在 Flutter 应用中用于处理用户输入的一些常用 Material 小部件。查看[Material 小部件库][]和Material 库 API 文档以获取小部件的完整列表。
Material 3 演示,了解 Material 库中提供的精选用户输入小部件示例。
演示: 查看 Flutter 的
如果 Material 和 Cupertino 库没有满足您需求的小部件,请查看 pub.dev 以查找 Flutter 和 Dart 社区拥有和维护的包。例如,flutter_slidable
包提供了一个 Slidable
小部件,它比上一节中描述的 Dismissible
小部件更具可定制性。
视频:
使用 GestureDetector 构建交互式小部件
#您是否已经仔细检查了小部件库、pub.dev,询问了您的编码朋友,仍然找不到符合您正在寻找的用户交互的小部件?您可以构建自己的自定义小部件,并使用 GestureDetector
使其具有交互性。
处理点击。
检查点: 使用此配方作为起点,创建您自己的自定义按钮小部件,该小部件可以
视频:
点击、拖动和其他手势,它解释了如何在 Flutter 中侦听和响应手势。
参考: 查看
附加视频: 好奇 Flutter 的
GestureArena
如何将原始用户交互数据转换为人类可识别的概念,如点击、拖动和捏合?查看此视频:GestureArena(解码 Flutter)
不要忘记辅助功能!
#如果您正在构建自定义小部件,请使用 Semantics
小部件对其含义进行注释。它为屏幕阅读器和其他基于语义分析的工具提供描述和元数据。
视频:
API 文档:
测试
#完成在应用中构建用户交互后,不要忘记编写测试以确保一切按预期工作!
这些教程将引导您编写模拟应用中用户交互的测试:
检查点: 按照此[点击、拖动和输入文本][] cookbook 文章,学习如何使用
WidgetTester
模拟和测试应用中的用户交互。
附加教程: [处理滚动][] cookbook 配方向您展示了如何通过使用窗口小部件测试滚动列表来验证窗口小部件列表是否包含预期内容。
下一步:网络
#此页面介绍了如何处理用户输入。现在您已经了解了如何处理来自应用程序用户的输入,您可以通过添加外部数据来使您的应用程序更有趣。在下一节中,您将学习如何通过网络为您的应用程序获取数据,如何将数据在 JSON 之间进行转换,身份验证以及其他网络功能。
反馈
#由于本网站的此部分正在不断发展,我们欢迎您的反馈!
除非另有说明,否则本网站上的文档反映的是 Flutter 的最新稳定版本。页面最后更新于 2025-01-30。 查看源代码 或 报告问题。