欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Flutter實(shí)現(xiàn)編寫富文本Text的示例代碼

 更新時間:2022年11月03日 09:09:36   作者:木偶麻歐  
這篇文章主要為大家詳細(xì)介紹了如何通過Flutter實(shí)現(xiàn)編寫富文本Text,文中的示例代碼講解詳細(xì),具有一定的借鑒價值,需要的可以參考一下

SuperText富文本設(shè)計方案

Flutter中要實(shí)現(xiàn)富文本,需要使用RichText或者Text.rich方法,通過拆分成List<InlineSpan>來實(shí)現(xiàn),第一感覺上好像還行,但實(shí)際使用了才知道,有一個很大的問題就是對于復(fù)雜的富文本效果,無法準(zhǔn)確拆分出具有實(shí)際效果的spans。因此想設(shè)計一個具有多種富文本效果,同時便于使用的富文本控件SuperText。

RichText原理

Flutter中的InlineSpan其實(shí)是Tree的結(jié)構(gòu)。例如一段md樣式的文字:

其實(shí)就是被拆分了3段TextSpan,然后下面繪制的時候,就會使用ParagraphBuilder分別訪問這3個節(jié)點(diǎn),3個節(jié)點(diǎn)分別往ParagraphBuilder中填充對應(yīng)的文字以及樣式。

那么是否這個樹的深度一定只有2層?

這個未必,如果一開始就解析拆分出所有的富文本效果,那么可能就只有2層,但實(shí)際上就算是多層,也是沒有問題的,例如:

這是一段粗體并且部分帶著斜體效果的文字

可以拆分成如下:

需要注意的是TextSpan中有兩個參數(shù),一個是text,一個是children。這兩個參數(shù)是同時生效的, 先用TextSpan中的style和structstyle顯示text,然后再接著顯示children。例如:

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

最終顯示的效果是123456abcdefg,其中abcdefg是藍(lán)色的。

方案設(shè)計

了解了富文本的原理后,封裝控件需要實(shí)現(xiàn)的目標(biāo)就確定了,那就是

自動將文本text,轉(zhuǎn)換成inlineSpan組成的樹

然后丟給Text控件去顯示。

那么如何去實(shí)現(xiàn)這個轉(zhuǎn)化的過程?我的想法是依次遍歷節(jié)點(diǎn),然后衍生出新的節(jié)點(diǎn),最終由葉子節(jié)點(diǎn)組成最終的顯示效果。

我們以包含自定義表情和##標(biāo)簽的效果為例子。

#一個[表情]的標(biāo)簽#哈哈哈哈哈

首先初始狀態(tài)只有文本text的情況下,可以認(rèn)為是一個樹的根節(jié)點(diǎn),里面存在文本text。我們可以先把標(biāo)簽解析出來,那么就能從這個根節(jié)點(diǎn),拆分出2個節(jié)點(diǎn):

然后再將兩個葉子節(jié)點(diǎn)解析自定義表情:

最終得到4個葉子節(jié)點(diǎn),最終生成的InlineSpan,應(yīng)該如下:

TextSpan(
    children: [
        TextSpan(
            style: TextStyle(color: Colors.blue),
            children: [
                TextSpan(
                    text: '#一個',
                ),
                WidgetSpan(
                    child: Image.asset(),
                ),
                TextSpan(
                    text: '的標(biāo)簽#',
                ),
            ],
        ),
         TextSpan(
            text: '哈哈哈哈哈', 
            style: TextStyle(color: Colors.black),
        ),
    ],
),

上述過程,涉及到三點(diǎn):1. 遍歷;2. 解析拆分;3. 生成節(jié)點(diǎn)。等到了最終所有葉子結(jié)點(diǎn)都無法再被拆分出新節(jié)點(diǎn)時,這顆InlineSpan樹就是最終的解析結(jié)果。

解析

如何進(jìn)行解析。像Emoji表情或者h(yuǎn)ttp鏈接那種,一般都是使用正則便能識別出來,而更加簡單的變顏色、改字體大小這種,在Android上都是直接通過設(shè)置起始位置和結(jié)束位置來標(biāo)明范圍的,我們也可以使用這種簡單好理解的方式來實(shí)現(xiàn),所以解析的時候,需要能夠拿到待解析內(nèi)容在原始文本中的位置。例如原文“一個需要放大的字”,已經(jīng)被其他解析器分成了兩段“一個需要”和“放大的字”,在斜體解析器解析“放大的字”的時候,需要知道原文第5到第6個字需要變成斜體,在把這5->6轉(zhuǎn)變成相對于“放大的字”這一段而言的第1到第2個字。

代碼設(shè)計

方案理解了之后,就開始簡單的框架編寫。

節(jié)點(diǎn)定義

按照樹結(jié)構(gòu),定義一個Node

class TextNode {
  ///該節(jié)點(diǎn)文本
  String text;
  TextStyle style;
  late InlineSpan span;

  ///該節(jié)點(diǎn)文本,在原始文本中的開始位置。include
  int startPosInOri;

  ///該節(jié)點(diǎn)文本,在原始文本中的結(jié)束位置。include
  int endPosInOri;

  List<TextNode>? subNodes;

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

Span構(gòu)造器定義

abstract class BaseSpanBuilder {

  bool isSupport(TextNode node);
  
  ///
  /// 解析生成子節(jié)點(diǎn)
  ///
  List<TextNode> parse(TextNode node);
}

SuperText定義

先作為一個簡單版的Text控件,接收text、TextStyle構(gòu)造器列表即可。

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

對應(yīng)的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 {
      //準(zhǔn)備根節(jié)點(diǎn)
      TextNode rootNode = TextNode(widget.text, widget.style,
          startPosInOri: 0, endPosInOri: widget.text.length - 1);
      rootNode.span = TextSpan(text: widget.text, style: widget.style);
      //開始生成子節(jié)點(diǎn)
      _generateNodes(rootNode, 0);
      //深度優(yōu)先遍歷,生成最終的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) {
          //生成了子節(jié)點(diǎn),那么把子節(jié)點(diǎn)拋給下個span構(gòu)造器
          for (TextNode n in subNodes) {
            _generateNodes(n, builderIndex + 1);
          }
        } else {
          //沒有子節(jié)點(diǎn),說明當(dāng)前的span構(gòu)造器不處理當(dāng)前的節(jié)點(diǎn)內(nèi)容,那么把當(dāng)前的節(jié)點(diǎn)拋給下個span構(gòu)造器
          _generateNodes(node, builderIndex + 1);
        }
      }
    }
  }

  ///
  /// 深度優(yōu)先遍歷,構(gòu)建最終的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);
      }
    }
  }

實(shí)現(xiàn)邏輯基本就是方案設(shè)計中的想法。

可以修改TextStyle的Span構(gòu)造器

舞臺準(zhǔn)備好了,那個要訓(xùn)練演員了。這里編寫一個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) {
      //富文本開始位置,在這段文字之內(nèi)
      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) {
        //結(jié)束位置在這段文字內(nèi)
        endRelative = startRelative + (endPos - startPos);
      } else {
        //結(jié)束位置,超出了這段文字。將開始到這段文字結(jié)束,都包含進(jìn)富文本去
        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 {
      //富文本開始位置不在這段文字之內(nèi),那就檢查富文本結(jié)尾的位置,是否在這段文字內(nèi)
      if (node.startPosInOri <= endPos) {
        int startRelative = 0;
        int endRelative;
        String subText;
        TextStyle textStyle;

        if (endPos <= node.endPosInOri) {
          //富文本結(jié)尾位置,在這段文字內(nèi)
          endRelative = endPos - node.startPosInOri;
        } else {
          //富文本結(jié)尾位置,超過了這段文字
          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),
            ],
          )

效果如圖:

這個用法,好像和原來的也沒啥差別啊。其實(shí)不然,首先多個效果之間可以交叉重疊,另外這里展示的是基本的使用TextStyle實(shí)現(xiàn)的富文本效果。如果是那種需要依靠正則解析拆分后實(shí)現(xiàn)的富文本效果,例如自定義表情,只需要一個EmojiSpanBuilder()即可。

結(jié)語

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

以上就是Flutter實(shí)現(xiàn)編寫富文本Text的示例代碼的詳細(xì)內(nèi)容,更多關(guān)于Flutter富文本的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評論