热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

sql上一行减下一行_书后拓展:Flutter中一行文字到屏幕上,渲染全过程!

我要写的这一系列文章旨在分享一些我想要继续分享,但碍于《Flutter开发之旅从南到北》中篇幅和话题的限制,没有深入分析的部分,读者们可以

我要写的这一系列文章旨在分享一些我想要继续分享,但碍于《Flutter 开发之旅从南到北》中篇幅和话题的限制,没有深入分析的部分,读者们可以在学有余力的情况下在这里继续拓展下去。

本文要讨论的话题是 Flutter 中的文本渲染,也假定你已经大致清楚了 Flutter 中 Widget、Element 和 RenderObject 等概念。

正文

在之前的文章中就有提及,Flutter 源码中除了无状态(StatelessWidget)和有状态(StatefluWidget)这两个直接继承自 Widget 的组件外,还存在其他另类的 Widget,如可以用来统一管理状态的可遗传组件 InheritedWidget用于自渲染组件的 RenderObjectWidget。

关于 StatelessWidget 和 StatefluWidget 的相关内容,我相信大部分读者已经接触的足够多了,那么,今天我们就来一起探究一下 RenderObjectWidget 的奥妙,看看它是如何帮助我们渲染组件的。

9a433fbec21fe89346e7054398fea856.png
Flutter Architecture

RenderObjectWidget 作为一个普普通通的 widget,毫无疑问是处在架构图中 framework 的 Widget 层,然而它的渲染对象 RenderParagraph 就已经到了 Rendering 层了,走进 RenderParagraph 这个类,发现它就是直接继承自我们熟悉的 RenderBox:

class RenderParagraph extends RenderBoxwith ContainerRenderObjectMixin,RenderBoxContainerDefaultsMixin,RelayoutWhenSystemFontsChangeMixin

也就是说,它依然会接受盒子约束限制自己的宽高,他依然渲染的是一个矩形,只不过在矩形内部渲染的子组件是文本而已。

当然,RenderParagraph 作为渲染对象也并不是无所不能,他内部完成渲染文本的使命还主要依靠一个  TextPainter 类型的对象 _textPainter。在 RenderParagraph 的 performLayout 方法和 paint 方法中都使用到了 _textPainter 对象来实现文本的最终绘制,部分代码如下:

// 负责布局
@override
void performLayout() {
  final BoxConstraints constraints = this.constraints;
  _layoutTextWithConstraints(constraints);

  // 获取文本大小并布局子组件
  final Size textSize = _textPainter.size;
  size = constraints.constrain(textSize);
  // ...
}

// 负责绘制
@override
void paint(PaintingContext context, Offset offset) {
  _textPainter.paint(context.canvas, offset);
  // ...
}

所以说, TextPainter 才是我们下一步要继续深挖的内容。

TextPainter

到了 TextPainter  就处于架构图中 framework 的 Painting 层了,我们正一步一步逼近根源,在这里,Flutter 会将每种样式的文本分段构成 ui.Paragraph 对象 _paragraph, 每个 ui.Paragraph 对象又由 ParagraphBuilder 生成,ParagraphBuilder 可以接受一个 ui.ParagraphStyle 对象,主要用来配置每个 Paragraph 的最大行数、文本方向、截断方式等信息(在上层我们可以通过 TextStyle 来定义)。TextPainter 类中的 _createParagraphStyle  方法专门用来生成 ui.ParagraphStyle 对象,如下:

ui.ParagraphStyle _createParagraphStyle([ TextDirection defaultTextDirection ]) {
  // ...
  return _text.style?.getParagraphStyle(
    textAlign: textAlign,
    textDirection: textDirection ?? defaultTextDirection,
    textScaleFactor: textScaleFactor,
    maxLines: _maxLines,
    textHeightBehavior: _textHeightBehavior,
    ellipsis: _ellipsis,
    locale: _locale,
    strutStyle: _strutStyle,
  ) ?? ui.ParagraphStyle(
    textAlign: textAlign,
    textDirection: textDirection ?? defaultTextDirection,
    maxLines: maxLines,
    textHeightBehavior: _textHeightBehavior,
    ellipsis: ellipsis,
    locale: locale,
  );
}

从这段代码可以看出,用户如果没有自定义样式,TextPainter 也会为文本设置一个默认样式。ui.Paragraph 对象 _paragraph 就由此生成,下面就是 TextPainter 中 layout 方法的部分代码(该方法会在 RenderParagraph 布局内部文本时被调用):

void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) {
  if (_paragraph == null) {
    // 创建具有特性样式的 ParagraphBuilder
    final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
    _text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
    // 创建出 ui.Paragraph 对象
    _paragraph = builder.build();
  }
  // ...
}

最后,TextPainter 直接在 paint 方法中将生成的 _paragraph 对象传入 canvas.drawParagraph 就可以把文本在画布中渲染出来了:

void paint(Canvas canvas, Offset offset) {
  // ...
  canvas.drawParagraph(_paragraph, offset);
}

这里,ParagraphBuilder 和 Paragraph 这两个类其实已经处于 framework 层中的最底层 Foundation 了,读者们也可以发现其中大部分的函数已经变成了在引擎层实现的空函数了。

79532d7be7a0f3528e1aafd0e5f00338.png

文本渲染引擎

到这里,我们已经自顶向下走过了 Flutter 整个 framework 层了,由于引擎层代码主要使用 C/C++ 编写,因此没办法在 Android Studio/VSCode 直接阅读,感兴趣的读者们可以自己在本地编译一份 Flutter Engine 代码,也可以直接到官方仓库(https://github.com/flutter/engine/)在线阅读。

Flutter 引擎层用来渲染文本的引擎叫做 LibTxt,代码集中放在 engine/third_party/txt/  中,该库依赖了 Minikin、ICU、HarfBuzz、Skia 等多个其他引擎库,内容比较庞大,我们暂不深究这一块内容。

最佳实践

理论的描述终究还是有点抽象,下面我们就来自己定义一个用来渲染文本的组件,其中就涉及到了对 TextPainter 和 Paragraph 的改写,

我们要做的这个文本组件 Flutter 官方并未提供,他可以用来将传入的文本垂直展示,因为正好可以用来展示我们的中国诗词,所以我将它命名为了 PoetryText,使用方法如下:

PoetryText(
  text: TextSpan(
    text: "床前明月光,疑似地上霜,举头望明月,低头思故乡。",
    style: TextStyle(
      color: Colors.black,
      fontSize: 30,
    ),
  ),
)

整体效果:

abfd49426e950bce832f37312c6e4942.png

如上所示,PoetryText 组件使用起来非常简单,接收一个 text 参数,传入一个特定样式的 TextSpan 即可,代码如下:

class PoetryText extends LeafRenderObjectWidget {
  const PoetryText({
    Key key,
    this.text,
  }) : super(key: key);

  final TextSpan text;

  @override
  RenderVerticalText createRenderObject(BuildContext context) {
    return RenderVerticalText(text);
  }

  @override
  void updateRenderObject(
      BuildContext context, RenderVerticalText renderObject) {
    renderObject.text = text;
  }
}

PoetryText 继承自 LeafRenderObjectWidget,它的 createRenderObject 也方法返回一个我们自定义的渲染对象 RenderVerticalText,而这里的 updateRenderObject 方法主要用于 Widget 的配置更新。

RenderVerticalText 的代码也很简单:

class RenderVerticalText extends RenderBox {
  RenderVerticalText(TextSpan text)
      : _textPainter = VerticalTextPainter(text: text);

  final VerticalTextPainter _textPainter;

  TextSpan get text => _textPainter.text;

  // 设置渲染的文本内容
  set text(TextSpan value) {
    // 比较新旧文本
    switch (_textPainter.text.compareTo(value)) {
      case RenderComparison.identical:
      case RenderComparison.metadata:
        return;
      case RenderComparison.paint:
        _textPainter.text = value;
        markNeedsPaint();
        break;
      case RenderComparison.layout:
        _textPainter.text = value;
        markNeedsLayout();
        break;
    }
  }

  // 布局组件大小
  void _layoutText({
    double minHeight = 0.0,
    double maxHeight = double.infinity,
  }) {
    _textPainter.layout(
      minHeight: minHeight,
      maxHeight: maxHeight,
    );
  }

  void _layoutTextWithConstraints(BoxConstraints constraints) {
    _layoutText(
      minHeight: constraints.minHeight,
      maxHeight: constraints.maxHeight,
    );
  }

  // 计算盒子最小高度
  @override
  double computeMinIntrinsicHeight(double width) {
    _layoutText();
    return _textPainter.minIntrinsicHeight;
  }

  // 计算盒子最大高度
  @override
  double computeMaxIntrinsicHeight(double width) {
    _layoutText();
    return _textPainter.maxIntrinsicHeight;
  }

  double _computeIntrinsicWidth(double height) {
    _layoutText(minHeight: height, maxHeight: height);
    return _textPainter.width;
  }

  // 计算盒子最小宽度
  @override
  double computeMinIntrinsicWidth(double height) {
    return _computeIntrinsicWidth(height);
  }

  // 计算盒子最大宽度
  @override
  double computeMaxIntrinsicWidth(double height) {
    return _computeIntrinsicWidth(height);
  }

  // 返回从文本顶部到第一个基线的距离
  @override
  double computeDistanceToActualBaseline(TextBaseline baseline) {
    return _textPainter.height;
  }

  // 布局
  @override
  void performLayout() {
    _layoutTextWithConstraints(constraints);
    final Size textSize = _textPainter.size;
    size = constraints.constrain(textSize);
  }

  // 渲染
  @override
  void paint(PaintingContext context, Offset offset) {
    _textPainter.paint(context.canvas, offset);
  }
}

每个 RenderObject 都会经历 layout 和 paint 两个过程,如这里继承自 RenderBox   的   RenderVerticalText ,其中重写了一系列方法,在 performLayout()paint() 分别用来做布局和渲染两个过程。

当然,在屏幕中渲染的任务还是主要交给了 VerticalTextPainter 类型的对象 _textPainter,它的代码如下:

class VerticalTextPainter {
  VerticalTextPainter({TextSpan text}) : _text = text;

  VerticalParagraph _paragraph;
  bool _needsLayout = true;

  TextSpan get text => _text;
  TextSpan _text;

  // ...

  // 供 RenderVerticalText 布局时调用
  void layout({double minHeight = 0.0, double maxHeight = double.infinity}) {
    if (!_needsLayout &&
        minHeight == _lastMinHeight &&
        maxHeight == _lastMaxHeight) return;
    _needsLayout = false;
    if (_paragraph == null) {
      final VerticalParagraphBuilder builder = VerticalParagraphBuilder(null);
      _applyTextSpan(builder, _text);
      _paragraph = builder.build();
    }
    _lastMinHeight = minHeight;
    _lastMaxHeight = maxHeight;
    // 调用 _paragraph 的 layout 方法
    _paragraph.layout(VerticalParagraphConstraints(height: maxHeight));
    if (minHeight != maxHeight) {
      final double newHeight = maxIntrinsicHeight.clamp(minHeight, maxHeight);
      if (newHeight != height)
        _paragraph.layout(VerticalParagraphConstraints(height: newHeight));
    }
  }

  // 设置 VerticalParagraphBuilder 参数
  void _applyTextSpan(VerticalParagraphBuilder builder, TextSpan textSpan) {
    final style = textSpan.style;
    final text = textSpan.text;
    final bool hasStyle = style != null;
    if (hasStyle) {
      builder.textStyle = style;
    }
    if (text != null) {
      builder.text = text;
    }
  }

  // 供 RenderVerticalText 绘制时调用
  void paint(Canvas canvas, Offset offset) {
    _paragraph.draw(canvas, offset);
  }
}

该类源自 TextPainter,TextPainter 的默认的实现是将文本水平绘制,而这里我们就可以修改部分逻辑,通过传入的文本定义自己文本组件的宽高,实现垂直展示的文本组件。

与 Flutter 源码保持一致,我们再将任务交给与 Flutter 中 Paragraph 对应的 VerticalParagraph:

class VerticalParagraph {
  VerticalParagraph(this._paragraphStyle, this._textStyle, this._text);
  ui.ParagraphStyle _paragraphStyle;
  ui.TextStyle _textStyle;String _text;// ...
  List _words = [];void layout(VerticalParagraphConstraints constraints) =>
      _layout(constraints.height);void _layout(double height) {if (height == _height) {return;
    }int count = _text.length;for (int i=0; i      _addWord(i);          // 保存文本中的每个字
    }
    _calculateLineBreaks(height);  // 计算换行
    _calculateWidth();            // 计算宽度
    _height = height;
    _calculateIntrinsicHeight();   // 计算高度
  }void _addWord(int index) {final builder = ui.ParagraphBuilder(_paragraphStyle)
      ..pushStyle(_textStyle)
      ..addText(_text.substring(index, index + 1));final paragraph = builder.build();
    paragraph.layout(ui.ParagraphConstraints(width: double.infinity));// 将每个字都保存在一个 ui.Paragraph 对象中,并封装在 Word 放入 _words 列表final run = Word(index, paragraph);
    _words.add(run);
  }List _lines = [];void _calculateLineBreaks(double maxLineLength) {if (_words.isEmpty) {return;
    }if (_lines.isNotEmpty) {
      _lines.clear();
    }int start &#61; 0;int end;double lineWidth &#61; 0;double lineHeight &#61; 0;// 遍历之前保存的每一个 Word 对象for (int i&#61;0; i<_words.length>      end &#61; i;final word &#61; _words[i];final wordWidth &#61; word.paragraph.maxIntrinsicWidth;final wordHeight &#61; word.paragraph.height;// 遇到 “&#xff0c;”、“。” 换行&#xff0c;保存每行的宽度和高度&#xff0c;调用 _addLine 放入 _lines 列表中if (_text.substring(i, i &#43; 1) &#61;&#61; "&#xff0c;" || _text.substring(i, i &#43; 1) &#61;&#61; "。") {
        lineWidth &#43;&#61; math.max(lineWidth, wordWidth);
        _addLine(start, end&#43;1, lineWidth, lineHeight);
        start &#61; end &#43; 1;
        lineWidth &#61; 0;
        lineHeight &#61; 0;
      } else {// 一行未结束&#xff0c;以竖直方向计算&#xff0c;该行的整体高度应该加上一个文字的高度
        lineHeight &#43;&#61; wordHeight;
      }
    }
    end &#61; _words.length;if (start       _addLine(start, end, lineWidth, lineHeight);
    }
  }void _addLine(int start, int end, double width, double height) {final bounds &#61; Rect.fromLTRB(0, 0, width, height);final LineInfo lineInfo &#61; LineInfo(start, end, bounds);
    _lines.add(lineInfo);
  }// 宽度为各行 “诗” 宽度的总和void _calculateWidth() {double sum &#61; 0;for (LineInfo line in _lines) {
      sum &#43;&#61; line.bounds.width;
    }
    _width &#61; sum;
  }// 高度取诗中最长的一行(以竖直方向为高)void _calculateIntrinsicHeight() {double sum &#61; 0;double maxRunHeight &#61; 0;for (LineInfo line in _lines) {
      sum &#43;&#61; line.bounds.width;
      maxRunHeight &#61; math.max(line.bounds.height, maxRunHeight);
    }
    _minIntrinsicHeight &#61; maxRunHeight;
    _maxIntrinsicHeight &#61; maxRunHeight;
  }// 计算完每个文字和每行诗的宽高后后&#xff0c;// 就可以在这里将文本绘制到 canvas 了。void draw(Canvas canvas, Offset offset) {
    canvas.save();// 移至开始绘制的位置
    canvas.translate(offset.dx, offset.dy);// 绘制每一行for (LineInfo line in _lines) {// 移到绘制该行的开始处
      canvas.translate(line.bounds.width &#43; 20, 0);// 遍历改行每一个 worddouble dy &#61; 0;for (int i &#61; line.textRunStart; i         // 绘制每行诗中的文字 ui.Paragraph&#xff0c;偏移量为该字位于所在行的位置
        canvas.drawParagraph(_words[i].paragraph, Offset(0, dy));
        dy &#43;&#61; _words[i].paragraph.height;
      }
    }
    canvas.restore();
  }
}

如上代码所示&#xff0c;便可以真正的将传入的文本绘制在系统提供给我们的 canvas 中了&#xff0c;其中依附下层我们需要做的仅仅是将传入的文本使用 ui.ParagraphBuilder 封装在 ui.Paragraph 对象中&#xff0c;然后再绘制出将该对象传给 canvas.drawParagraph() 即可。

这样&#xff0c;我们自定义的 PoetryText 组件就完成了&#xff0c;包含的几个重要部分如下&#xff1a;

  • PoetryText&#xff0c;继承 LeafRenderObjectWidget 的 Widget。
  • RenderVerticalText&#xff0c;继承 RenderBox 的组件 的 RenderObject。
  • VerticalTextPainter&#xff0c;改写自 TextPainter。
  • VerticalParagraph&#xff0c;改写自 Paragraph。

完整代码&#xff0c;参见&#xff1a;https://github.com/MeandNi/flutter_poetry_text

延伸阅读

Flutter.cn&#xff1a;https://api.flutter-io.cn/flutter/dart-ui/dart-ui-library.html

Flutter 中三棵重要的树(渲染过程、布局约束、应用视图的构建等)

相关资源可点击「阅读全文」查看我的博客原文。


我的新书《Flutter 开发之旅从南到北》终于和大家见面了&#xff01;(抽奖送书啦)

抽奖进行中....




推荐阅读
  • 我有一个从C项目编译的.o文件,该文件引用了名为init_static_pool ... [详细]
  • 本文介绍了如何在 Vue 3 组合 API 中正确设置 setup() 函数的 TypeScript 类型,以避免隐式 any 类型的问题。 ... [详细]
  • 在分析Android的Audio系统时,我们对mpAudioPolicy->get_input进行了详细探讨,发现其背后涉及的机制相当复杂。本文将详细介绍这一过程及其背后的实现细节。 ... [详细]
  • C语言是计算机科学和编程领域的基石,许多初学者在学习过程中会感到困惑。本文将详细介绍C语言的基本概念、关键语法和实用示例,帮助你快速上手C语言。 ... [详细]
  • 短视频app源码,Android开发底部滑出菜单首先依赖三方库implementationandroidx.appcompat:appcompat:1.2.0im ... [详细]
  • iOS snow animation
    CTSnowAnimationView.hCTMyCtripCreatedbyalexon1614.Copyright©2016年ctrip.Allrightsreserved.# ... [详细]
  • WPF项目学习.一
    WPF项目搭建版权声明:本文为博主初学经验,未经博主允许不得转载。一、前言记录在学习与制作WPF过程中遇到的解决方案。使用MVVM的优点是数据和视图分离,双向绑定,低耦合,可重用行 ... [详细]
  • Java设计模式详解:解释器模式的应用与实现
    本文详细介绍了Java设计模式中的解释器模式,包括其定义、应用场景、优缺点以及具体的实现示例。通过音乐解释器的例子,帮助读者更好地理解和应用这一模式。 ... [详细]
  • 解决Unreal Engine中UMG按钮长时间按住自动释放的问题
    本文探讨了在Unreal Engine中使用UMG按钮时,长时间按住按钮会导致自动释放的问题,并提供了详细的解决方案。 ... [详细]
  • 自然语言处理(NLP)——LDA模型:对电商购物评论进行情感分析
    目录一、2020数学建模美赛C题简介需求评价内容提供数据二、解题思路三、LDA简介四、代码实现1.数据预处理1.1剔除无用信息1.1.1剔除掉不需要的列1.1.2找出无效评论并剔除 ... [详细]
  • 本文将深入探讨 iOS 中的 Grand Central Dispatch (GCD),并介绍如何利用 GCD 进行高效多线程编程。如果你对线程的基本概念还不熟悉,建议先阅读相关基础资料。 ... [详细]
  • Hadoop的文件操作位于包org.apache.hadoop.fs里面,能够进行新建、删除、修改等操作。比较重要的几个类:(1)Configurati ... [详细]
  • 如果应用程序经常播放密集、急促而又短暂的音效(如游戏音效)那么使用MediaPlayer显得有些不太适合了。因为MediaPlayer存在如下缺点:1)延时时间较长,且资源占用率高 ... [详细]
  • Vue应用预渲染技术详解与实践 ... [详细]
  • 本文介绍了UUID(通用唯一标识符)的概念及其在JavaScript中生成Java兼容UUID的代码实现与优化技巧。UUID是一个128位的唯一标识符,广泛应用于分布式系统中以确保唯一性。文章详细探讨了如何利用JavaScript生成符合Java标准的UUID,并提供了多种优化方法,以提高生成效率和兼容性。 ... [详细]
author-avatar
有海的地方最美_171
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有