如何在flutter中使用dart代码实现whatsApp等文本字段中的编号列表功能?

问题描述 投票:0回答:1

我正在尝试像 WhatsApp 一样在我的应用程序中实现编号列表功能。 它几乎已经实现了,但我在我的代码中发现了 3 个错误。

注意:我没有使用任何外部包,而是创建了自定义

TextEditingController
来实现此目的。

  1. BUG1

  2. BUG2

  3. BUG3

代码:

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();
    }
  }
}

flutter dart textfield texteditingcontroller
1个回答
0
投票

当我查看您附加到您的问题的错误时,我认为可能存在其他情况,您当前的实现将无法完美工作(例如用户从列表中间切掉行)。所以我花了一些时间想出了一种替代方法,因为我发现这个问题很有趣。它不是很理想,我什至不确定它是否 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,
      ),
    );
  }
}

© www.soinside.com 2019 - 2024. All rights reserved.