Last active
January 13, 2025 11:37
-
-
Save YuerLee/3e220a6fc89aa14b4543fd248d9dc0ec to your computer and use it in GitHub Desktop.
CustomCursorTextField
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'package:flutter/material.dart'; | |
import 'package:flutter/services.dart'; | |
void main() { | |
runApp(MaterialApp( | |
home: NameInputField(), | |
)); | |
} | |
class NameInputField extends StatefulWidget { | |
@override | |
_NameInputFieldState createState() => _NameInputFieldState(); | |
} | |
class _NameInputFieldState extends State<NameInputField> { | |
late CustomTextEditingController _controller; | |
final FocusNode _textFieldFocusNode = FocusNode(); | |
bool _isAdjusting = false; // 防止循環調用 | |
@override | |
void initState() { | |
super.initState(); | |
_controller = CustomTextEditingController(); | |
_controller.addListener(_handleTextChanges); | |
} | |
@override | |
void dispose() { | |
_controller.removeListener(_handleTextChanges); | |
_controller.dispose(); | |
_textFieldFocusNode.dispose(); | |
super.dispose(); | |
} | |
// 更新光標位置以確保其在合法位置 | |
void _handleTextChanges() { | |
if (_isAdjusting) return; | |
// 獲取所有允許的光標位置 | |
List<int> allowedPositions = _controller.getAllowedCursorPositions(); | |
final selection = _controller.selection; | |
if (!selection.isCollapsed) return; // 不處理選取範圍 | |
if (!allowedPositions.contains(selection.start)) { | |
// 找到最接近的允許位置 | |
int newPosition = allowedPositions | |
.lastWhere((pos) => pos <= selection.start, orElse: () => 0); | |
_isAdjusting = true; | |
_controller.value = _controller.value.copyWith( | |
selection: TextSelection.collapsed(offset: newPosition), | |
); | |
_isAdjusting = false; | |
} | |
} | |
// 當 Enter 被按下時,將當前輸入的文字轉換為一個完整的名字 | |
void _addName() { | |
String text = _controller.text; | |
int cursorPosition = _controller.selection.baseOffset; | |
// 找出最後一個分隔符的位置 | |
int lastSeparator = text.lastIndexOf(", "); | |
String currentInput; | |
if (lastSeparator != -1 && lastSeparator + 2 < cursorPosition) { | |
currentInput = text.substring(lastSeparator + 2, cursorPosition).trim(); | |
} else { | |
currentInput = text.substring(0, cursorPosition).trim(); | |
} | |
if (currentInput.isEmpty) return; | |
// 構建新的文本 | |
String before = | |
lastSeparator != -1 ? text.substring(0, lastSeparator + 2) : ""; | |
String after = text.substring(cursorPosition); | |
String newText = currentInput.isNotEmpty | |
? (before.isNotEmpty ? "$before$currentInput, " : "$currentInput, ") + | |
after | |
: text; | |
// 更新文本和光標位置 | |
_isAdjusting = true; | |
_controller.text = newText; | |
_controller.selection = TextSelection.collapsed( | |
offset: before.length + currentInput.length + 2); | |
_isAdjusting = false; | |
} | |
// 處理 Backspace 鍵事件,根據光標位置執行不同的刪除操作 | |
void _handleBackspace() { | |
String text = _controller.text; | |
int cursorPosition = _controller.selection.baseOffset; | |
if (cursorPosition == 0) return; | |
// 檢查是否光標位於已提交名字後的逗號位置 | |
// 比如 "Name1, Name2, |" 或 "Name1, Name2, " | |
if (cursorPosition >= 2 && | |
text.substring(cursorPosition - 2, cursorPosition) == ", ") { | |
// 刪除前一個名字及其分隔符 | |
int lastSeparator = text.lastIndexOf(", ", cursorPosition - 3); | |
if (lastSeparator == -1) { | |
// 刪除第一個名字 | |
String newText = text.substring(cursorPosition).trimLeft(); | |
_isAdjusting = true; | |
_controller.text = newText; | |
_controller.selection = TextSelection.collapsed(offset: 0); | |
_isAdjusting = false; | |
} else { | |
// 刪除前一個名字 | |
String newText = text.substring(0, lastSeparator + 2) + | |
text.substring(cursorPosition); | |
_isAdjusting = true; | |
_controller.text = newText; | |
_controller.selection = | |
TextSelection.collapsed(offset: lastSeparator + 2); | |
_isAdjusting = false; | |
} | |
return; | |
} | |
// 如果光標不在已提交名字後的位置,允許逐字刪除 | |
String newText = | |
text.substring(0, cursorPosition - 1) + text.substring(cursorPosition); | |
_isAdjusting = true; | |
_controller.text = newText; | |
_controller.selection = TextSelection.collapsed(offset: cursorPosition - 1); | |
_isAdjusting = false; | |
} | |
// 處理鍵盤按鍵事件 | |
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) { | |
if (event is KeyDownEvent) { | |
if (event.logicalKey == LogicalKeyboardKey.enter) { | |
_addName(); | |
return KeyEventResult.handled; | |
} else if (event.logicalKey == LogicalKeyboardKey.backspace) { | |
_handleBackspace(); | |
return KeyEventResult.handled; | |
} | |
// 根據最新需求,不處理 Delete 鍵 | |
} | |
return KeyEventResult.ignored; | |
} | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
appBar: AppBar( | |
title: Text('自訂 Name Input TextField'), | |
), | |
body: Padding( | |
padding: const EdgeInsets.all(16.0), | |
child: Focus( | |
onKeyEvent: _handleKeyEvent, // 使用 onKeyEvent 處理鍵盤事件 | |
child: TextField( | |
controller: _controller, | |
focusNode: _textFieldFocusNode, | |
decoration: InputDecoration( | |
border: OutlineInputBorder(), | |
labelText: '輸入姓名,按 Enter 完成', | |
), | |
style: TextStyle(color: Colors.black), // 預設文字顏色 | |
), | |
), | |
), | |
); | |
} | |
} | |
class CustomTextEditingController extends TextEditingController { | |
CustomTextEditingController({String? text}) : super(text: text); | |
// 獲取所有允許的光標位置 | |
List<int> getAllowedCursorPositions() { | |
List<int> positions = [0]; | |
List<String> names = text.split(", "); | |
int current = 0; | |
for (var name in names) { | |
current += name.length; | |
positions.add(current + 2); // 加上 ", " | |
} | |
if (positions.last != text.length) { | |
positions.add(text.length); | |
} | |
return positions; | |
} | |
@override | |
TextSpan buildTextSpan( | |
{required BuildContext context, | |
TextStyle? style, | |
required bool withComposing}) { | |
List<TextSpan> children = []; | |
String currentText = this.text; | |
// 分割名字,以 ", " 作為分隔符 | |
List<String> names = currentText.split(", "); | |
bool endsWithSeparator = currentText.endsWith(", "); | |
for (int i = 0; i < names.length; i++) { | |
String name = names[i]; | |
if (i == names.length - 1 && !endsWithSeparator) { | |
// 正在輸入的名字,顯示為黑色 | |
children | |
.add(TextSpan(text: name, style: TextStyle(color: Colors.black))); | |
} else { | |
// 已提交的名字,顯示為藍色 | |
children | |
.add(TextSpan(text: name, style: TextStyle(color: Colors.blue))); | |
} | |
if (i != names.length - 1) { | |
// 添加 ", " 分隔符,顯示為黑色 | |
children | |
.add(TextSpan(text: ', ', style: TextStyle(color: Colors.black))); | |
} | |
} | |
return TextSpan( | |
style: style ?? TextStyle(color: Colors.black), children: children); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment