本文由 简悦SimpRead 转码,原文地址 itnext.io
让我们用字符而不是字来呈现椭圆的文字。
Flutter中的声明式UI相当不错,易于使用,尽可能多地使用它是非常诱人的。但很多时候,开发者会过度使用它--把所有东西都写成声明式的,即使有时任务可以用更有效、更容易理解的命令式方式来完成。
每个人都应该明白--在声明式编程和命令式编程之间必须有一个平衡。两者都有自己的用途,而且在某些任务中都比其他任务更有优势。
在这一系列的文章中,我将描述如何通过从头开始创建自定义Widget来解决不同的问题。每一个问题都比前一个问题要复杂一些。
在看代码之前,我们需要知道一些基本的东西。
Widget- 是一个不可变的(最好是const)类,它包含Elements和RenderObjects的配置属性。它也负责创建所述的 "元素 "和 "RenderObjects"。重要的是要理解 - Widgets从不包含状态或任何业务逻辑,只传递它们。
Element-是一个负责实际UI树的实体。它对所有的子节点和(与Widget不同)它的父节点都有引用。元素在大多数情况下被重复使用,除非key或Widget被改变。所以如果只有Widget的属性被改变,即使新的Widget被分配,Element也会保持不变。
State-只不过是Element中的一个用户定义的类,它也有一些从其Element暴露出来的回调。
渲染对象(RenderObject) - 负责实际尺寸的计算、儿童的放置、绘制、触摸事件的处理等等。这些对象与Android或其他框架的经典 "视图 "最为相似。
为什么我们同时拥有Elements和RenderObjects?因为效率高。每个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);
}
这里是结果。
你可以在我的GitHub上找到实现。 github.com/MatrixDev/F…
希望你喜欢它!
- Part 2.a - ChildSize (with helpers)
- Part 2.b - ChildSize (no helpers)
- Part 3.a - SimpleOverlay (with helpers)
- Part 3.b - SimpleOverlay (no helpers)