在 Flutter 中編寫自定義小部件(第1部分)ー EllipsizedText

會煮咖啡的貓發表於2021-05-31

老鐵記得 轉發 ,貓哥會呈現更多 Flutter 好文~~~~

微信 flutter 研修群 ducafecat

原文

rlesovyi.medium.com/writing-cus…

程式碼

github.com/MatrixDev/F…

正文

宣告式使用者介面在 Flutter 是相當不錯,易於使用,它是非常誘人的使用盡可能。但是很多時候,開發人員只是做得太過火了ーー用宣告的方式編寫所有東西,即使有時候任務可以以更強制性的方式更有效、更容易理解。

每個人都應該明白的---- 在陳述和指令式程式設計之間必須有一個平衡。每種方法都有自己的用途,每種方法在某些任務上都比其他方法更加出色。

在本系列文章中,我將描述如何通過從頭建立自定義小部件來解決不同的問題。每一個都比前一個稍微複雜一點。

思考

在檢視程式碼之前,我們需要知道一些基本的事情。

Widget ー只是一個不可變的(最好是 const)類,它包含 Elements 和 RenderObjects 的配置屬性。它還負責建立上述元素和渲染物件。需要理解的重要事情ー小部件從不包含狀態或任何業務邏輯,只是傳遞它們。

元素ー是負責實際 UI 樹的實體。它包含對所有子元素的引用,以及(不像 Widget)對其父元素的引用。元素在大多數情況下都會被重用,除非鍵或小部件被更改。因此,如果 onlyWidget 屬性被更改,即使分配了新的 Widget,Element 也將保持不變。

State ー只不過是 Element 內部的一個使用者定義類,它還公開了一些來自它的回撥。

RenderObject ーー負責實際尺寸的計算、子元素的放置、繪製、觸控事件的處理等。這些物件與 Android 或其他框架的經典檢視非常相似。

為什麼我們同時擁有元素和渲染物件?因為效率高。每個小部件都有各自的元素,但只有一些有渲染物件。由於這一點,很多佈局,觸控和其他層次遍歷呼叫可以省略。

程式碼

第一個例子是一個非常簡單的小部件,它在文字不適合時用省略號縮放文字。為什麼我們需要這樣一個小部件時,內建的文字已經省略號支援你可能會問?答案很簡單---- 到目前為止,它只是通過文字而不是字元來表達 github.com/flutter/flu…

那麼,我們開始吧。Flutter 有許多內建的基類和 mixin,它們將幫助構建完全自定義的小部件。以下是其中的一些:

  • LeafRenderObjectWidget 沒有 child
  • SingleChildRenderObjectWidget 一個 child
  • MultiChildRenderObjectWidget 多個 child

在我們的例子中,我們將使用 LeafRenderObjectWidget,因為我們只需要渲染文字,並且不會有子節點:

enum Ellipsis { start, middle, end }

class EllipsizedText extends LeafRenderObjectWidget {
  final String text;
  final TextStyle? style;
  final Ellipsis ellipsis;

  const EllipsizedText(
    this.text, {
    Key? key,
    this.style,
    this.ellipsis = Ellipsis.end,
  }) : super(key: key);

  @override
  RenderObject createRenderObject(BuildContext context) {
    return RenderEllipsizedText()..widget = this;
  }

  @override
  void updateRenderObject(BuildContext context, RenderEllipsizedText renderObject) {
    renderObject.widget = this;
  }
}
複製程式碼

我們建立了我們的 Widget,唯一不同尋常的是有兩種方法:

  • createRenderObject — 負責實際建立我們的 RenderObject
  • updateRenderObject — 當 Widget 的資料發生變化但 RenderObject 保持不變時,將呼叫 updateRenderObject ー。在這種情況下,我們需要更新 RenderObject 中的資料,否則它將呈現舊文字

我還需要注意,將每個值從小部件複製到 RenderObject 是首選的。但是我會通過整個 Widget,因為不管怎樣它們都是不可變的(而且我懶得編寫所有的樣板程式碼)。

現在讓我們從實際的渲染物件開始:

class RenderEllipsizedText extends RenderBox {
  var _widgetChanged = false;
  var _widget = const EllipsizedText('');

  set widget(EllipsizedText widget) {
    if (_widget.text == widget.text &&
        _widget.style == widget.style &&
        _widget.ellipsis == widget.ellipsis) {
      return;
    }
    _widgetChanged = true;
    _widget = widget;
    markNeedsLayout();
  }
}
複製程式碼

在這裡,我們定義了所有的變數,並編寫了一個 setter 來實際更新它們。還有一個檢查值是否實際發生了更改的防護措施ー如果沒有更改,則沒有必要重新計算省略號和重繪文字。

現在我們需要佈局渲染物件。

class RenderEllipsizedText extends RenderBox {
  // ...
  var _constraints = const BoxConstraints();

  @override
  void performLayout() {
    if (!_widgetChanged && _constraints == constraints && hasSize) {
      return;
    }

    _widgetChanged = false;
    _constraints = constraints;

    size =_ellipsize(
      minWidth: constraints.minWidth,
      maxWidth: constraints.maxWidth,
    );
  }
}
複製程式碼

佈局的過程相當簡單。所有我們需要做的ー根據提供給我們的約束計算渲染物件的大小。約束只描述我們必須遵守的最小和最大規模。另外,如果沒有任何變化,並且在以前的佈局傳遞過程中已經計算了大小,則新增額外的檢查。

實際建立省略號文字的過程相當繁瑣,而且肯定有更好的解決方案,但我選擇使用二進位制搜尋來尋找最佳匹配。

class RenderEllipsizedText extends RenderBox {
  // ...
  final _textPainter = TextPainter(textDirection: TextDirection.ltr);

  Size _ellipsize({required double minWidth, required double maxWidth}) {
    final text = _widget.text;

    if (_layoutText(length: text.length, minWidth: minWidth) > maxWidth) {
      var left = 0;
      var right = text.length - 1;

      while (left < right) {
        final index = (left + right) ~/ 2;
        if (_layoutText(length: index, minWidth: minWidth) > maxWidth) {
          right = index;
        } else {
          left = index + 1;
        }
      }
      _layoutText(length: right - 1, minWidth: minWidth);
    }

    return constraints.constrain(Size(_textPainter.width, _textPainter.height));
  }
}
複製程式碼

我不會講完所有這些邏輯(如果你願意,你可以通過它來閱讀)。但是重要的是 TextPainter 是用來計算文字大小的。如果文字大小長於我們的約束,我會盡量使它越來越短,直到它符合我們的約束。

_layoutText 用來計算我們裁剪後的文字大小:

double _layoutText({required int length, required double minWidth}) {
  final text = _widget.text;
  final style = _widget.style;
  final ellipsis = _widget.ellipsis;

  String ellipsizedText = '';

  switch (ellipsis) {
    case Ellipsis.start:
      if (length > 0) {
        ellipsizedText = text.substring(text.length - length, text.length);
        if (length != text.length) {
          ellipsizedText = '...' + ellipsizedText;
        }
      }
      break;
    case Ellipsis.middle:
      if (length > 0) {
        ellipsizedText = text;
        if (length != text.length) {
          var start = text.substring(0, (length / 2).round());
          var end = text.substring(text.length - start.length, text.length);
          ellipsizedText = start + '...' + end;
        }
      }
      break;
    case Ellipsis.end:
      if (length > 0) {
        ellipsizedText = text.substring(0, length);
        if (length != text.length) {
          ellipsizedText = ellipsizedText + '...';
        }
      }
      break;
  }

  _textPainter.text = TextSpan(text: ellipsizedText, style: style);
  _textPainter.layout(minWidth: minWidth, maxWidth: double.infinity);
  return _textPainter.width;
}
複製程式碼

差不多就是這樣了,我們剩下要做的就是——實際上畫出我們的文字。

@override
void paint(PaintingContext context, Offset offset) {
  _textPainter.paint(context.canvas, offset);
}
複製程式碼

© 貓哥

ducafecat.tech/

github.com/ducafecat

往期

開源

GetX Quick Start

github.com/ducafecat/g…

新聞客戶端

github.com/ducafecat/f…

strapi 手冊譯文

getstrapi.cn

微信討論群 ducafecat

系列集合

譯文

ducafecat.tech/categories/…

開源專案

ducafecat.tech/categories/…

Dart 程式語言基礎

space.bilibili.com/404904528/c…

Flutter 零基礎入門

space.bilibili.com/404904528/c…

Flutter 實戰從零開始 新聞客戶端

space.bilibili.com/404904528/c…

Flutter 元件開發

space.bilibili.com/404904528/c…

Flutter Bloc

space.bilibili.com/404904528/c…

Flutter Getx4

space.bilibili.com/404904528/c…

Docker Yapi

space.bilibili.com/404904528/c…

相關文章