Flutter文本框添加图片表情(粗制滥造版)

0 阅读1分钟

效果图:

无标题.png

使用 Unicode 的私有区做表情的占位符

继承重写 TextEditingController,在 buildTextSpan 内显示时调整为表情图片。

表情键值对:

final Map<int, String> emojiMap = {
    0xE001: 'assets/images/1.png',
    0xE002: 'assets/images/2.png',
    0xE003: 'assets/images/3.png',
  };

遍历字符串把占位符显示为表情的方法:

  List<InlineSpan> _parseText(String text, TextStyle? style) {
    final spans = <InlineSpan>[];
    final buffer = StringBuffer();
    for (final codePoint in text.runes) {
      // 如果是表情
      if (codePoint >= 0xE000 && codePoint <= 0xF8FF) {
        // 将 buffer 内的字符加入 spans 并清空 buffer。
        if (buffer.isNotEmpty) {
          spans.add(TextSpan(text: buffer.toString(), style: style));
          buffer.clear();
        }
        // 处理表情
        final asset = emojiMap[codePoint];
        if (asset != null) {
          spans.add(
            WidgetSpan(
              child: Image.asset(
                asset,
                width: style?.fontSize,
                height: style?.fontSize,
              ),
            ),
          );
        } else {
          buffer.writeCharCode(codePoint);
        }
      } else {
        buffer.writeCharCode(codePoint);
      }
    }
    if (buffer.isNotEmpty) {
      spans.add(TextSpan(text: buffer.toString(), style: style));
    }
    return spans;
  }

 

buildTextSpan:

  @override
  TextSpan buildTextSpan({
    required BuildContext context,
    TextStyle? style,
    required bool withComposing,
  }) {
    assert(
      !value.composing.isValid || !withComposing || value.isComposingRangeValid,
    );
    final bool composingRegionOutOfRange =
        !value.isComposingRangeValid || !withComposing;

    if (composingRegionOutOfRange) {
      // 修改为使用 _parseText()
      // 原本的:return TextSpan(style: style, text: text);
      return TextSpan(style: style, children: _parseText(text, style));
    }

    final TextStyle composingStyle =
        style?.merge(const TextStyle(decoration: TextDecoration.underline)) ??
        const TextStyle(decoration: TextDecoration.underline);

    // 这里也是修改为使用 _parseText()
    return TextSpan(
      style: style,
      children: <TextSpan>[
        TextSpan(
          children: _parseText(value.composing.textBefore(value.text), style),
        ),
        TextSpan(
          style: composingStyle,
          children: _parseText(value.composing.textInside(value.text), style),
        ),
        TextSpan(
          children: _parseText(value.composing.textAfter(value.text), style),
        ),
      ],
    );
  }

添加表情:

  void addEmoji(int codePoint) {
    // 获取当前选区,如果无效(如未聚焦),则默认光标在文本末尾
    final TextSelection currentSelection = selection.isValid
        ? selection
        : TextSelection.collapsed(offset: text.length);

    // 根据选区情况构造新文本和光标位置
    final String newText;
    final int newCursorOffset;

    if (currentSelection.isCollapsed) {
      // 折叠光标:直接插入
      final int pos = currentSelection.baseOffset;
      newText =
          text.substring(0, pos) +
          String.fromCharCode(codePoint) +
          text.substring(pos);
      newCursorOffset = pos + 1;
    } else {
      // 有选中文本:先删除选中内容,再在开始位置插入
      final int start = currentSelection.start;
      final int end = currentSelection.end;
      newText =
          text.substring(0, start) +
          String.fromCharCode(codePoint) +
          text.substring(end);
      newCursorOffset = start + 1;
    }

    // 更新控制器值,并设置光标折叠在新字符之后
    value = value.copyWith(
      text: newText,
      selection: TextSelection.collapsed(offset: newCursorOffset),
      // 清除组合范围,因为插入操作会中断输入法组合
      composing: TextRange.empty,
    );
  }