• Flutter中那些你需要知道的文本知识!


    通过阅读本文,您将了解到

    1. 文本的组成部分;
    2. Flutter对于文本&段落是如何绘制的;
    3. 明白Flutter Text 背后的逻辑;
    4. 在业务中碰到一些文本显示的问题时,知道从哪些地方去尝试修改。

    前言

    文字是记录语言的书写符号系统,是形、音、义的统一体,是人类最重要的辅助性 交际工具。作为一个Flutter开发者,我们都知道可以通过Text()这个文本组件将文字显示出来。但是这其中的Flutter的字体是怎么组成的?Flutter文本是怎么构建的?Render Tree是怎样绘制文本的…作为本专栏(整个专栏都在与文本打交道)的第一篇文章,让我们从这些原理细节讲起。希望能对你认识Flutter的文本渲染有所帮助。

    注:本文的目的在于让大家了解Flutter中的基本文本知识,快速的带大家了解渲染流程,但并未很深入的分析Flutter文本渲染的原理。

    字体基础理论通用部分

    在整个网络世界中,大家可以将字体理解为一个数字文件,它是一个包含特定大小、粗细和样式的文件。它定义了每个字的形状、大小和图形。

    例如Bariol_Regular.otf。.otf是字体文件格式。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SLGvJ6LK-1669544494694)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b43356354404639bd7b54526a2970da~tplv-k3u1fbpfcp-watermark.image?)]

    有了字体格式后,我们会碰到相同的字体大小却有不同的显示布局这个问题。因为每一个字体格式都定义了它自己的参考大小,每一个字符都是基于这个大小设计的。所以即使设置同样的字体大小,也会有不同的布局。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M19SgzcN-1669544494696)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/541c8653d5964a3b9358d941b2ff3661~tplv-k3u1fbpfcp-watermark.image?)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-flIXYZLi-1669544494697)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/06896234fa3846469fb647a380bc607b~tplv-k3u1fbpfcp-watermark.image?)]

    在Flutter中文本由哪些部分组成?

    Baseline
    • 在Flutter中,每一个字符都会在Baseline(基线)上。有了这个基线后,就算是不同大小的文字也可以处于同一水平线上。Baseline是非常重要的,因为可以通过它测量文本和元素之间的垂直距离。其他还有MiddlelineBottomlineTopline

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VZO6UXAO-1669544494698)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2853b80fe819407b928a6fd93e29a859~tplv-k3u1fbpfcp-watermark.image?)]

    • Baseline的算法公式推导有兴趣的朋友可以自行搜索。
    Text Spacing
    • 文字间距是指一段文本中每个文字之间插入的空间。
    • 在Flutter中可以通过TextStyle下的wordSpacing设置单词与单词之间的间距,通过letterSpacing设置字符与字符之间的间距。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uyChiBG1-1669544494698)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28a543c5d32840ee9f4a48d2ea333688~tplv-k3u1fbpfcp-watermark.image?)]

    Weight
    • Weight是指字体笔画的粗细,在Flutter中通过fontWeight设置。

      常见的有:normalbold,其他还有FontWeight.w100…等粗细值

    TextStyle(
      fontWeight: FontWeight.bold 
    ),
    
    • 1
    • 2
    • 3
    TextSpan

    在Flutter中,我们经常会使用Text()这个组件,但是我们通过阅读Text()的源码后就可以知道,它的build方法返回的就是RichText组件。所以它会呈现为TextSpanSpan指的是字符之间的行距。

    @override
    Widget build(BuildContext context) {
    ...
      Widget result = RichText(
        ...
        text: TextSpan(
          style: effectiveTextStyle,
          text: data,
          children: textSpan != null ? [textSpan!] : null,
        ),
      );
      ...
      return result;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    Height

    在Flutter中,定义了一个TextStyle.height,用于给呈现文本的TextSpan一个准确的行高。

    TextStyle(height: 1)
    
    • 1

    但是我们需要注意,每一种字体格式都定义了自己的字体度量默认高度,这也是为什么即使设置了相同的字体高度,也会有不同的TextSpan的高度。

    让我们来看下这个例子:

    红色是Flutter默认的字体,蓝色是Bariol_Regular字体,绿色是Bellota-Regular字体,看看他们在相同height下不同的框高度。

    • 默认height

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ikr2c80A-1669544494699)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f47d0f38c7443b8bbaad4ac3e15cc04~tplv-k3u1fbpfcp-watermark.image?)]

    • height: 1.0

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qe9aiK52-1669544494700)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fa54e9cf2b28475ea39155ede0583499~tplv-k3u1fbpfcp-watermark.image?)]

    • height:0.8

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tbGTBpwJ-1669544494701)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1733f3d05cb2482fb868d92c7e14d8dc~tplv-k3u1fbpfcp-watermark.image?)]

    这个例子也很好的验证了:

    • 即使使用一样的fontSize,每种字体也都有不同的高度
    • 每一种字体都有不同的基线。

    那么关于Flutter的字体组成我们也可以得到一个结论:使用多种字体大概率会因为基线的不同导致布局不协调!

    Flutter中是如何绘制文本的?

    通过ParagraphFlutter最后绘制文本时都是通过Paragraph完成的!

    // Paragraph paragraph:文本对象
    // Offset offset:文本绘制的位置
    void drawParagraph(Paragraph paragraph, Offset offset)
    
    • 1
    • 2
    • 3

    举个例子: 通过drawParagraph绘制一段文字

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7TkjSos-1669544494702)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fc53245c2bbd4a9c86207b2cf58afbfd~tplv-k3u1fbpfcp-watermark.image?)]

    import 'dart:ui' as ui;
    class TextPainter extends CustomPainter {
      //创建段落构建器
      ParagraphBuilder paragraphBuilder = ParagraphBuilder(
          ParagraphStyle(fontWeight: FontWeight.bold, fontSize: 16))
        ..pushStyle(ui.TextStyle(color: Colors.black))
        ..addText('通过drawParagraph绘制的 Hello Taxze');
    ​
      @override
      void paint(Canvas canvas, Size size) {
        //设置段落宽度
        ParagraphConstraints paragraphConstraints =
            ParagraphConstraints(width: size.width);
        //计算绘制的文本位置及尺寸
        Paragraph paragraph = paragraphBuilder.build()
          ..layout(paragraphConstraints);
        //绘制
        canvas.drawParagraph(paragraph, const Offset(40.0, 50.0));
      }
    ​
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    使用:

    @override
    Widget build(BuildContext context) {
      return Scaffold(
        ...
        body: SizedBox.expand(child: CustomPaint(painter: TextPainter())),
      );
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    SizedBox.expand包裹CustomPaint是为了给ParagraphConstraints(width: size.width)一个size。你也可以用其他的组件包裹它。

    关于Flutter使用CustomPaint绘制文字的实践较为复杂,若要讲清楚绘制的主要知识点,则需要另开一篇文章来讲述。若对这个部分感兴趣的朋友可以阅读下这篇文章:Flutter学习:使用CustomPaint绘制文字 @菠萝橙子丶

    Flutter是如何把一段长文字转变成段落的?

    你有没有想过,Flutter是如何把一段长文字生成下面的这样一个段落的呢?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K8zsfS9R-1669544494703)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6aa369fa78f4762b66bfa5f64026cee~tplv-k3u1fbpfcp-watermark.image?)]

    这张效果图的代码:

    Container(
        color: Colors.red,
        width: 200,
        height: 100,
        margin: EdgeInsets.all(30),
        child: Text(
            "通过drawParagraph绘制的 Taxze Hello....")),
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么其中的自动换行是怎么实现的呢?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hQ2ezYSG-1669544494704)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a4fe3f2943234c1ca5372dc902c53b57~tplv-k3u1fbpfcp-watermark.image?)]
    我们知道,段落指的就是一段文本,我们要给每个字符一个合适的大小和位置。那么Flutter是如何计算这些参数的呢?

    在前文说到过,Flutter最后绘制文本时都是通过Paragraph完成的。Flutter就是通过Paragraph.layout来计算这些参数,而且ParagraphBuilder给每个字符都在渲染前分配了一个偏移量。通过Paragraph可以知道所有占位符的位置和尺寸大小。

    class TextPosition {
      //创建一个表示字符串中特定位置的对象。
      const TextPosition({
        required this.offset,
        this.affinity = TextAffinity.downstream,
      }) : assert(offset != null),
           assert(affinity != null);
      //举个例子:有一个“Hello”字符,offset = 0表示光标在字符H之前,offset = 5表示光标在字符o之后。
      final int offset;
      final TextAffinity affinity;
    ​
      @override
      bool operator ==(Object other) {
        if (other.runtimeType != runtimeType)
          return false;
        return other is TextPosition
            && other.offset == offset
            && other.affinity == affinity;
      }
    ​
      @override
      int get hashCode => Object.hash(offset, affinity);
    ​
      @override
      String toString() {
        return 'TextPosition(offset: $offset, affinity: $affinity)';
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

    Text()背后的大哥有哪些?

    –文本的渲染流程

    从之前讲述的知识点,Text()组件它的build方法返回的就是RichText,但是Flutter最后绘制文本时又都是通过Paragraph完成的!那么其中的完整的一个流程是怎么样的呢?话不多说,先上图!

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dDMUcy7t-1669544494704)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dbce4fff785640e78c6c743b6ed6c409~tplv-k3u1fbpfcp-watermark.image?)]

    组件层

    如图所示,每当我们使用Text组件时,它实际上创建的是RichText组件。但是RichTextText不同的是,TextString作为参数,而RichTextInlinSpan作为参数(或者说是TextSpan)。

    const Text(String this.data)
        
    //通过Text.rich构造函数传给RichText
    const Text.rich(InlineSpan this.textSpan)
        
    RichText(
          ...
          text: TextSpan(
            style: effectiveTextStyle,
            text: data,
            children: textSpan != null ? [textSpan!] : null,
          ),
    )
    
    //TextSpan继承于InlineSpan
    class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotation {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    RichText接收TextSpan,而每一个TextSpan都有更多的子TextSpan,这些子TextSpan会 继承父TextSpan的样式。例如:
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PF2spzd5-1669544494705)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/233e496530c94552aca073ef9b90a60c~tplv-k3u1fbpfcp-watermark.image?)]

     RichText(
        text: TextSpan(
            style: Theme.of(context)
                .textTheme
                .bodyText1
                ?.copyWith(fontSize: 24),
            children: [
          TextSpan(
            text: 'Taxze ',
          ),
          TextSpan(text: 'blog', style: TextStyle(color: Colors.blue)),
          TextSpan(
            text: ' Flutter',
          ),
          TextSpan(text: '稀土掘金', style: TextStyle(color: Colors.blue)),
        ]))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    不过,RichText本身是MultiChildRenderObjectWidget的子类。它们之间有这样的继承关系:

    class RichText extends MultiChildRenderObjectWidget {}
    abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {}
    
    • 1
    • 2

    MultiChildRenderObjectWidget产生的MultiChildRenderObjectElement则是这样的关系:

    class MultiChildRenderObjectElement extends RenderObjectElement {}
    abstract class RenderObjectElement extends Element {}
    
    • 1
    • 2

    RichText实际上是需要一个InlineSpan,而InlineSpan可以是TextSpan或者是WidgetSpan。对WidgetSpan有兴趣的朋友,可以参考官方的文档WidgetSpan

    到这里为止,我们可以将RichText(包括RichText)之前的所有划分为组件层,那么我们现在就要进入渲染层了。

    渲染层

    我们已经知道了RichText会创建一个渲染对象— RenderParagraph,那么RenderParagraph是干什么的呢?

    RichTextMultiChildRenderObjectWidget的子类,它会把MultiChildRenderObjectElement往下传递,但是此时MultiChildRenderObjectElement没有渲染,它还没有什么作用。这个时候RichText会给它一个RenderParagraphRenderParagraph会收到RenderPadding的指令,这个时候MultiChildRenderObjectElement就准备好了一切,就可以开始工作了。

    这样解释可能有点抽象,那么我们来看下这个例子:

    body: Container(
                alignment: Alignment.center,
                child: Text("Taxze Hello"), ,
    )
    
    • 1
    • 2
    • 3
    • 4

    很简单的一个小例子,它的结构也很清晰:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhsYCYUC-1669544494705)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef477b4af1b849f3a3cf5aa3f68c8466~tplv-k3u1fbpfcp-watermark.image?)]

    当Flutter把三棵树都构建完后:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zinkstt2-1669544494706)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58bc0cb3d88a4dc1b08305026eef423a~tplv-k3u1fbpfcp-watermark.image?)]

    那么当我们改变文本时,又会发生什么呢?

    最先改变的当然是组件层:

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-94R0jawX-1669544494706)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/313481d32f85479c8eb3dd8ffe47b545~tplv-k3u1fbpfcp-watermark.image?)]

    我们会有一个 “新” 的组件树。不过你真的认为都是新的吗?Flutter会充分利用现有的元素,让我们来看下这个名为canUpdate的方法吧。

    static bool canUpdate(Widget oldWidget, Widget newWidget) {
      return oldWidget.runtimeType == newWidget.runtimeType
          && oldWidget.key == newWidget.key;
    }
    
    • 1
    • 2
    • 3
    • 4

    通过这个方法,Flutter可以检查一个老的组件的Typekey,并把它和新的组件进行比较。如果它们都相同的话,就不需要更新。

    所以就算更新后,Container更新之后它还是存在的,而且我们没有给它一个Key,所以OldContainerNewContainer是完全相同的。Align、Text、以及RichText它们的Type和Key都没有变化,重新构建它们没有什么意义,所以它们都不会有更新。

    到这里,我猜你肯定会问,都没有更新,那么文本是如何改变的呢?

    那么我们就要讲到组件中的属性了。组件除了具有Type和Key之外,还有属性。属性的改变会使RenderParagraph显示新的文本。

    不过关于文本的更改渲染到现在我们都是在纸上谈兵,那么我们现在就来用一个简单的例子去验证之前的结论。

      bool _isFirst = true;
    ​
      @override
      Widget build(BuildContext context) {
        return Scaffold(
            floatingActionButton: FloatingActionButton(
              child: const Icon(Icons.swap_horiz),
              onPressed: () {
                setState(() {
                  _isFirst = !_isFirst;
                });
              },
            ),
            body: _isFirst ? first() : second());
      }
    }
    ​
    Widget first() => Container(
          alignment: Alignment.center,
          child: const Text("Taxze First"),
    );
    ​
    Widget second() => Container(
          alignment: Alignment.center,
          child: const Text("Taxze Second"),
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    非常简单的一个例子,点击按钮更改显示文字。当我们点下按钮时,文本改变后,所有的组件都会重用,Flutter只会重建RenderPadding

    绘制层

    在渲染层中,我们最后发生文本变化都在RenderParagraph上,不过RenderParagraph并不会直接的绘制文本,而是会创建一TextPainter来管理绘制的工作。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JgUMZp4Z-1669544494706)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f358e123221c48ef8891ea7a5ef49620~tplv-k3u1fbpfcp-watermark.image?)]
    不过,TextPainter做的事和它的名字完全不一样,你以为就是它来绘制文本的吗?No~

    实际上,它只是负责管理绘制的事,但它自己不会去绘制(当老板)。

    基础层

    到现在为止,你会发现,讲了那么多,但是还是没有那个ta去真正的绘制文本,就好像之前的所有的组件都在当中间商,把活外包了出去。

    到了Flutter的最底层,你会发现有一个ParagraphBuilderParagraph,在前面关于Flutter如何绘制文本中,我们也提到了Flutter最后绘制文本时都是通过Paragraph完成的,而TextPainter是负责创建ParagraphBuilder的,但是当你翻看Paragraph类的源码时,你会发现,大部分的函数都是空函数,原来这哥们也没干活啊!

    @pragma('vm:entry-point')
    class Paragraph extends NativeFieldWrapperClass1 {
      @pragma('vm:entry-point')
      Paragraph._();
    ​
      bool _needsLayout = true;
    ​
      double get width native 'Paragraph_width';
    ​
      double get height native 'Paragraph_height';
    ​
      double get longestLine native 'Paragraph_longestLine';
    ​
      double get minIntrinsicWidth native 'Paragraph_minIntrinsicWidth';
    ​
      double get maxIntrinsicWidth native 'Paragraph_maxIntrinsicWidth';
    ​
      double get alphabeticBaseline native 'Paragraph_alphabeticBaseline';
      ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    引擎层

    ParagraphParagraphBuilder这两个类都将绘制的工作交给了Flutter Engine后,我们也要将视线放到SkParagraph上了,在以前Flutter Engine处理文本绘制的库是LibText。后面切换成了SkParagraph,但是也实现了和Libtext相同的API。对于Flutter引擎在这篇文章中只做一个简单的说明,若对引擎感兴趣的朋友可以自己编译FlutterEngine进行学习,或者在线阅读

    –更详细更深入的Flutter文本渲染原理有兴趣的朋友可以阅读这篇文章

    解决Flutter文本基线不对齐的问题

    经常在各大Flutter交流群中看到有哥们问这样的问题:Row中,两个文本没有对齐,这怎么处理呀?

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLL0YCuo-1669544494707)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/327583bd15264122a5b1e9c4bc2c0aca~tplv-k3u1fbpfcp-watermark.image?)]
    展示图代码:

    Center(
      child: Row(
        children: [
          ColoredBox(
            color: Colors.amber,
            child: Text.rich(TextSpan(children: [
              TextSpan(text: "¥999", style: TextStyle(fontSize: 28)),
              TextSpan(text: ".9", style: TextStyle(fontSize: 14)),
            ])),
          ),
          ColoredBox(
            color: Colors.red,
            child: Text.rich(TextSpan(children: [
              TextSpan(text: "123", style: TextStyle(fontSize: 12)),
            ])),
          ),
        ],
      ),
    )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    其实处理这个问题很简单,只需要给Row加上:

    textBaseline: TextBaseline.alphabetic,
    crossAxisAlignment: CrossAxisAlignment.baseline,
    
    • 1
    • 2

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JFwhozn4-1669544494707)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/add602d2e218444e8ccf7c3f157b108b~tplv-k3u1fbpfcp-watermark.image?)]

    关于更多有关文本的布局问题大家可以查看官方这篇文档

    尾述

    在这篇文章中,我们知道了文本是由什么组成的,Flutter是怎样将文本显示到屏幕上的。但是这也只是Flutter关于文本的一小部分,关于文本的编辑…等内容将会在后续的文章中继续探索。希望这篇文章能对你有所帮助,有问题欢迎在评论区留言讨论~

    参考&推荐阅读

    Flutter Text Rendering — @Jonathan Sande

    书后拓展:Flutter 中一行文字到屏幕上,渲染全过程!@MeandNi

    Flutter 小技巧之玩转字体渲染和问题修复@恋猫de小郭

    Flutter学习:使用CustomPaint绘制文字 @菠萝橙子丶

    关于我

    Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里

    如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?😝

    — 文字是人类用符号记录表达信息以传之久远的方式和工具。

  • 相关阅读:
    【超好懂的比赛题解】2022CCPC四川省赛 个人题解
    UE5、CesiumForUnreal实现加载GeoJson绘制单面(Polygon)功能(StaticMesh方式)
    还在为日期计算烦恼?Java8帮你轻松搞定
    hdu 1528-Card Game Cheater(贪心算法)
    【EMC专题】EMC是常规设计准则的例外情况
    2.9 PE结构:重建导入表结构
    金仓数据库KingbaseES客户端应用参考手册--3. createdb
    Sping面试题
    【100个 Unity实用技能】| C#中 Add 和 AddRange 的区别 及 使用示例
    高阶 CSS 技巧在复杂动效中的应用
  • 原文地址:https://blog.csdn.net/txaz6/article/details/128067688