You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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],
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

关键修改点说明

  1. 添加FocusNode监听器:在initState中给_textFocus添加监听,焦点变化时调用setState更新UI,确保Visibility能正确响应焦点状态。
  2. 复用TextEditingController:编辑状态下不再新建控制器,直接给_textyController赋值,保证TextField和FocusNode绑定正常,焦点状态准确。
  3. 编辑时自动获取焦点:点击编辑按钮后,通过addPostFrameCallback延迟请求焦点,确保控制器赋值完成后再获取焦点。
  4. 提交/点击外部时处理焦点:提交任务后主动让TextField失去焦点,点击外部时也根据状态处理焦点,确保单选按钮能及时隐藏。

内容的提问来源于stack exchange,提问作者Hans Ramzan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 01:22:00