Flutter实现编写富文本Text的示例代码

目录
  • SuperText富文本设计方案
  • RichText原理
  • 方案设计
    • 解析
  • 代码设计
    • 节点定义
    • Span构造器定义
    • SuperText定义
    • 可以修改TextStyle的Span构造器
  • 效果展示
  • 结语

SuperText富文本设计方案

Flutter中要实现富文本,需要使用RichText或者Text.rich方法,通过拆分成List<InlineSpan>来实现,第一感觉上好像还行,但实际使用了才知道,有一个很大的问题就是对于复杂的富文本效果,无法准确拆分出具有实际效果的spans。因此想设计一个具有多种富文本效果,同时便于使用的富文本控件SuperText。

RichText原理

Flutter中的InlineSpan其实是Tree的结构。例如一段md样式的文字:

其实就是被拆分了3段TextSpan,然后下面绘制的时候,就会使用ParagraphBuilder分别访问这3个节点,3个节点分别往ParagraphBuilder中填充对应的文字以及样式。

那么是否这个树的深度一定只有2层?

这个未必,如果一开始就解析拆分出所有的富文本效果,那么可能就只有2层,但实际上就算是多层,也是没有问题的,例如:

这是一段粗体并且部分带着斜体效果的文字

可以拆分成如下:

需要注意的是TextSpan中有两个参数,一个是text,一个是children。这两个参数是同时生效的, 先用TextSpan中的style和structstyle显示text,然后再接着显示children。例如:

Text.rich(
    TextSpan(
        text: '123456',
        children: [
                    TextSpan(
                        text: 'abcdefg',
                        style: TextStyle(color: Colors.blue),
                    ),
                  ]
    ),
)

最终显示的效果是123456abcdefg,其中abcdefg是蓝色的。

方案设计

了解了富文本的原理后,封装控件需要实现的目标就确定了,那就是

自动将文本text,转换成inlineSpan组成的树

然后丢给Text控件去显示。

那么如何去实现这个转化的过程?我的想法是依次遍历节点,然后衍生出新的节点,最终由叶子节点组成最终的显示效果。

我们以包含自定义表情和##标签的效果为例子。

#一个[表情]的标签#哈哈哈哈哈

首先初始状态只有文本text的情况下,可以认为是一个树的根节点,里面存在文本text。我们可以先把标签解析出来,那么就能从这个根节点,拆分出2个节点:

然后再将两个叶子节点解析自定义表情:

最终得到4个叶子节点,最终生成的InlineSpan,应该如下:

TextSpan(
    children: [
        TextSpan(
            style: TextStyle(color: Colors.blue),
            children: [
                TextSpan(
                    text: '#一个',
                ),
                WidgetSpan(
                    child: Image.asset(),
                ),
                TextSpan(
                    text: '的标签#',
                ),
            ],
        ),
         TextSpan(
            text: '哈哈哈哈哈',
            style: TextStyle(color: Colors.black),
        ),
    ],
),

上述过程,涉及到三点:1. 遍历;2. 解析拆分;3. 生成节点。等到了最终所有叶子结点都无法再被拆分出新节点时,这颗InlineSpan树就是最终的解析结果。

解析

如何进行解析。像Emoji表情或者http链接那种,一般都是使用正则便能识别出来,而更加简单的变颜色、改字体大小这种,在Android上都是直接通过设置起始位置和结束位置来标明范围的,我们也可以使用这种简单好理解的方式来实现,所以解析的时候,需要能够拿到待解析内容在原始文本中的位置。例如原文“一个需要放大的字”,已经被其他解析器分成了两段“一个需要”和“放大的字”,在斜体解析器解析“放大的字”的时候,需要知道原文第5到第6个字需要变成斜体,在把这5->6转变成相对于“放大的字”这一段而言的第1到第2个字。

代码设计

方案理解了之后,就开始简单的框架编写。

节点定义

按照树结构,定义一个Node

class TextNode {
  ///该节点文本
  String text;
  TextStyle style;
  late InlineSpan span;

  ///该节点文本,在原始文本中的开始位置。include
  int startPosInOri;

  ///该节点文本,在原始文本中的结束位置。include
  int endPosInOri;

  List<TextNode>? subNodes;

  TextNode(this.text, this.style,
      {required this.startPosInOri, required this.endPosInOri});
}

Span构造器定义

abstract class BaseSpanBuilder {

  bool isSupport(TextNode node);

  ///
  /// 解析生成子节点
  ///
  List<TextNode> parse(TextNode node);
}

SuperText定义

先作为一个简单版的Text控件,接收textTextStyle构造器列表即可。

class SuperText extends StatefulWidget {
  final String text;
  final TextStyle style;
  final List<BaseSpanBuilder>? spanBuilders;

  const SuperText(
    this.text, {
    Key? key,
    required this.style,
    this.spanBuilders,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _SuperTextState();
  }
}

对应的build()方法:

  late InlineSpan _textSpan;

  @override
  Widget build(BuildContext context) {
    return Text.rich(
      _textSpan,
      style: widget.style,
    );
  }

之后需要做的事就是把传入的text解析成_textSpan即可。

  InlineSpan _buildSpans() {
    if (widget.spanBuilders?.isEmpty ?? true) {
      return TextSpan(text: widget.text, style: widget.style);
    } else {
      //准备根节点
      TextNode rootNode = TextNode(widget.text, widget.style,
          startPosInOri: 0, endPosInOri: widget.text.length - 1);
      rootNode.span = TextSpan(text: widget.text, style: widget.style);
      //开始生成子节点
      _generateNodes(rootNode, 0);
      //深度优先遍历,生成最终的inlineSpan
      List<InlineSpan> children = [];
      dfs(rootNode, children);
      return TextSpan(children: children, style: widget.style);
    }
  }

  void _generateNodes(TextNode node, int builderIndex) {
    BaseSpanBuilder spanBuilder = widget.spanBuilders![builderIndex];
    if (spanBuilder.isSupport(node)) {
      List<TextNode> subNodes = spanBuilder.parse(node);
      node.subNodes = subNodes.isEmpty ? null : subNodes;
      if (builderIndex + 1 < widget.spanBuilders!.length) {
        if (subNodes.isNotEmpty) {
          //生成了子节点,那么把子节点抛给下个span构造器
          for (TextNode n in subNodes) {
            _generateNodes(n, builderIndex + 1);
          }
        } else {
          //没有子节点,说明当前的span构造器不处理当前的节点内容,那么把当前的节点抛给下个span构造器
          _generateNodes(node, builderIndex + 1);
        }
      }
    }
  }

  ///
  /// 深度优先遍历,构建最终的List<InlineSpan>
  ///
  void dfs(TextNode node, List<InlineSpan> children) {
    if (node.subNodes?.isEmpty ?? true) {
      children.add(node.span);
    } else {
      for (TextNode n in node.subNodes!) {
        dfs(n, children);
      }
    }
  }

实现逻辑基本就是方案设计中的想法。

可以修改TextStyle的Span构造器

舞台准备好了,那个要训练演员了。这里编写一个TextStyleSpanBuilder,用于接受TextStyle作为富文本样式:

class TextStyleSpanBuilder extends BaseSpanBuilder {
  final int startPos;
  final int endPos;
  final Color? textColor;
  final double? fontSize;
  final FontWeight? fontWeight;
  final Color? backgroundColor;
  final TextDecoration? decoration;
  final Color? decorationColor;
  final TextDecorationStyle? decorationStyle;
  final double? decorationThickness;
  final String? fontFamily;
  final double? height;
  final List<Shadow>? shadows;

  TextStyleSpanBuilder(
      this.startPos,
      this.endPos, {
        this.textColor,
        this.fontSize,
        this.fontWeight,
        this.backgroundColor,
        this.decoration,
        this.decorationColor,
        this.decorationStyle,
        this.decorationThickness,
        this.fontFamily,
        this.height,
        this.shadows,
      }) : assert(startPos >= 0 && startPos <= endPos);

  @override
  List<TextNode> parse(TextNode node) {
    List<TextNode> result = [];
    if (startPos > node.endPosInOri || endPos < node.startPosInOri) {
      return result;
    }

    if (startPos >= node.startPosInOri) {
      //富文本开始位置,在这段文字之内
      if (startPos > node.startPosInOri) {
        int endRelative = startPos - node.startPosInOri;
        String subText = node.text.substring(0, endRelative);
        TextNode subNode = TextNode(
          subText,
          node.style,
          startPosInOri: node.startPosInOri,
          endPosInOri: startPos - 1,
        );
        subNode.span = TextSpan(text: subNode.text, style: subNode.style);
        result.add(subNode);
      }

      //富文本在这段文字的开始位置
      int startRelative = startPos - node.startPosInOri;
      int endRelative;
      String subText;
      TextStyle textStyle;

      if (endPos <= node.endPosInOri) {
        //结束位置在这段文字内
        endRelative = startRelative + (endPos - startPos);
      } else {
        //结束位置,超出了这段文字。将开始到这段文字结束,都包含进富文本去
        endRelative = node.endPosInOri - node.startPosInOri;
      }

      subText = node.text.substring(startRelative, endRelative + 1);
      textStyle = copyStyle(node.style);
      TextNode subNode = TextNode(
        subText,
        textStyle,
        startPosInOri: node.startPosInOri + startRelative,
        endPosInOri: node.startPosInOri + endRelative,
      );
      subNode.span = TextSpan(text: subNode.text, style: subNode.style);
      result.add(subNode);

      if (endPos < node.endPosInOri) {
        //还有剩下的一段
        startRelative = endPos - node.startPosInOri + 1;
        endRelative = node.endPosInOri - node.startPosInOri;
        subText = node.text.substring(startRelative, endRelative + 1);
        TextNode subNode = TextNode(
          subText,
          node.style,
          startPosInOri: endPos + 1,
          endPosInOri: node.endPosInOri,
        );
        subNode.span = TextSpan(text: subNode.text, style: subNode.style);
        result.add(subNode);
      }
    } else {
      //富文本开始位置不在这段文字之内,那就检查富文本结尾的位置,是否在这段文字内
      if (node.startPosInOri <= endPos) {
        int startRelative = 0;
        int endRelative;
        String subText;
        TextStyle textStyle;

        if (endPos <= node.endPosInOri) {
          //富文本结尾位置,在这段文字内
          endRelative = endPos - node.startPosInOri;
        } else {
          //富文本结尾位置,超过了这段文字
          endRelative = node.endPosInOri - node.startPosInOri;
        }
        subText = node.text.substring(startRelative, endRelative + 1);
        textStyle = copyStyle(node.style);
        TextNode subNode = TextNode(
          subText,
          textStyle,
          startPosInOri: node.startPosInOri + startRelative,
          endPosInOri: node.startPosInOri + endRelative,
        );
        subNode.span = TextSpan(text: subNode.text, style: subNode.style);
        result.add(subNode);

        if (endPos < node.endPosInOri) {
          //还有剩下的一段
          startRelative = endPos - node.startPosInOri + 1;
          endRelative = node.endPosInOri - node.startPosInOri;
          subText = node.text.substring(startRelative, endRelative + 1);
          TextNode subNode = TextNode(
            subText,
            node.style,
            startPosInOri: endPos + 1,
            endPosInOri: node.endPosInOri,
          );
          subNode.span = TextSpan(text: subNode.text, style: subNode.style);
          result.add(subNode);
        }
      }
    }
    return result;
  }

  TextStyle copyStyle(TextStyle style) {
    return style.copyWith(
      color: textColor,
      fontSize: fontSize,
      fontWeight: fontWeight,
      backgroundColor: backgroundColor,
      decoration: decoration,
      decorationColor: decorationColor,
      decorationStyle: decorationStyle,
      decorationThickness: decorationThickness,
      fontFamily: fontFamily,
      height: height,
      shadows: shadows,
    );
  }

  @override
  bool isSupport(TextNode node) {
    return node.span is EmojiSpan || node.span is TextSpan;
  }
}

parse方法在做的事,就是将一个TextNode拆分成多段的TextNode。

效果展示

SuperText(
            '0123456789',
            style: const TextStyle(color: Colors.red, fontSize: 16),
            spanBuilders: [
              TextStyleSpanBuilder(2, 6, textColor: Colors.blue),
              TextStyleSpanBuilder(4, 7, fontSize: 40),
              TextStyleSpanBuilder(6, 9, backgroundColor: Colors.green),
              TextStyleSpanBuilder(1, 1, decoration: TextDecoration.underline),
            ],
          )

效果如图:

这个用法,好像和原来的也没啥差别啊。其实不然,首先多个效果之间可以交叉重叠,另外这里展示的是基本的使用TextStyle实现的富文本效果。如果是那种需要依靠正则解析拆分后实现的富文本效果,例如自定义表情,只需要一个EmojiSpanBuilder()即可。

结语

按照这个方案,对于不同的富文本效果,只需要定制不同的spanBuilder就可以了,使用方法非常类似于Android的SpannableStringBuilder

以上就是Flutter实现编写富文本Text的示例代码的详细内容,更多关于Flutter富文本的资料请关注我们其它相关文章!

(0)

相关推荐

  • flutter TextField换行自适应的实现

    无论哪种界面框架输入文本框都是非常重要的控件, 但是发现flutter中的输入框TextField介绍的虽然多,但是各个属性怎么组合满足需要很多文章却说不清楚, 再加上控件版本变更频繁很多功能的介绍都是比较陈旧的属性.现在就需要一个类似微信的输入文本框, 这样一个非常实用的效果flutter要如何实现?前提是尽量用已有属性,少写或不写代码. 先明确这种输入文本框有哪些功能点? 能够自定义各种间距.主要是控件外边距(margin); 内间距(padding); 能够自定义样式. 输入框边框(圆角(

  • Flutter实现Text完美封装

    使用惯了android的布局,对Flutter的组件和布局简直深恶痛绝啊,于是下定决心,一点一点封装Flutter的基础组件,今天封装的是Text组件,自认为封装的非常完美,完全可以用android布局的写法来写Text了,而且可以直接设置margin,padding,color,font,等等所有的属性,只需要一行代码就能实现,废话不多说,先看效果 我们可以看到,颜色,边框,圆角通通都设置完成了,还有其他的属性,就不都一一展示了,实现这个效果需要哪些代码呢?看下面 TextView( "自定义

  • Flutter中嵌入Android 原生TextView实例教程

    前言 本篇文章 中写到的是 flutter 调用了Android 原生的 TextView 案例 添加原生组件的流程基本上可以描述为: 1 android 端实现原生组件PlatformView提供原生view 2 android 端创建PlatformViewFactory用于生成PlatformView 3 android 端创建FlutterPlugin用于注册原生组件 4 flutter 平台嵌入 原生view 1 创建原生组件 创建在fLutter工程时会生成几个文件夹,lib是放fl

  • Flutter输入框TextField属性及监听事件介绍

    textField用于文本输入,它提供了很多属性: const TextField({ ... TextEditingController controller, FocusNode focusNode, InputDecoration decoration = const InputDecoration(), TextInputType keyboardType, TextInputAction textInputAction, TextStyle style, TextAlign textA

  • flutter text组件使用示例详解

    目录 正文 Text组件 Text组件构造器上的主要属性 正文 flutter组件的实现参考了react的设计理念,界面上所有的内容都是由组件构成,同时也有状态组件和无状态组件之分,这里简单介绍最基本的组件. 在组件代码的书写方式上,web端开发的样式主要有由css进行控制,而客户端开发根据使用的技术栈不同,写法也稍微有些不同:ReactNative的写法和web比较类似,但是ReactNative是使用StyleSheet.create()方法创建样式对象,以内联的方式进行书写. import

  • Flutter实现编写富文本Text的示例代码

    目录 SuperText富文本设计方案 RichText原理 方案设计 解析 代码设计 节点定义 Span构造器定义 SuperText定义 可以修改TextStyle的Span构造器 效果展示 结语 SuperText富文本设计方案 Flutter中要实现富文本,需要使用RichText或者Text.rich方法,通过拆分成List<InlineSpan>来实现,第一感觉上好像还行,但实际使用了才知道,有一个很大的问题就是对于复杂的富文本效果,无法准确拆分出具有实际效果的spans.因此想设

  • vue集成kindeditor富文本的实现示例代码

    指令 该指令的作用是dom渲染后触发,因为非vue的插件有的是dom必须存在的情况下才可以执行 Vue.directive('loaded-callback', { inserted: function (el, binding, vnode) { binding.value(el, binding, vnode) } }) 安装kindeditor npm install kindeditor kindeditor组件 <template> <div class="kinde

  • Flutter实现自定义搜索框AppBar的示例代码

    目录 介绍 效果图 实现步骤 完整源码 总结 介绍 开发中,页面头部为搜索样式的设计非常常见,为了可以像系统AppBar那样使用,这篇文章记录下在Flutter中自定义一个通用的搜索框AppBar记录. 功能点: 搜索框.返回键.清除搜索内容功能.键盘处理. 效果图 实现步骤 首先我们先来看下AppBar的源码,实现了PreferredSizeWidget类,我们可以知道这个类主要是控制AppBar的高度的,Scaffold脚手架里的AppBar的参数类型就是PreferredSizeWidge

  • 基于Python编写一个点名器的示例代码

    目录 前言 主界面 添加姓名 查看花名册 使用指南 名字转动功能 完整代码 前言 想起小学的时候老师想点名找小伙伴回答问题的时候,老师竟斥巨资买了个点名器.今日无聊便敲了敲小时候老师斥巨资买的点名器. 本人姓白,就取名小白点名器啦,嘿嘿 代码包含:添加姓名.查看花名册.使用指南.随机抽取名字的功能(完整源码在最后) 主界面 定义主界面.使用“w+”模式创建test.txt文件(我添加了个背景图片,若不需要可省略) #打开时预加载储存在test.txt文件中的花名册 namelist = [] w

  • Flutter 实现酷炫的3D效果示例代码

    此文讲解3个酷炫的3D动画效果. 下面是要实现的效果: Flutter 中3D效果是通过 Transform 组件实现的,没有变换效果的实现: class TransformDemo extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('3D 变换Demo'), ), body: Container( alignm

  • python实现企业微信定时发送文本消息的示例代码

    企业微信定时发送文本消息 使用工具:企业微信机器人+python可执行文件+计算机管理中的任务计划程序 第一步:创建群机器人 选择群聊,单击鼠标右键,添加群机器人. 建立群机器人后,右键查看机器人,如下 复制机器人的链接. 第二步:编辑python程序 import requests from datetime import datetime url = 'https://qyapi.we......' #机器人的webhook地址 headers = {'Content-type':'appl

  • Flutter仿钉钉考勤日历的示例代码

    本文主要介绍了Flutter仿钉钉考勤日历的示例代码,分享给大家,具体如下: 效果 原型 开发 1. 使用 // 考勤日历 DatePickerDialog( initialDate: DateTime.now(), firstDate: DateTime(2020), lastDate: DateTime(2030), onDateChanged: onDateChanged, // 0:无状态,1:正常考勤 2:异常考情,迟到,早退, // 若不满一个月,日历会自动用0补满一个月 check

  • Flutter实现仿微信分享功能的示例代码

    目录 1.首先去pub官网 2 在微信开放平台注册开发者账号以及创建你的应用程序 3 在分享页面 3.1 初始化 3.2 检测微信是否安装 3.3 分享微信消息 总结 本文设计到的知识点有 主要问题 Flutter 用来快速开发 Android iOS平台应用,在Flutter 中,通过 fluwx或者fluwx_no_pay 插件来实现微信分享功能 主要还是看自己的需求,本示例我将按照没有支付的实现.至于为什么,主要是ios打包提审比较麻烦. 那么接下来就看一下如何实现吧, 1.首先去pub官

  • Python设置Word全局样式和文本样式的示例代码

    目录 全局样式的定义 文本样式的定义 上一章节我们学习了如何生成 word 文档以及在文档行中添加各种内容,今天我们基于上一章节的内容进行添砖加瓦 —> 对内容进行各种样式的设置,让其能够看起来更加的美观. 全局样式的定义 通过全局样式的设置,可以使得 word 全文都可以继承这样的样式效果: 使用方法: style = document_obj.styles['Normal'] 通过 Document 对象调用 styles 对象集,通过中括号的方式选择全局样式,获得 样式对象 . for s

  • 基于Python编写微信清理工具的示例代码

    目录 主要功能 运行环境 核心代码 完整代码 前几天网上找了一款 PC 端微信自动清理工具,用了一下,电脑释放了 30GB 的存储空间,而且不会删除文字的聊天记录,很好用,感觉很多人都用得到,就在此分享一下,而且是用 Python 写的,喜欢 Python 的小伙伴可以探究一下. 主要功能 它可以自动删除 PC 端微信自动下载的大量文件.视频.图片等数据内容,释放几十 G 的空间占用,而且不会删除文字的聊天记录,可以放心使用. 工作以后,微信的群聊实在太多了,动不动就被拉入一个群中,然后群聊里大

随机推荐