[Flutter翻译]在Flutter中编写自定义Widget(第一部分) - EllipsizedText

102 阅读5分钟

本文由 简悦SimpRead 转码,原文地址 itnext.io

让我们用字符而不是字来呈现椭圆的文字。

image.png

Flutter中的声明式UI相当不错,易于使用,尽可能多地使用它是非常诱人的。但很多时候,开发者会过度使用它--把所有东西都写成声明式的,即使有时任务可以用更有效、更容易理解的命令式方式来完成。

每个人都应该明白--在声明式编程和命令式编程之间必须有一个平衡。两者都有自己的用途,而且在某些任务中都比其他任务更有优势。

在这一系列的文章中,我将描述如何通过从头开始创建自定义Widget来解决不同的问题。每一个问题都比前一个问题要复杂一些。

在看代码之前,我们需要知道一些基本的东西。

Widget- 是一个不可变的(最好是const)类,它包含ElementsRenderObjects的配置属性。它也负责创建所述的 "元素 "和 "RenderObjects"。重要的是要理解 - Widgets从不包含状态或任何业务逻辑,只传递它们。

Element-是一个负责实际UI树的实体。它对所有的子节点和(与Widget不同)它的父节点都有引用。元素在大多数情况下被重复使用,除非keyWidget被改变。所以如果只有Widget的属性被改变,即使新的Widget被分配,Element也会保持不变。

State-只不过是Element中的一个用户定义的类,它也有一些从其Element暴露出来的回调。

渲染对象(RenderObject) - 负责实际尺寸的计算、儿童的放置、绘制、触摸事件的处理等等。这些对象与Android或其他框架的经典 "视图 "最为相似。

为什么我们同时拥有ElementsRenderObjects?因为效率高。每个Widget都有各自的Element,但只有一些有RenderObjects。由于这一点,很多布局、触摸和其他层次的遍历调用都可以被省略。

第一个例子是一个相当简单的Widget,当文本不合适时,它可以将其放大。你可能会问,为什么我们需要这样一个Widget,因为内置的Text已经支持省略号了?答案很简单--就目前而言,它只能按字数而不是按字符进行切割( github.com/flutter/flu… )。所以,如果你有一个很长的单词在结尾--大多数时候你只能看到这个单词的第一个字母,即使还有很多空间可以填充。

所以让我们开始吧。Flutter有很多内置的基础类和混合类,可以帮助建立完全自定义的Widget。下面是其中的几个。

  • LeafRenderObjectWidget - 没有孩子
  • SingleChildRenderObjectWidget - 有一个单一的孩子
  • MultiChildRenderObjectWidget - 有任意数量的孩子。

在我们的案例中,我们将使用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保持不变时,将被调用。在这种情况下,我们需要更新RenderObject中的数据,否则它将呈现旧的文本。

我还需要注意的是,最好是将Widget中的每个值复制到RenderObject中。但是我将传递整个Widget,因为无论如何它们都是不可改变的(而且我也懒得写那些模板代码)。

现在让我们从实际的RenderObject开始。

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来实际更新它们。还有一个保护机制来检查数值是否真的发生了变化--如果没有任何变化,就没有必要重新计算省略号和重绘文本。

现在我们需要布局我们的RenderObject。

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

布局的过程是非常简单的。我们需要做的是--根据提供给我们的约束条件来计算我们的RenderObject的大小。约束条件只描述了我们必须遵守的最小和最大的尺寸。另外,如果没有任何变化,并且在上一次布局过程中已经计算出了尺寸,则会添加额外的检查。

创建椭圆形文本的实际过程是非常麻烦的,而且肯定有更好的解决方案,但我选择使用二进制搜索来找到最佳匹配。

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

这里是结果。

1.gif

你可以在我的GitHub上找到实现。 github.com/MatrixDev/F…

希望你喜欢它!


www.deepl.com 翻译