我正在尝试像 WhatsApp 一样在我的应用程序中实现编号列表功能。 它几乎已经实现了,但我在我的代码中发现了 3 个错误。
注意:我没有使用任何外部包,而是创建了自定义
TextEditingController
来实现此目的。
代码:
class TestingPage extends StatefulWidget {
const TestingPage({super.key});
@override
State<TestingPage> createState() => _TestingPageState();
}
class _TestingPageState extends State<TestingPage> {
CustomTextEditingController controller = CustomTextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Testing"),
centerTitle: true,
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30),
child: TextField(
controller: controller,
maxLines: 8,
decoration: const InputDecoration(
hintText: "Enter text here...", border: OutlineInputBorder()),
),
),
),
);
}
}
自定义
TextEditingController
类:
import 'package:flutter/cupertino.dart';
class CustomTextEditingController extends TextEditingController {
bool isBackPressed = false;
String _previousText = '';
CustomTextEditingController({String text = ''}) : super(text: text) {
addListener(_onTextChanged);
}
// Custom function to handle text changes
void _onTextChanged() {
String text = this.text;
int cursorPosition = selection.baseOffset;
if (_previousText.length > text.length) {
isBackPressed = true;
} else {
isBackPressed = false;
}
_previousText = text;
if (cursorPosition > 0) {
// Detect Enter key press
if (text[cursorPosition - 1] == '\n' && !isBackPressed) {
debugPrint("inside12");
String previousLine = _getPreviousLine(text, cursorPosition);
RegExp numberRegex = RegExp(r'^(\d+)\.\s');
Match? match = numberRegex.firstMatch(previousLine);
if (match != null) {
int currentNumber = int.parse(match.group(1)!);
String newText = text.replaceRange(
cursorPosition,
cursorPosition,
'${currentNumber + 1}. ',
);
value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: cursorPosition + currentNumber.toString().length + 2,
),
);
}
}
// Handle backspace to clear line numbering
if (text[cursorPosition - 1] == '\n' &&
cursorPosition > 1 &&
text.substring(cursorPosition - 4, cursorPosition - 1) == '. ') {
debugPrint("inside11");
String newText = text.substring(0, cursorPosition - 4) +
text.substring(cursorPosition);
value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(offset: cursorPosition - 4),
);
}
}
}
@override
void dispose() {
// Clean up the listener when the controller is disposed
removeListener(_onTextChanged);
super.dispose();
}
String _getPreviousLine(String text, int cursorPosition) {
int lastNewline = text.lastIndexOf('\n', cursorPosition - 2);
if (lastNewline == -1) {
return text.substring(0, cursorPosition).trim();
} else {
return text.substring(lastNewline + 1, cursorPosition - 1).trim();
}
}
}
当我查看您附加到您的问题的错误时,我认为可能存在其他情况,您当前的实现将无法完美工作(例如用户从列表中间切掉行)。所以我花了一些时间想出了一种替代方法,因为我发现这个问题很有趣。它不是很理想,我什至不确定它是否 100% 有效,但我分享我的想法,希望它对你有帮助。
基本上你有两种处理情况:
Enter
继续上一个列表。对于修改部分,我的方法(这就是为什么它不是最佳的)只是采用整个文本字段并对行重新编号。虽然不是很理想,但它提供了一些附加功能:
代码可以复制粘贴到DartPad,请尝试一下。我添加了一些评论,但我想解释一件事:为什么我决定添加
KeyBoardListener
?如果没有这个,我无法区分在某处添加新行和删除最后一行。在不观察Enter
键是否被按下的情况下,如果最后一行有数字,并且用户尝试删除它,它会立即重新添加。也许您会找到更好的解决方案。
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: TestingPage(),
);
}
}
class TestingPage extends StatefulWidget {
const TestingPage({super.key});
@override
State<TestingPage> createState() => _TestingPageState();
}
class _TestingPageState extends State<TestingPage> {
// separator between number and text in numbered list
final _numberRegExp = RegExp(r'^(\d+)\.\s');
late final TextEditingController _controller;
late final FocusNode _focusNode;
bool _wasEnterPressed = false;
@override
void initState() {
super.initState();
_controller = TextEditingController();
_focusNode = FocusNode();
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Testing"),
centerTitle: true,
),
body: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: KeyboardListener(
focusNode: _focusNode,
// store if Enter was pressed to prevent re-adding
// numbering to last line it it was deleted by the user
onKeyEvent: (event) {
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.enter) {
_wasEnterPressed = true;
}
},
child: TextField(
onChanged: _onTextChanged,
controller: _controller,
maxLines: 32,
decoration: const InputDecoration(
hintText: "Enter text here...",
border: OutlineInputBorder()),
)),
),
),
);
}
// called on every change in the text field
void _onTextChanged(String text) {
// if field is empty or offset is 0, nothing needs to be done
if (text.isEmpty || _controller.selection.baseOffset < 1) {
_wasEnterPressed = false;
return;
}
// if the character before the current cursor is new line and
// Enter was pressed, add a new number if needed
if (text[_controller.selection.baseOffset - 1] == '\n' && _wasEnterPressed) {
// add the next number
final newText = _addNextNumber(text);
// also renumber lists if anything was added
if (newText != null) {
_renumberLists(newText);
}
} else {
// otherwise just renumber lists
_renumberLists(text);
}
// this needs to be cleared because last Enter press was handled
_wasEnterPressed = false;
}
// add next number if a previous list is continued
String? _addNextNumber(String text) {
final offset = _controller.selection.baseOffset;
String? previousLine;
int currentPosition = 0;
final lines = text.split('\n');
// find the line before the current cursor, return if not found
for (var line in lines) {
currentPosition += (line.length + 1);
if (currentPosition == offset) {
previousLine = line;
break;
}
}
if (previousLine == null) {
return null;
}
// check if the line before has a leading number, if not, return
Match? match = _numberRegExp.firstMatch(previousLine);
if (match == null) {
return null;
}
// get number from previous row as text and integer
final matched = match.group(0)!;
final previousNumberAsText = matched.substring(0, matched.length - 2);
final previousNumber = int.tryParse(previousNumberAsText);
if (previousNumber == null) {
return null;
}
// this will be the new number and the entire new value of the field
// after the new number is inserted to the beginning of the new line
final newNumberAsText = '${previousNumber + 1}. ';
final newText = text.replaceRange(offset, offset, newNumberAsText);
// set new value and manage offset
_controller.value = TextEditingValue(
text: newText,
selection: TextSelection.collapsed(
offset: _controller.selection.baseOffset + newNumberAsText.length,
),
);
// return the new text because _renumberLists needs it
return newText;
}
// renumber every list found in text field
void _renumberLists(String text) {
final lines = text.split('\n');
final newLines = <String>[];
int nextNumber = 1;
int charactersDifference = 0;
for (var line in lines) {
Match? match = _numberRegExp.firstMatch(line);
if (match == null) {
// this restarts the numbering for the next numbered block
nextNumber = 1;
newLines.add(line);
} else {
final currentNumber = match.group(0)!;
final nextNumberAsText = nextNumber.toString();
nextNumber++;
final currentLineText = line.substring(currentNumber.length);
charactersDifference -= currentNumber.length;
charactersDifference += nextNumberAsText.length + 2;
newLines.add('$nextNumberAsText. $currentLineText');
}
}
// set new value and manage offset
_controller.value = TextEditingValue(
text: newLines.join('\n'),
selection: TextSelection.collapsed(
offset: _controller.selection.baseOffset + charactersDifference,
),
);
}
}