Flutter实现TextField失焦时隐藏单选按钮容器
问题:单选按钮无法随TextField焦点/编辑状态显示隐藏
我希望单选按钮默认处于隐藏状态,仅在TextField获得焦点或处于编辑状态时显示。尝试使用Visibility(visible: _textFocus.hasFocus || isEditing,)实现这个逻辑,但方案无效;把单选按钮行放入Container中也没有效果。以下是我的Flutter代码:
import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; void main() { runApp(MaterialApp( home: const TodoApp(), theme: ThemeData(primarySwatch: Colors.yellow), )); } class Todo { final String text; final bool checked; int priority; static int _tasknumber = 0; final int key; Todo({ required this.text, required this.checked, required this.priority, }) : key = _tasknumber++; } class TodoApp extends StatefulWidget { const TodoApp({super.key}); @override State<TodoApp> createState() => _TodoAppState(); } class _TodoAppState extends State<TodoApp> { final _textyController = TextEditingController(); List<Todo> data = []; final FocusNode _textFocus = FocusNode(); int? _radioValue = 1; bool isEditing = false; int editingIndex = -1; // 初始值为-1,表示当前无编辑任务 // 清除已完成任务 void deleteComplete() { List<Todo> completedTasks = data.where((todo) => todo.checked).toList(); setState(() { data.removeWhere((todo) => todo.checked); }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('已删除${completedTasks.length}条已完成任务'), duration: const Duration(seconds: 4), action: SnackBarAction( label: '撤销', onPressed: () { setState(() { data.insertAll(0, completedTasks); }); }, ), ), ); } // 取消所有任务勾选 void untickAll() { List<Todo> tasksBeforeUntick = List.from(data); setState(() { for (int index = 0; index < data.length; index++) { if (data[index].checked) { data[index] = Todo( text: data[index].text, checked: false, priority: data[index].priority, ); } } }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('已取消所有已完成任务的勾选'), duration: const Duration(seconds: 4), action: SnackBarAction( label: '撤销', onPressed: () { setState(() { data = List.from(tasksBeforeUntick); }); }, ), ), ); } // 删除任务并支持撤销 void deleteTasks(int index, Todo itemToDelete) { setState(() { data.removeAt(index); }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('任务已删除'), duration: const Duration(seconds: 4), action: SnackBarAction( label: '撤销', onPressed: () { setState(() { data.insert(index, itemToDelete); }); }, ), ), ); } @override void initState() { super.initState(); // 新增:监听焦点变化,触发UI更新 _textFocus.addListener(() { setState(() {}); }); } @override void dispose() { _textFocus.dispose(); _textyController.dispose(); super.dispose(); } @override Widget build(context) { data.sort((a, b) { if (a.checked == b.checked) { if (a.priority == b.priority) { return 0; } else if (a.priority < b.priority) { return -1; } else { return 1; } } else if (a.checked) { return 1; // 已完成任务移到下方 } else { return -1; // 未完成任务留在上方 } }); String priorityLabel(int priority) { if (priority == 1) { return '高'; } else if (priority == 2) { return '中'; } else { return '低'; } } Color tileColor(Todo todo) { if (todo.checked) { return Colors.grey.withOpacity(0.2); } else { if (todo.priority == 1) { return Colors.red[300] ?? Colors.red; } else if (todo.priority == 2) { return Colors.orange[300] ?? Colors.yellow; } else { return Colors.yellow[300] ?? Colors.green; } } } return Scaffold( backgroundColor: Colors.yellow[100], appBar: AppBar( title: const Text("待办清单"), centerTitle: true, elevation: 0, ), body: Column( children: [ ElevatedButton( onPressed: deleteComplete, child: const Text('清除已完成'), ), ElevatedButton( onPressed: untickAll, child: const Text('取消所有勾选'), ), Expanded( child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 15.0), child: ReorderableListView.builder( reverse: false, itemCount: data.length, onReorder: (oldIndex, newIndex) { setState(() { if (oldIndex < newIndex) { newIndex -= 1; } final Todo item = data.removeAt(oldIndex); data.insert(newIndex, item); }); }, itemBuilder: (context, index) { if (data.isEmpty) { return const ListTile( title: Text("暂无任务"), ); } else { return Container( key: ValueKey(data[index].key), padding: const EdgeInsets.symmetric(vertical: 10), child: Slidable( endActionPane: ActionPane( motion: const ScrollMotion(), children: [ SlidableAction( backgroundColor: Colors.green, icon: Icons.edit, label: '编辑', onPressed: (context) => { setState(() { isEditing = true; editingIndex = index; // 修改:编辑时直接给现有控制器赋值,而非新建 _textyController.text = data[index].text; _radioValue = data[index].priority; // 编辑时自动获取焦点 WidgetsBinding.instance.addPostFrameCallback((_) { FocusScope.of(context).requestFocus(_textFocus); }); }), }, ), SlidableAction( backgroundColor: Colors.red[400] ?? Colors.red, icon: Icons.delete_forever, label: '删除', onPressed: (context) => { setState(() { if (isEditing && editingIndex >= 0 && editingIndex < data.length) { isEditing = false; _textyController.clear(); _radioValue = 1; } deleteTasks(index, data[index]); }), }, ), ], ), child: Container( decoration: BoxDecoration( color: tileColor(data[index]), ), child: Padding( padding: const EdgeInsets.all(8.0), child: CheckboxListTile( activeColor: Colors.black, controlAffinity: ListTileControlAffinity.leading, title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( data[index].text, style: TextStyle( color: data[index].checked ? Colors.grey.withOpacity(0.6) : Colors.black, decoration: data[index].checked ? TextDecoration.lineThrough : TextDecoration.none), ), ), Text( data[index].checked ? '' : '${index + 1}', style: TextStyle( color: Colors.black.withOpacity(0.1), fontSize: 21, fontWeight: FontWeight.w800), ), ], ), subtitle: Text(priorityLabel((data[index].priority))), value: data[index].checked, onChanged: (newCheckedValue) { setState(() { data[index] = Todo( text: data[index].text, checked: newCheckedValue ?? false, priority: data[index].priority, ); }); }, ), ), )), ); } }, ), ), ), Padding( padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 15), child: Column( children: [ Visibility( visible: _textFocus.hasFocus || isEditing, child: Container( color: Colors.red.withOpacity(0.1), padding: const EdgeInsets.symmetric(vertical: 10), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Column( children: [ Radio( value: 1, groupValue: isEditing ? data[editingIndex].priority : _radioValue, onChanged: (changedRadio) { setState(() { if (isEditing) { data[editingIndex] = Todo( text: _textyController.text, checked: data[editingIndex].checked, priority: changedRadio ?? 1, ); } else { _radioValue = changedRadio; } }); }, ), const Text('高'), ], ), Column( children: [ Radio( value: 2, groupValue: isEditing ? data[editingIndex].priority : _radioValue, onChanged: (changedRadio) { setState(() { if (isEditing) { data[editingIndex] = Todo( text: _textyController.text, checked: data[editingIndex].checked, priority: changedRadio ?? 1, ); } else { _radioValue = changedRadio; } }); }, ), const Text('中'), ], ), Column( children: [ Radio( value: 3, groupValue: isEditing ? data[editingIndex].priority : _radioValue, onChanged: (changedRadio) { setState(() { if (isEditing) { data[editingIndex] = Todo( text: _textyController.text, checked: data[editingIndex].checked, priority: changedRadio ?? 1, ); } else { _radioValue = changedRadio; } }); }, ), const Text('低'), ], ), ], ), ), ), TextField( focusNode: _textFocus, // 修改:始终使用同一个控制器,避免焦点丢失 controller: _textyController, onSubmitted: (value) { setState(() { if (isEditing) { data[editingIndex] = Todo( text: value, checked: data[editingIndex].checked, priority: _radioValue ?? 1, ); isEditing = false; } else { data.insert( 0, Todo( text: value, checked: false, priority: _radioValue ?? 1, )); } _textyController.clear(); // 提交后失去焦点,隐藏单选按钮 _textFocus.unfocus(); }); }, onTapOutside: (event) { // 点击外部时失去焦点,隐藏单选按钮 setState(() { if (!isEditing) { _textFocus.unfocus(); } }); }, decoration: const InputDecoration( labelText: '+ 添加任务', labelStyle: TextStyle( color: Colors.grey, ), border: OutlineInputBorder(), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.black), ), ), cursorColor: Colors.grey[400], ), ], ), ), ], ), ); } }
关键修改点说明
- 添加FocusNode监听器:在
initState中给_textFocus添加监听,焦点变化时调用setState更新UI,确保Visibility能正确响应焦点状态。 - 复用TextEditingController:编辑状态下不再新建控制器,直接给
_textyController赋值,保证TextField和FocusNode绑定正常,焦点状态准确。 - 编辑时自动获取焦点:点击编辑按钮后,通过
addPostFrameCallback延迟请求焦点,确保控制器赋值完成后再获取焦点。 - 提交/点击外部时处理焦点:提交任务后主动让TextField失去焦点,点击外部时也根据状态处理焦点,确保单选按钮能及时隐藏。
内容的提问来源于stack exchange,提问作者Hans Ramzan
相关产品推荐
相关产品推荐

