Flutter中那些你需要知道的文本知識!-創(chuàng)新互聯(lián)

通過閱讀本文,您將了解到

創(chuàng)新互聯(lián)為客戶提供專業(yè)的成都網(wǎng)站設(shè)計、成都網(wǎng)站制作、程序、域名、空間一條龍服務(wù),提供基于WEB的系統(tǒng)開發(fā). 服務(wù)項目涵蓋了網(wǎng)頁設(shè)計、網(wǎng)站程序開發(fā)、WEB系統(tǒng)開發(fā)、微信二次開發(fā)、成都手機網(wǎng)站制作等網(wǎng)站方面業(yè)務(wù)。
  1. 文本的組成部分;
  2. Flutter對于文本&段落是如何繪制的;
  3. 明白Flutter Text 背后的邏輯;
  4. 在業(yè)務(wù)中碰到一些文本顯示的問題時,知道從哪些地方去嘗試修改。
前言

文字是記錄語言的書寫符號系統(tǒng),是形、音、義的統(tǒng)一體,是人類最重要的輔助性 交際工具。作為一個Flutter開發(fā)者,我們都知道可以通過Text()這個文本組件將文字顯示出來。但是這其中的Flutter的字體是怎么組成的?Flutter文本是怎么構(gòu)建的?Render Tree是怎樣繪制文本的…作為本專欄(整個專欄都在與文本打交道)的第一篇文章,讓我們從這些原理細節(jié)講起。希望能對你認識Flutter的文本渲染有所幫助。

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

字體基礎(chǔ)理論通用部分

在整個網(wǎng)絡(luò)世界中,大家可以將字體理解為一個數(shù)字文件,它是一個包含特定大小、粗細和樣式的文件。它定義了每個字的形狀、大小和圖形。

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

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-SLGvJ6LK-1669544494694)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b43356354404639bd7b54526a2970da~tplv-k3u1fbpfcp-watermark.image?)]

有了字體格式后,我們會碰到相同的字體大小卻有不同的顯示布局這個問題。因為每一個字體格式都定義了它自己的參考大小,每一個字符都是基于這個大小設(shè)計的。所以即使設(shè)置同樣的字體大小,也會有不同的布局。

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-M19SgzcN-1669544494696)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/541c8653d5964a3b9358d941b2ff3661~tplv-k3u1fbpfcp-watermark.image?)]

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-flIXYZLi-1669544494697)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/06896234fa3846469fb647a380bc607b~tplv-k3u1fbpfcp-watermark.image?)]

在Flutter中文本由哪些部分組成? Baseline
  • 在Flutter中,每一個字符都會在Baseline(基線)上。有了這個基線后,就算是不同大小的文字也可以處于同一水平線上。Baseline是非常重要的,因為可以通過它測量文本和元素之間的垂直距離。其他還有Middleline、Bottomline、Topline。

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-VZO6UXAO-1669544494698)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2853b80fe819407b928a6fd93e29a859~tplv-k3u1fbpfcp-watermark.image?)]

  • Baseline的算法公式推導(dǎo)有興趣的朋友可以自行搜索。
Text Spacing
  • 文字間距是指一段文本中每個文字之間插入的空間。
  • 在Flutter中可以通過TextStyle下的wordSpacing設(shè)置單詞與單詞之間的間距,通過letterSpacing設(shè)置字符與字符之間的間距。

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-uyChiBG1-1669544494698)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28a543c5d32840ee9f4a48d2ea333688~tplv-k3u1fbpfcp-watermark.image?)]

Weight
  • Weight是指字體筆畫的粗細,在Flutter中通過fontWeight設(shè)置。

    常見的有:normal、bold,其他還有FontWeight.w100…等粗細值

TextStyle(
  fontWeight: FontWeight.bold 
),
TextSpan

在Flutter中,我們經(jīng)常會使用Text()這個組件,但是我們通過閱讀Text()的源碼后就可以知道,它的build方法返回的就是RichText組件。所以它會呈現(xiàn)為TextSpan。Span指的是字符之間的行距。

@override
Widget build(BuildContext context) {
...
  Widget result = RichText(
    ...
    text: TextSpan(
      style: effectiveTextStyle,
      text: data,
      children: textSpan != null ?[textSpan!] : null,
    ),
  );
  ...
  return result;
}
Height

在Flutter中,定義了一個TextStyle.height,用于給呈現(xiàn)文本的TextSpan一個準確的行高。

TextStyle(height: 1)

但是我們需要注意,每一種字體格式都定義了自己的字體度量默認高度,這也是為什么即使設(shè)置了相同的字體高度,也會有不同的TextSpan的高度。

讓我們來看下這個例子:

紅色是Flutter默認的字體,藍色是Bariol_Regular字體,綠色是Bellota-Regular字體,看看他們在相同height下不同的框高度。

  • 默認height

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ikr2c80A-1669544494699)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f47d0f38c7443b8bbaad4ac3e15cc04~tplv-k3u1fbpfcp-watermark.image?)]

  • height: 1.0

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qe9aiK52-1669544494700)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fa54e9cf2b28475ea39155ede0583499~tplv-k3u1fbpfcp-watermark.image?)]

  • height:0.8

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tbGTBpwJ-1669544494701)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1733f3d05cb2482fb868d92c7e14d8dc~tplv-k3u1fbpfcp-watermark.image?)]

這個例子也很好的驗證了:

  • 即使使用一樣的fontSize,每種字體也都有不同的高度
  • 每一種字體都有不同的基線。

那么關(guān)于Flutter的字體組成我們也可以得到一個結(jié)論:使用多種字體大概率會因為基線的不同導(dǎo)致布局不協(xié)調(diào)!

Flutter中是如何繪制文本的?

通過Paragraph,Flutter最后繪制文本時都是通過Paragraph完成的!

// Paragraph paragraph:文本對象
// Offset offset:文本繪制的位置
void drawParagraph(Paragraph paragraph, Offset offset)

舉個例子: 通過drawParagraph繪制一段文字

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(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 {
  //創(chuàng)建段落構(gòu)建器
  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) {
    //設(shè)置段落寬度
    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;
}

使用:

@override
Widget build(BuildContext context) {
  return Scaffold(
    ...
    body: SizedBox.expand(child: CustomPaint(painter: TextPainter())),
  );
}

SizedBox.expand包裹CustomPaint是為了給ParagraphConstraints(width: size.width)一個size。你也可以用其他的組件包裹它。

關(guān)于Flutter使用CustomPaint繪制文字的實踐較為復(fù)雜,若要講清楚繪制的主要知識點,則需要另開一篇文章來講述。若對這個部分感興趣的朋友可以閱讀下這篇文章:Flutter學(xué)習(xí):使用CustomPaint繪制文字 — @菠蘿橙子丶

Flutter是如何把一段長文字轉(zhuǎn)變成段落的?

你有沒有想過,F(xiàn)lutter是如何把一段長文字生成下面的這樣一個段落的呢?

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(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....")),

那么其中的自動換行是怎么實現(xiàn)的呢?

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hQ2ezYSG-1669544494704)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a4fe3f2943234c1ca5372dc902c53b57~tplv-k3u1fbpfcp-watermark.image?)]
我們知道,段落指的就是一段文本,我們要給每個字符一個合適的大小和位置。那么Flutter是如何計算這些參數(shù)的呢?

在前文說到過,F(xiàn)lutter最后繪制文本時都是通過Paragraph完成的。Flutter就是通過Paragraph.layout來計算這些參數(shù),而且ParagraphBuilder給每個字符都在渲染前分配了一個偏移量。通過Paragraph可以知道所有占位符的位置和尺寸大小。

class TextPosition {
  //創(chuàng)建一個表示字符串中特定位置的對象。
  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)';
  }
}
Text()背后的大哥有哪些?

–文本的渲染流程

從之前講述的知識點,Text()組件它的build方法返回的就是RichText,但是Flutter最后繪制文本時又都是通過Paragraph完成的!那么其中的完整的一個流程是怎么樣的呢?話不多說,先上圖!

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dDMUcy7t-1669544494704)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dbce4fff785640e78c6c743b6ed6c409~tplv-k3u1fbpfcp-watermark.image?)]

組件層

如圖所示,每當我們使用Text組件時,它實際上創(chuàng)建的是RichText組件。但是RichTextText不同的是,TextString作為參數(shù),而RichTextInlinSpan作為參數(shù)(或者說是TextSpan)。

const Text(String this.data)
    
//通過Text.rich構(gòu)造函數(shù)傳給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 {}

RichText接收TextSpan,而每一個TextSpan都有更多的子TextSpan,這些子TextSpan會 繼承父TextSpan的樣式。例如:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(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)),
    ]))

不過,RichText本身是MultiChildRenderObjectWidget的子類。它們之間有這樣的繼承關(guān)系:

class RichText extends MultiChildRenderObjectWidget {}
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {}

MultiChildRenderObjectWidget產(chǎn)生的MultiChildRenderObjectElement則是這樣的關(guān)系:

class MultiChildRenderObjectElement extends RenderObjectElement {}
abstract class RenderObjectElement extends Element {}

RichText實際上是需要一個InlineSpan,而InlineSpan可以是TextSpan或者是WidgetSpan。對WidgetSpan有興趣的朋友,可以參考官方的文檔WidgetSpan。

到這里為止,我們可以將RichText(包括RichText)之前的所有劃分為組件層,那么我們現(xiàn)在就要進入渲染層了。

渲染層

我們已經(jīng)知道了RichText會創(chuàng)建一個渲染對象—RenderParagraph,那么RenderParagraph是干什么的呢?

RichTextMultiChildRenderObjectWidget的子類,它會把MultiChildRenderObjectElement往下傳遞,但是此時MultiChildRenderObjectElement沒有渲染,它還沒有什么作用。這個時候RichText會給它一個RenderParagraph,RenderParagraph會收到RenderPadding的指令,這個時候MultiChildRenderObjectElement就準備好了一切,就可以開始工作了。

這樣解釋可能有點抽象,那么我們來看下這個例子:

body: Container(
            alignment: Alignment.center,
            child: Text("Taxze Hello"), ,
)

很簡單的一個小例子,它的結(jié)構(gòu)也很清晰:

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-yhsYCYUC-1669544494705)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef477b4af1b849f3a3cf5aa3f68c8466~tplv-k3u1fbpfcp-watermark.image?)]

當Flutter把三棵樹都構(gòu)建完后:

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-zinkstt2-1669544494706)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58bc0cb3d88a4dc1b08305026eef423a~tplv-k3u1fbpfcp-watermark.image?)]

那么當我們改變文本時,又會發(fā)生什么呢?

最先改變的當然是組件層:

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-94R0jawX-1669544494706)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/313481d32f85479c8eb3dd8ffe47b545~tplv-k3u1fbpfcp-watermark.image?)]

我們會有一個 “新” 的組件樹。不過你真的認為都是新的嗎?Flutter會充分利用現(xiàn)有的元素,讓我們來看下這個名為canUpdate的方法吧。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

通過這個方法,F(xiàn)lutter可以檢查一個老的組件的Typekey,并把它和新的組件進行比較。如果它們都相同的話,就不需要更新。

所以就算更新后,Container更新之后它還是存在的,而且我們沒有給它一個Key,所以OldContainerNewContainer是完全相同的。Align、Text、以及RichText它們的Type和Key都沒有變化,重新構(gòu)建它們沒有什么意義,所以它們都不會有更新。

到這里,我猜你肯定會問,都沒有更新,那么文本是如何改變的呢?

那么我們就要講到組件中的屬性了。組件除了具有Type和Key之外,還有屬性。屬性的改變會使RenderParagraph顯示新的文本。

不過關(guān)于文本的更改渲染到現(xiàn)在我們都是在紙上談兵,那么我們現(xiàn)在就來用一個簡單的例子去驗證之前的結(jié)論。

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"),
);

非常簡單的一個例子,點擊按鈕更改顯示文字。當我們點下按鈕時,文本改變后,所有的組件都會重用,F(xiàn)lutter只會重建RenderPadding。

繪制層

在渲染層中,我們最后發(fā)生文本變化都在RenderParagraph上,不過RenderParagraph并不會直接的繪制文本,而是會創(chuàng)建一TextPainter來管理繪制的工作。

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JgUMZp4Z-1669544494706)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f358e123221c48ef8891ea7a5ef49620~tplv-k3u1fbpfcp-watermark.image?)]
不過,TextPainter做的事和它的名字完全不一樣,你以為就是它來繪制文本的嗎?No~

實際上,它只是負責(zé)管理繪制的事,但它自己不會去繪制(當老板)。

基礎(chǔ)層

到現(xiàn)在為止,你會發(fā)現(xiàn),講了那么多,但是還是沒有那個ta去真正的繪制文本,就好像之前的所有的組件都在當中間商,把活外包了出去。

到了Flutter的最底層,你會發(fā)現(xiàn)有一個ParagraphBuilderParagraph,在前面關(guān)于Flutter如何繪制文本中,我們也提到了Flutter最后繪制文本時都是通過Paragraph完成的,而TextPainter是負責(zé)創(chuàng)建ParagraphBuilder的,但是當你翻看Paragraph類的源碼時,你會發(fā)現(xiàn),大部分的函數(shù)都是空函數(shù),原來這哥們也沒干活?。?/p>

@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';
  ...
}

引擎層

ParagraphParagraphBuilder這兩個類都將繪制的工作交給了Flutter Engine后,我們也要將視線放到SkParagraph上了,在以前Flutter Engine處理文本繪制的庫是LibText。后面切換成了SkParagraph,但是也實現(xiàn)了和Libtext相同的API。對于Flutter引擎在這篇文章中只做一個簡單的說明,若對引擎感興趣的朋友可以自己編譯FlutterEngine進行學(xué)習(xí),或者在線閱讀。

–更詳細更深入的Flutter文本渲染原理有興趣的朋友可以閱讀這篇文章

解決Flutter文本基線不對齊的問題

經(jīng)常在各大Flutter交流群中看到有哥們問這樣的問題:Row中,兩個文本沒有對齊,這怎么處理呀?

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(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)),
        ])),
      ),
    ],
  ),
)

其實處理這個問題很簡單,只需要給Row加上:

textBaseline: TextBaseline.alphabetic,
crossAxisAlignment: CrossAxisAlignment.baseline,

[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JFwhozn4-1669544494707)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/add602d2e218444e8ccf7c3f157b108b~tplv-k3u1fbpfcp-watermark.image?)]

關(guān)于更多有關(guān)文本的布局問題大家可以查看官方這篇文檔。

尾述

在這篇文章中,我們知道了文本是由什么組成的,F(xiàn)lutter是怎樣將文本顯示到屏幕上的。但是這也只是Flutter關(guān)于文本的一小部分,關(guān)于文本的編輯…等內(nèi)容將會在后續(xù)的文章中繼續(xù)探索。希望這篇文章能對你有所幫助,有問題歡迎在評論區(qū)留言討論~

參考&推薦閱讀

Flutter Text Rendering — @Jonathan Sande

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

Flutter 小技巧之玩轉(zhuǎn)字體渲染和問題修復(fù) — @戀貓de小郭

Flutter學(xué)習(xí):使用CustomPaint繪制文字 — @菠蘿橙子丶

關(guān)于我

Hello,我是Taxze,如果您覺得文章對您有價值,希望您能給我的文章點個??,有問題需要聯(lián)系我的話:我在這里

如果您覺得文章還差了那么點東西,也請通過關(guān)注督促我寫出更好的文章——萬一哪天我進步了呢?😝

— 文字是人類用符號記錄表達信息以傳之久遠的方式和工具。

你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧

當前名稱:Flutter中那些你需要知道的文本知識!-創(chuàng)新互聯(lián)
文章來源:http://www.muchs.cn/article40/djjgho.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站改版、關(guān)鍵詞優(yōu)化、小程序開發(fā)、網(wǎng)站建設(shè)、網(wǎng)站策劃網(wǎng)站維護

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

手機網(wǎng)站建設(shè)