Interfaz de chat Flutter: el cuadro de entrada TextField implementa la función @ y otras funciones de resaltado de visualización de expresiones regulares coincidentes

Interfaz de chat Flutter: el cuadro de entrada TextField implementa la función @ y otras funciones de resaltado de visualización de expresiones regulares coincidentes

1. Breve descripción

Descripción:
Cuando un amigo estaba hablando recientemente, mencionó el resaltado de los cuadros de entrada. Es necesario insertar una etiqueta de estilo especial en el campo de texto de Flutter, como por ejemplo: "Por favor @张三 responda". Esta cadena de caracteres se ingresa en el campo de texto. Cuando se ingresa @, aparece una selección de lista de amigos y luego "@张三" está resaltado. en TextField.

Las representaciones son las siguientes.

Insertar descripción de la imagen aquí
efectos de vídeo

Interfaz de chat Flutter: el cuadro de entrada TextField implementa la función @

En el artículo compilado ayer, es simple y práctico modificar el código directamente al construirTextSpan de TextEditingController.

List<InlineSpan> textSpans = RichTextHelper.getRichText(value.text);
    if (composingRegionOutOfRange) {
    
    
      return TextSpan(style: style, children: textSpans);
    }

Habrá un problema con la entrada del cursor, se ha modificado y mejorado aquí.

Puede usarlo rich_text_controllerpara implementarlo, puede verlo viendo el código fuente de rich_text_controller.RichTextController继承TextEditingController,重写了buildTextSpan。经过我在iPhone上测试,当输入@汉字的时候,对中文兼容会有问题,这里做一下修改完善修改。

2. Método buildTextSpan de TextEditingController

En el método buildTextSpan en TextEditingController, podemos ver que el código de este método
Insertar descripción de la imagen aquí

composingRegionOutOfRange: solo ingrese palabras completadas

La última parte del código contiene palabras sin terminar.

final TextStyle composingStyle = style?.merge(const TextStyle(decoration: TextDecoration.underline))
        ?? const TextStyle(decoration: TextDecoration.underline);
    return TextSpan(
      style: style,
      children: <TextSpan>[
        TextSpan(text: value.composing.textBefore(value.text)),
        TextSpan(
          style: composingStyle,
          text: value.composing.textInside(value.text),
        ),
        TextSpan(text: value.composing.textAfter(value.text)),
      ],
    );
  • composingStyle Usted mismo puede modificar el estilo de la palabra completa que no se ha ingresado.

  • value.composing.textBefore: la palabra antes de la entrada actual.

  • value.composing.textAfter: la palabra después de la entrada actual.

Durante el proceso de entrada, podemos resaltar las coincidencias value.composing.textBefore y value.composing.textAfter.

El código se muestra a continuación.

valor.componer.textoAntes

TextSpan(style: style, children: buildRegExpSpan(context: context, text: value.composing.textBefore(value.text))),

valor.composing.textDespués

TextSpan(style: style, children: buildRegExpSpan(context: context, text: value.composing.textAfter(value.text))),

Coincidir con expresión regular

List<TextSpan> buildRegExpSpan(
      {
    
    required BuildContext context,
        TextStyle? style,
        required String? text}) {
    
    
    List<TextSpan> children = [];
    if (!(text != null && text.isNotEmpty)) {
    
    
      return children;
    }
    final matches = <String>{
    
    };
    List<Map<String, List<int>>> matchIndex = [];

    // Validating with REGEX
    RegExp? allRegex;
    allRegex = patternMatchMap != null
        ? RegExp(patternMatchMap?.keys.map((e) => e.pattern).join('|') ?? "",
        caseSensitive: regExpCaseSensitive,
        dotAll: regExpDotAll,
        multiLine: regExpMultiLine,
        unicode: regExpUnicode)
        : null;
    // Validating with Strings
    RegExp? stringRegex;
    stringRegex = stringMatchMap != null
        ? RegExp(r'\b' + stringMatchMap!.keys.join('|').toString() + r'+\$',
        caseSensitive: regExpCaseSensitive,
        dotAll: regExpDotAll,
        multiLine: regExpMultiLine,
        unicode: regExpUnicode)
        : null;
    
    text.splitMapJoin(
      stringMatchMap == null ? allRegex! : stringRegex!,
      onNonMatch: (String span) {
    
    
        if (stringMatchMap != null &&
            children.isNotEmpty &&
            stringMatchMap!.keys.contains("${
      
      children.last.text}$span")) {
    
    
          final String? ks =
          stringMatchMap!["${
      
      children.last.text}$span"] != null
              ? stringMatchMap?.entries.lastWhere((element) {
    
    
            return element.key
                .allMatches("${
      
      children.last.text}$span")
                .isNotEmpty;
          }).key
              : '';

          children.add(TextSpan(text: span, style: stringMatchMap![ks!]));
          return span.toString();
        } else {
    
    
          children.add(TextSpan(text: span, style: style));
          return span.toString();
        }
      },
      onMatch: (Match m) {
    
    
        matches.add(m[0]!);
        final RegExp? k = patternMatchMap?.entries.firstWhere((element) {
    
    
          return element.key.allMatches(m[0]!).isNotEmpty;
        }).key;

        final String? ks = stringMatchMap?[m[0]] != null
            ? stringMatchMap?.entries.firstWhere((element) {
    
    
          return element.key.allMatches(m[0]!).isNotEmpty;
        }).key
            : '';
        if (deleteOnBack!) {
    
    
          if ((isBack(text!, _lastValue) && m.end == selection.baseOffset)) {
    
    
            WidgetsBinding.instance.addPostFrameCallback((_) {
    
    
              children.removeWhere((element) => element.text! == text);
              text = text!.replaceRange(m.start, m.end, "");
              selection = selection.copyWith(
                baseOffset: m.end - (m.end - m.start),
                extentOffset: m.end - (m.end - m.start),
              );
            });
          } else {
    
    
            children.add(
              TextSpan(
                text: m[0],
                style: stringMatchMap == null
                    ? patternMatchMap![k]
                    : stringMatchMap![ks],
              ),
            );
          }
        } else {
    
    
          children.add(
            TextSpan(
              text: m[0],
              style: stringMatchMap == null
                  ? patternMatchMap![k]
                  : stringMatchMap![ks],
            ),
          );
        }
        final resultMatchIndex = matchValueIndex(m);
        if (resultMatchIndex != null && onMatchIndex != null) {
    
    
          matchIndex.add(resultMatchIndex);
          onMatchIndex!(matchIndex);
        }

        return (onMatch(List<String>.unmodifiable(matches)) ?? '');
      },
    );
    return children;
  }

Aquí se utiliza el código en rich_text_controller y se realizan las modificaciones correspondientes. La expresión regular ingresada por @张三 se resalta normalmente.

El código completo de text_field_controller es el siguiente

import 'package:flutter/material.dart';

class TextFieldController extends TextEditingController {
    
    
  final Map<RegExp, TextStyle>? patternMatchMap;
  final Map<String, TextStyle>? stringMatchMap;
  final Function(List<String> match) onMatch;
  final Function(List<Map<String, List<int>>>)? onMatchIndex;
  final bool? deleteOnBack;
  String _lastValue = "";

  /// controls the caseSensitive property of the full [RegExp] used to pattern match
  final bool regExpCaseSensitive;

  /// controls the dotAll property of the full [RegExp] used to pattern match
  final bool regExpDotAll;

  /// controls the multiLine property of the full [RegExp] used to pattern match
  final bool regExpMultiLine;

  /// controls the unicode property of the full [RegExp] used to pattern match
  final bool regExpUnicode;

  bool isBack(String current, String last) {
    
    
    return current.length < last.length;
  }

  TextFieldController(
      {
    
    String? text,
        this.patternMatchMap,
        this.stringMatchMap,
        required this.onMatch,
        this.onMatchIndex,
        this.deleteOnBack = false,
        this.regExpCaseSensitive = true,
        this.regExpDotAll = false,
        this.regExpMultiLine = false,
        this.regExpUnicode = false})
      : assert((patternMatchMap != null && stringMatchMap == null) ||
      (patternMatchMap == null && stringMatchMap != null)),
        super(text: text);

  /// Setting this will notify all the listeners of this [TextEditingController]
  /// that they need to update (it calls [notifyListeners]).
  
  set text(String newText) {
    
    
    value = value.copyWith(
      text: newText,
      selection: const TextSelection.collapsed(offset: -1),
      composing: TextRange.empty,
    );
  }

  /// Builds [TextSpan] from current editing value.
  
  TextSpan buildTextSpan(
      {
    
    required BuildContext context,
        TextStyle? style,
        required bool withComposing}) {
    
    
    assert(!value.composing.isValid || !withComposing || value.isComposingRangeValid);
    // If the composing range is out of range for the current text, ignore it to
    // preserve the tree integrity, otherwise in release mode a RangeError will
    // be thrown and this EditableText will be built with a broken subtree.
    final bool composingRegionOutOfRange = !value.isComposingRangeValid || !withComposing;

    if (composingRegionOutOfRange) {
    
    
      List<TextSpan> children = [];
      final matches = <String>{
    
    };
      List<Map<String, List<int>>> matchIndex = [];

      // Validating with REGEX
      RegExp? allRegex;
      allRegex = patternMatchMap != null
          ? RegExp(patternMatchMap?.keys.map((e) => e.pattern).join('|') ?? "",
          caseSensitive: regExpCaseSensitive,
          dotAll: regExpDotAll,
          multiLine: regExpMultiLine,
          unicode: regExpUnicode)
          : null;
      // Validating with Strings
      RegExp? stringRegex;
      stringRegex = stringMatchMap != null
          ? RegExp(r'\b' + stringMatchMap!.keys.join('|').toString() + r'+\$',
          caseSensitive: regExpCaseSensitive,
          dotAll: regExpDotAll,
          multiLine: regExpMultiLine,
          unicode: regExpUnicode)
          : null;
      
      text.splitMapJoin(
        stringMatchMap == null ? allRegex! : stringRegex!,
        onNonMatch: (String span) {
    
    
          if (stringMatchMap != null &&
              children.isNotEmpty &&
              stringMatchMap!.keys.contains("${
      
      children.last.text}$span")) {
    
    
            final String? ks =
            stringMatchMap!["${
      
      children.last.text}$span"] != null
                ? stringMatchMap?.entries.lastWhere((element) {
    
    
              return element.key
                  .allMatches("${
      
      children.last.text}$span")
                  .isNotEmpty;
            }).key
                : '';

            children.add(TextSpan(text: span, style: stringMatchMap![ks!]));
            return span.toString();
          } else {
    
    
            children.add(TextSpan(text: span, style: style));
            return span.toString();
          }
        },
        onMatch: (Match m) {
    
    
          matches.add(m[0]!);
          final RegExp? k = patternMatchMap?.entries.firstWhere((element) {
    
    
            return element.key.allMatches(m[0]!).isNotEmpty;
          }).key;

          final String? ks = stringMatchMap?[m[0]] != null
              ? stringMatchMap?.entries.firstWhere((element) {
    
    
            return element.key.allMatches(m[0]!).isNotEmpty;
          }).key
              : '';
          if (deleteOnBack!) {
    
    
            if ((isBack(text, _lastValue) && m.end == selection.baseOffset)) {
    
    
              WidgetsBinding.instance.addPostFrameCallback((_) {
    
    
                children.removeWhere((element) => element.text! == text);
                text = text.replaceRange(m.start, m.end, "");
                selection = selection.copyWith(
                  baseOffset: m.end - (m.end - m.start),
                  extentOffset: m.end - (m.end - m.start),
                );
              });
            } else {
    
    
              children.add(
                TextSpan(
                  text: m[0],
                  style: stringMatchMap == null
                      ? patternMatchMap![k]
                      : stringMatchMap![ks],
                ),
              );
            }
          } else {
    
    
            children.add(
              TextSpan(
                text: m[0],
                style: stringMatchMap == null
                    ? patternMatchMap![k]
                    : stringMatchMap![ks],
              ),
            );
          }
          final resultMatchIndex = matchValueIndex(m);
          if (resultMatchIndex != null && onMatchIndex != null) {
    
    
            matchIndex.add(resultMatchIndex);
            onMatchIndex!(matchIndex);
          }

          return (onMatch(List<String>.unmodifiable(matches)) ?? '');
        },
      );

      _lastValue = text;
      return TextSpan(style: style, children: children);
    }

    final TextStyle composingStyle = style?.merge(const TextStyle(decoration: TextDecoration.underline))
        ?? const TextStyle(decoration: TextDecoration.underline);
    return TextSpan(
      children: <TextSpan>[
        TextSpan(style: style, children: buildRegExpSpan(context: context, text: value.composing.textBefore(value.text))),
        TextSpan(
          style: composingStyle,
          text: value.composing.textInside(value.text),
        ),
        TextSpan(style: style, children: buildRegExpSpan(context: context, text: value.composing.textAfter(value.text))),
      ],
    );
  }

  Map<String, List<int>>? matchValueIndex(Match match) {
    
    
    final matchValue = match[0]?.replaceFirstMapped('#', (match) => '');
    if (matchValue != null) {
    
    
      final firstMatchChar = match.start + 1;
      final lastMatchChar = match.end - 1;
      final compactMatch = {
    
    
        matchValue: [firstMatchChar, lastMatchChar]
      };
      return compactMatch;
    }
    return null;
  }

  List<TextSpan> buildRegExpSpan(
      {
    
    required BuildContext context,
        TextStyle? style,
        required String? text}) {
    
    
    List<TextSpan> children = [];
    if (!(text != null && text.isNotEmpty)) {
    
    
      return children;
    }
    final matches = <String>{
    
    };
    List<Map<String, List<int>>> matchIndex = [];

    // Validating with REGEX
    RegExp? allRegex;
    allRegex = patternMatchMap != null
        ? RegExp(patternMatchMap?.keys.map((e) => e.pattern).join('|') ?? "",
        caseSensitive: regExpCaseSensitive,
        dotAll: regExpDotAll,
        multiLine: regExpMultiLine,
        unicode: regExpUnicode)
        : null;
    // Validating with Strings
    RegExp? stringRegex;
    stringRegex = stringMatchMap != null
        ? RegExp(r'\b' + stringMatchMap!.keys.join('|').toString() + r'+\$',
        caseSensitive: regExpCaseSensitive,
        dotAll: regExpDotAll,
        multiLine: regExpMultiLine,
        unicode: regExpUnicode)
        : null;
    
    text.splitMapJoin(
      stringMatchMap == null ? allRegex! : stringRegex!,
      onNonMatch: (String span) {
    
    
        if (stringMatchMap != null &&
            children.isNotEmpty &&
            stringMatchMap!.keys.contains("${
      
      children.last.text}$span")) {
    
    
          final String? ks =
          stringMatchMap!["${
      
      children.last.text}$span"] != null
              ? stringMatchMap?.entries.lastWhere((element) {
    
    
            return element.key
                .allMatches("${
      
      children.last.text}$span")
                .isNotEmpty;
          }).key
              : '';

          children.add(TextSpan(text: span, style: stringMatchMap![ks!]));
          return span.toString();
        } else {
    
    
          children.add(TextSpan(text: span, style: style));
          return span.toString();
        }
      },
      onMatch: (Match m) {
    
    
        matches.add(m[0]!);
        final RegExp? k = patternMatchMap?.entries.firstWhere((element) {
    
    
          return element.key.allMatches(m[0]!).isNotEmpty;
        }).key;

        final String? ks = stringMatchMap?[m[0]] != null
            ? stringMatchMap?.entries.firstWhere((element) {
    
    
          return element.key.allMatches(m[0]!).isNotEmpty;
        }).key
            : '';
        if (deleteOnBack!) {
    
    
          if ((isBack(text!, _lastValue) && m.end == selection.baseOffset)) {
    
    
            WidgetsBinding.instance.addPostFrameCallback((_) {
    
    
              children.removeWhere((element) => element.text! == text);
              text = text!.replaceRange(m.start, m.end, "");
              selection = selection.copyWith(
                baseOffset: m.end - (m.end - m.start),
                extentOffset: m.end - (m.end - m.start),
              );
            });
          } else {
    
    
            children.add(
              TextSpan(
                text: m[0],
                style: stringMatchMap == null
                    ? patternMatchMap![k]
                    : stringMatchMap![ks],
              ),
            );
          }
        } else {
    
    
          children.add(
            TextSpan(
              text: m[0],
              style: stringMatchMap == null
                  ? patternMatchMap![k]
                  : stringMatchMap![ks],
            ),
          );
        }
        final resultMatchIndex = matchValueIndex(m);
        if (resultMatchIndex != null && onMatchIndex != null) {
    
    
          matchIndex.add(resultMatchIndex);
          onMatchIndex!(matchIndex);
        }

        return (onMatch(List<String>.unmodifiable(matches)) ?? '');
      },
    );
    return children;
  }
}

En este punto puedes ver que @张三 está resaltado en la representación.

3. Utilice TextFieldController para probar el resaltado de @张三

Después de ajustar TextFieldController, pruebo @张三resaltar aquí

Inicializamos TextFieldController

// Add a controller
  late TextFieldController _controller;

  
  void initState() {
    
    
    // TODO: implement initState
    _controller = TextFieldController(
      patternMatchMap: {
    
    
        //
        //* Returns every Hashtag with red color
        //
        RegExp(r"@[^\s]+\s?"):TextStyle(color:Colors.green),
        //
        //* Returns every Hashtag with red color
        //
        RegExp(r"\B#[a-zA-Z0-9]+\b"):TextStyle(color:Colors.red),
        //
        //* Returns every Mention with blue color and bold style.
        //
        RegExp(r"\B@[a-zA-Z0-9]+\b"):TextStyle(fontWeight: FontWeight.w800 ,color:Colors.blue,),
        //
        //* Returns every word after '!' with yellow color and italic style.
        //
        RegExp(r"\B![a-zA-Z0-9]+\b"):TextStyle(color:Colors.yellow, fontStyle:FontStyle.italic),
        // add as many expressions as you need!
      },
      //* starting v1.2.0
      // Now you have the option to add string Matching!
      // stringMatchMap: {
    
    
      //   "String1":TextStyle(color: Colors.red),
      //   "String2":TextStyle(color: Colors.yellow),
      // },
      //! Assertion: Only one of the two matching options can be given at a time!

      //* starting v1.1.0
      //* Now you have an onMatch callback that gives you access to a List<String>
      //* which contains all matched strings
      onMatch: (List<String> matches){
    
    
        // Do something with matches.
        //! P.S
        // as long as you're typing, the controller will keep updating the list.
      },
      deleteOnBack: true,
      // You can control the [RegExp] options used:
      regExpUnicode: true,
    );

    super.initState();
  }

Utilice TextFieldController en TextField. El código específico es el siguiente.

TextField(
        minLines: 1,
        maxLines: null,
        keyboardType: TextInputType.multiline,
        textAlignVertical: TextAlignVertical.center,
        autofocus: true,
        focusNode: editFocusNode,
        controller: _controller,
        textInputAction: TextInputAction.send,
        decoration: InputDecoration(
          contentPadding: EdgeInsets.symmetric(vertical: 10, horizontal: 8.0),
          filled: true,
          isCollapsed: true,
          floatingLabelBehavior: FloatingLabelBehavior.never,
          hintText: "说点什么吧~",
          hintStyle: TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.w400,
            fontStyle: FontStyle.normal,
            color: ColorUtil.hexColor(0xACACAC),
            decoration: TextDecoration.none,
          ),
          enabledBorder: OutlineInputBorder(
            /*边角*/
            borderRadius: const BorderRadius.all(
              Radius.circular(5.0), //边角为30
            ),
            borderSide: BorderSide(
              color: ColorUtil.hexColor(0xf7f7f7), //边框颜色为绿色
              width: 1, //边线宽度为1
            ),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: const BorderRadius.all(
              Radius.circular(5.0), //边角为30
            ),
            borderSide: BorderSide(
              color: ColorUtil.hexColor(0xECECEC), //边框颜色为绿色
              width: 1, //宽度为1
            ),
          ),
        ),
      )

Después de la prueba de eliminación de entrada, el "@张三" ingresado se resalta y se muestra normalmente en TextField.

Utilice buildTextSpan de TextEditingController, puede ver: https://blog.csdn.net/gloryFlow/article/details/132889374
Mejore el cuadro de entrada de TextField que coincide con el resaltado de expresiones regulares, puede ver: https://blog.csdn.net/gloryFlow /artículo/detalles/132899084

4. Resumen

Interfaz de chat Flutter: cuadro de entrada TextField buildTextSpan implementa la función @ para resaltar la visualización. Personaliza y modifica TextEditingController.
Hay mucho contenido y es posible que la descripción no sea precisa, así que perdóneme.

Dirección de este artículo: https://blog.csdn.net/gloryFlow/article/details/132899084

Estudia y registra, sigue mejorando cada día.

Supongo que te gusta

Origin blog.csdn.net/gloryFlow/article/details/132899084
Recomendado
Clasificación