Flutter问题记录 - Text组件设置不限行数无效


前言

梳理Flutter项目的过程中发现还有一些遗留的TODO没处理,其中有一个和Text组件相关。

开发环境

  • Flutter: 3.7.12
  • Dart: 2.19.6

问题描述

Text组件设置maxLines: null不限制行数:

Text(
  'The [Text] widget displays a string of text with single style. The string might break across multiple lines or might all be displayed on the same line depending on the layout constraints.',
  softWrap: true,
  overflow: TextOverflow.ellipsis,
  maxLines: null,
)

设置无效,只显示一行:

screenshot1

当时赶项目进度,没时间细究,暂时通过设置maxLines: 9999实现虚假的不限制行数。

问题分析

先看看maxLines的文档注释:

/// An optional maximum number of lines for the text to span, wrapping if necessary.
/// If the text exceeds the given number of lines, it will be truncated according
/// to [overflow].
///
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
/// edge of the box.
///
/// If this is null, but there is an ambient [DefaultTextStyle] that specifies
/// an explicit number for its [DefaultTextStyle.maxLines], then the
/// [DefaultTextStyle] value will take precedence. You can use a [RichText]
/// widget directly to entirely override the [DefaultTextStyle].

从文档中可知,当maxLines设为null时,如果DefaultTextStyle中有指定maxLines的值,那么将以这个值为准。查看Text组件的build方法,确实是这样的。

Textbuild方法(省略部分):


Widget build(BuildContext context) {
  final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context);
  ...
  Widget result = RichText(
    ...,
    maxLines: maxLines ?? defaultTextStyle.maxLines,
    ...,
  );
  ...
  return result;
}

问题的原因难道就是这个?断点调试看看:

screenshot2

DefaultTextStyle并没有指定maxLines的值,也是null。通过不断断点调试,可以很确定maxLines的值一直都是null。这难道是Flutter的bug?想了想应该不可能,如果是Flutter的bug,这么明显的问题应该早就被修复了。

假定Flutter没问题,那问题只能出在Text组件的使用上。除了maxLines,还设置了softWrapoverflow属性。咦🤔?️不限制行数的情况下,文本不存在溢出情况,那设置overflow属性完全是多此一举。移除overflow: TextOverflow.ellipsis,重新运行一切正常!

TextOverflow定义:

enum TextOverflow {
  /// Clip the overflowing text to fix its container.
  clip,

  /// Fade the overflowing text to transparent.
  fade,

  /// Use an ellipsis to indicate that the text has overflowed.
  ellipsis,

  /// Render overflowing text outside of its container.
  visible,
}

奇怪的是只有TextOverflow.ellipsis有问题,其他文本溢出处理方式没问题。

screenshot3

通过追踪overflow属性的传递,来到RenderParagraph类的构造方法:

screenshot4

初始化TextPainter时,如果overflow属性值等于TextOverflow.ellipsis,会用_kEllipsis常量初始化ellipsis_kEllipsis常量定义:

// '\u2026'即省略号'…'
const String _kEllipsis = '\u2026';

继续往下走,在TextPainter类中找到关于ellipsis的定义:

/// The string used to ellipsize overflowing text. Setting this to a non-empty
/// string will cause this string to be substituted for the remaining text
/// if the text can not fit within the specified maximum width.
///
/// Specifically, the ellipsis is applied to the last line before the line
/// truncated by [maxLines], if [maxLines] is non-null and that line overflows
/// the width constraint, or to the first line that is wider than the width
/// constraint, if [maxLines] is null. The width constraint is the `maxWidth`
/// passed to [layout].
///
/// After this is set, you must call [layout] before the next call to [paint].
///
/// The higher layers of the system, such as the [Text] widget, represent
/// overflow effects using the [TextOverflow] enum. The
/// [TextOverflow.ellipsis] value corresponds to setting this property to
/// U+2026 HORIZONTAL ELLIPSIS (…).
String? get ellipsis => _ellipsis;
String? _ellipsis;

关键在于第二段文档注释,当maxLinesnull且文本不止一行(文本宽度超出一行的宽度)时,ellipsis会用于第一行,也就是截断为一行。文档已经说的很清楚了,我也不知道当时为啥还要标个TODO😂。不过有句话说得好,来都来了,怎么也得继续再往下看看具体是在哪做文本截断处理的。

找了一番,发现难以继续分析下去,文本的绘制应该涉及到了Flutter引擎部分,光靠Flutter框架代码想要分析下去有点难。不过,也不是没有收获,在RenderParagraph类的performLayout方法中发现了一些关键调用:

screenshot5

执行布局时,调用textPainter.size获取的高度只有一行的高度,最关键的是调用_textPainter.didExceedMaxLines返回了true,这表示文本被截断或被省略。 didExceedMaxLines方法的定义:

/// Whether any text was truncated or ellipsized.
///
/// If [maxLines] is not null, this is true if there were more lines to be
/// drawn than the given [maxLines], and thus at least one line was omitted in
/// the output; otherwise it is false.
///
/// If [maxLines] is null, this is true if [ellipsis] is not the empty string
/// and there was a line that overflowed the `maxWidth` argument passed to
/// [layout]; otherwise it is false.
///
/// Valid only after [layout] has been called.
bool get didExceedMaxLines {
  assert(_debugAssertTextLayoutIsValid);
  return _paragraph!.didExceedMaxLines;
}

didExceedMaxLines方法的第三段文档注释也明确说了,如果maxLinesnullellipsis不为空,会返回true。沿着_paragraph!.didExceedMaxLines来到了Flutter SDK目录/bin/cache/pkg/sky_engine/lib/ui/text.dart文件:

/// True if there is more vertical content, but the text was truncated, either
/// because we reached `maxLines` lines of text or because the `maxLines` was
/// null, `ellipsis` was not null, and one of the lines exceeded the width
/// constraint.
///
/// See the discussion of the `maxLines` and `ellipsis` arguments at
/// [ParagraphStyle.new].
<Bool Function(Pointer<Void>)>(symbol: 'Paragraph::didExceedMaxLines', isLeaf: true)
external bool get didExceedMaxLines;

从这开始,已经脱离Flutter框架,来到Flutter引擎。经过一番查找,最终锁定在了skia项目中的TextWrapper.cpp文件,里面有个breakTextIntoLines函数,顾名思义,这个函数的作用就是将文本分成几行。这个函数有点长,又是C++写的,直接看有点累,如果能调试会轻松很多,调试环境的搭建可以参考这篇文章Flutter - 搭建引擎调试环境(iOS)

screenshot6

从调试结果看,如果有ellipsis且不限制行数时,会提前终止循环,也就是第一行就被截断。分析到这,基本可以确定文本最终就是在这被分行截断。

解决方案

Text组件设置不限行数时,请勿将overflow属性设置为TextOverflow.ellipsis

最后

如果这篇文章对你有所帮助,请不要吝啬你的点赞👍加星🌟,谢谢~

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
要在Flutter设置`Text`组件的高度超出后不报错,可以使用`Expanded`和`LayoutBuilder`组件来实现。以下是基本步骤: 1. 使用`Expanded`组件包裹`Text`组件,以便`Text`组件可以自动扩展高度。 ```dart Expanded( child: Text( '需要显示的文本', style: TextStyle(fontSize: 16), // 设置文本字体大小 textAlign: TextAlign.center, // 设置文本对齐方式 strutStyle: StrutStyle.disabled, // 禁用strut softWrap: true, // 允许文本自动换行 textScaleFactor: 1.0, // 文本缩放因子 textDirection: TextDirection.ltr, // 文本方向 locale: Locale('zh'), // 本地化语言 overflowReplacement: Text('文本溢出'), // 整个文本溢出时的替换组件 textWidthBasis: TextWidthBasis.longestLine, // 文本宽度基准 textHeightBehavior: TextHeightBehavior( // 文本高度行为 applyHeightToFirstAscent: false, applyHeightToLastDescent: false, ), ), ); ``` 2. 在`Text`组件外层包裹`LayoutBuilder`组件,以便获取`Text`组件的实际高度。 ```dart LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( child: Expanded( child: Text( '需要显示的文本', style: TextStyle(fontSize: 16), // 设置文本字体大小 textAlign: TextAlign.center, // 设置文本对齐方式 strutStyle: StrutStyle.disabled, // 禁用strut softWrap: true, // 允许文本自动换行 textScaleFactor: 1.0, // 文本缩放因子 textDirection: TextDirection.ltr, // 文本方向 locale: Locale('zh'), // 本地化语言 overflowReplacement: Text('文本溢出'), // 整个文本溢出时的替换组件 textWidthBasis: TextWidthBasis.longestLine, // 文本宽度基准 textHeightBehavior: TextHeightBehavior( // 文本高度行为 applyHeightToFirstAscent: false, applyHeightToLastDescent: false, ), ), ), constraints: BoxConstraints( maxWidth: constraints.maxWidth, // 设置容器的最大宽度 ), ); }, ); ``` 完整代码示例如下: ```dart LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Container( child: Expanded( child: Text( '需要显示的文本', style: TextStyle(fontSize: 16), // 设置文本字体大小 textAlign: TextAlign.center, // 设置文本对齐方式 strutStyle: StrutStyle.disabled, // 禁用strut softWrap: true, // 允许文本自动换行 textScaleFactor: 1.0, // 文本缩放因子 textDirection: TextDirection.ltr, // 文本方向 locale: Locale('zh'), // 本地化语言 overflowReplacement: Text('文本溢出'), // 整个文本溢出时的替换组件 textWidthBasis: TextWidthBasis.longestLine, // 文本宽度基准 textHeightBehavior: TextHeightBehavior( // 文本高度行为 applyHeightToFirstAscent: false, applyHeightToLastDescent: false, ), ), ), constraints: BoxConstraints( maxWidth: constraints.maxWidth, // 设置容器的最大宽度 ), ); }, ); ``` 需要注意的是,使用`Expanded`组件包裹`Text`组件时,不能设置最大行数,否则会出现溢出警告。因此,要确保`Text`组件的高度不会超过父容器的高度。在上述代码中,我们将父容器的最大宽度设置为`Text`组件的宽度,以便让`Text`组件自动扩展高度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值