dart - FocusScope 被 Navigator 搞砸了
问题描述
我正在编写一个简单的提醒应用程序,它本质上是一个ListView
of TextField
s,当模糊或提交时,它会更新数据库。我使用一堆GestureDetector
s 和FocusNode
s 来模糊TextField
用户点击复选框或TextField
.
当这是唯一的路线时,它工作得很好。但是,当我在现有页面上推送相同的页面时,焦点行为变得完全错误并且应用程序无法使用。
这是一个演示视频:https ://www.youtube.com/watch?v=13E9LY8yD3A
我的代码基本上是这样的:
/// main.dart
class MyApp extends StatelessWidget {
static FocusScopeNode rootScope; // just for debug
@override
Widget build(BuildContext context) {
rootScope = FocusScope.of(context);
return MaterialApp(home: ReminderPage());
}
}
-
/// reminder_page.dart
class ReminderPage extends StatelessWidget {
final _blurNode = FocusNode();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Remind'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
// Push new identical page.
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ReminderPage(),
));
},
),
],
),
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('reminders').snapshots(),
builder: (context, snapshot) {
return _buildBody(context, snapshot.data);
},
),
);
}
Widget _buildBody(BuildContext context, QuerySnapshot data) {
List<Reminder> reminders =
data.documents.map((s) => Reminder.fromSnapshot(s)).toList();
return GestureDetector(
onTap: () {
_blur(context);
},
child: ListView(
children: reminders.map((r) => ReminderCard(r)).toList(),
),
);
}
void _blur(context) {
FocusScope.of(context).requestFocus(_blurNode);
}
}
-
/// reminder_card.dart
class ReminderCard extends StatelessWidget {
final Reminder reminder;
final TextEditingController _controller;
final _focusNode = FocusNode();
final _blurNode = FocusNode();
ReminderCard(this.reminder)
: _controller = TextEditingController(text: reminder.text) {
_focusNode.addListener(() {
if (!_focusNode.hasFocus) {
reminder.updateText(_controller.text); // update database
}
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
_blur(context);
},
child: Row(
children: <Widget>[
_buildCheckBox(context),
_buildTextField(context),
],
),
);
}
Widget _buildCheckBox(context) {
return Checkbox(
value: reminder.done,
onChanged: (done) {
print(MyApp.rootScope.toStringDeep()); // print Focus tree
_blur(context);
reminder.updateDone(done); // update database
},
);
}
Widget _buildTextField(context) {
return TextField(
onSubmitted: reminder.updateText, // update database
focusNode: _focusNode,
);
}
void _blur(context) {
FocusScope.of(context).requestFocus(_blurNode);
}
}
我发现这个问题听起来非常相似,但我不明白自定义转换如何解决任何问题并且与焦点有关。和 OP 一样,我尝试了很多不同的东西来搞乱FocusScope
,包括 call detach()
,或者一直向下reparentIfNeeded()
传递根目录,所以每次都不会创建一个新的,但这些都没有提供任何接近工作的东西。而且我还尝试了自定义过渡,但无济于事。FocusScope
FocusScope
调试输出在第一条路线上显示了这一点(当我选中复选框时):
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#f07c7(FOCUSED)
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#f138f(FOCUSED)
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): └─child 1: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#e68b3(FOCUSED)
这在第二条路线上:
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): ├─child 1: FocusScopeNode#a1008
I/flutter (28362): └─child 2: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#a76e6
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): ├─child 1: FocusScopeNode#a1008
I/flutter (28362): │ focus: FocusNode#02ebf(FOCUSED)
I/flutter (28362): │
I/flutter (28362): └─child 2: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#a76e6
I/flutter (28362): FocusScopeNode#68466
I/flutter (28362): └─child 1: FocusScopeNode#5b855
I/flutter (28362): ├─child 1: FocusScopeNode#a1008
I/flutter (28362): │ focus: FocusNode#917da(FOCUSED)
I/flutter (28362): │
I/flutter (28362): └─child 2: FocusScopeNode#76ef6
I/flutter (28362): focus: FocusNode#a76e6
因此,当我们推送第二条路线时,第一条路线的 FocusScope 似乎变成了子 2,这对我来说听起来是正确的。
我究竟做错了什么?
解决方案
感谢卢卡斯上面的评论和这个其他的问题,我能够解决这个问题。
首先,我减少了FocusNode
s 的数量:只有一个 per TextField
,一个用于 parent ReminderPage
。父级现在有一个blur()
使所有TextField
s 不聚焦的功能;这样,当我TextField
在编辑另一个时单击复选框时,正在编辑的复选框没有焦点。
其次,我更改了我的reminder.updateText()
函数(此处未显示),因此它仅在文本与现有文本不同时更新数据库。否则,我们将重建卡片,因为StreamBuilder
,弄乱了TextField
正在编辑的焦点。
第三,我现在正在听TextEditingController
而不是对FocusNode
数据库进行更改。但是我仍然只在FocusNode
没有焦点时更新数据库,否则StreamBuilder
会重建页面并再次混淆焦点。
但这仍然不能解释为什么当它ReminderPage
是应用程序的主页时它工作得相当好,而不是当它被推送到路由顶部时。答案来自另一个遇到相同问题的 SO 问题:当放置在启动屏幕之后时,小部件会不断重建,但在用作应用程序主页时不会。我仍然不明白为什么这有什么不同,但同样的修复对我有用:将其更改为 StatefulWidget 并且仅在实际发生更改时重建。
最终代码如下所示。// --->
我用评论突出显示了差异。
/// main.dart
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(home: ReminderPage());
}
}
-
/// reminder_page.dart
class ReminderPage extends StatelessWidget {
final _blurNode = FocusNode();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Remind'),
actions: <Widget>[
IconButton(
icon: Icon(Icons.add),
onPressed: () {
// Push new identical page.
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ReminderPage(),
));
},
),
],
),
body: StreamBuilder<QuerySnapshot>(
stream: Firestore.instance.collection('reminders').snapshots(),
builder: (context, snapshot) {
return _buildBody(context, snapshot.data);
},
),
);
}
Widget _buildBody(BuildContext context, QuerySnapshot data) {
List<Reminder> reminders =
data.documents.map((s) => Reminder.fromSnapshot(s)).toList();
return GestureDetector(
onTap: () {
// ---> Blur all TextFields when clicking in the background.
blur(context);
},
child: ListView(
// ---> Passing the parent to each child so they can call parent.blur()
children: reminders.map((r) => ReminderCard(r, this)).toList(),
),
);
}
// ---> This will unfocus all TextFields.
void blur(context) {
FocusScope.of(context).requestFocus(_blurNode);
}
}
-
/// reminder_card.dart
// ---> Converted to a StatefulWidget! That way we can save a snapshot of reminder
// as it was when we last built the widget, and only rebuild it if it changed.
class ReminderCard extends StatefulWidget {
final Reminder reminder;
final TextEditingController _controller;
// ---> Only one focus node, for the TextField.
final _focusNode = FocusNode();
// ---> The parent.
final ReminderPage page;
ReminderCard(this.reminder, this.page)
: _controller = TextEditingController(text: reminder.text) {
// ---> Listen to text changes. But only updating the database
// if the TextField is unfocused.
_controller.addListener(() {
if (!_focusNode.hasFocus) {
reminder.updateText(_controller.text); // update database
}
});
}
@override
ReminderCardState createState() => ReminderCardState();
}
class ReminderCardState extends State<ReminderCard> {
Widget card;
Reminder snapshotWhenLastBuilt;
@override
Widget build(BuildContext context) {
// ---> Only rebuild if something changed, otherwise return the
// card built previously.
// The equals() function is a method of the Reminder class that just tests a
// few fields.
if (card == null || !widget.reminder.equals(snapshotWhenLastBuilt)) {
card = _buildCard(context);
snapshotWhenLastBuilt = widget.reminder;
}
return card;
}
Widget _buildCard(context) {
return GestureDetector(
onTap: () {
// ---> Blur all TextFields when clicking in the background.
widget.page.blur(context);
},
child: Row(
children: <Widget>[
_buildCheckBox(context),
_buildTextField(context),
],
),
);
}
Widget _buildCheckBox(context) {
return Checkbox(
value: widget.reminder.done,
onChanged: (done) {
// ---> Blur all TextFields when clicking on a checkbox.
widget.page.blur(context);
widget.reminder.updateDone(done); // update database
},
);
}
Widget _buildTextField(context) {
return TextField(
focusNode: widget._focusNode,
controller: widget._controller,
);
}
}
推荐阅读
- python - 有没有办法使用 Python 自动校准 OpenCV 中的 detectMultiscale() 中的比例和邻居?
- php - 函数没有被回调给登录用户
- json - 如何从 JSON 中获取值,其中 JSON 以 RestAssured 中的 Array Block 开头
- reactjs - 我想使用 D3 和 Chartjs 在 React 中绘制前 100 行本地 CSV
- mysql-workbench - MySQL Workbench 8.0 CE:获取上一帧按钮丢失
- excel - 用于打开 Visual Basic 编辑器的 Excel 宏
- amazon-web-services - 我无法弄清楚我在 AWS IAM 中缺少允许编辑防火墙规则的权限
- mysql - 用户'root'@'localhost'的访问被拒绝-在vps上启动一个spring项目
- gradle - Gradle 与远程平面 IVY 存储库的依赖关系:如何摆脱 jar 文件中添加的破折号
- python - 如何在python中从不同长度的列表创建字典