小菜在學習 Flutter 過程中,有特别需求是對于文本過長的内容需要展示固定行數,而在文本右下角有提示使用者點選展開和收起;小菜嘗試自定義一個可折疊收縮的 ACEFoldTextView;

ACEFoldTextView
小菜首先簡單梳理了一下設計流程,如下圖所示;
- 當文本内容所占據行數小于等于限制的最大行數時,預設展示整個文本内容,不會有【展開/收起】;
- 當文本内容所占據行數大于限制的最大行數時,預設展示最大行數内容,并在右下角顯示【展開】提示;
- 點選【展開】區域時,當文本内容最後一行内容與【展開】區域占據内容寬度之和小于最大寬度時,預設展示【收起】;
- 點選【展開】區域時,當文本内容最後一行内容與【展開】區域占據内容寬度之和大于等于最大寬度時,【收起】區域換行展示;
1. 透明漸變【展開/收起】
小菜整體通過 Stack 層級嵌套方式在右下角顯示可點選的【展開/收起】文本區,為了提高顯示效果,并防止完全遮擋内容文本,小菜嘗試了兩種方式來實作顔色透明度漸變;
1.1 ShaderMask 着色器
小菜之前有重點介紹過 ShaderMask 着色器,可以對子 Widget 進行顔色處理,包括遮罩層特效展示;小菜設定了一個 LinearGradient 線性漸變,但 ShaderMask 是對整個子 Widget 遮罩層生效,可能會影響 Text 文本顯示效果,需要 Stack 層級使用;
_transparentWid02() => ShaderMask(
shaderCallback: (bounds) => LinearGradient(
colors: [_bgColor.withOpacity(0.0), _bgColor],
).createShader(bounds),
child: Container(
alignment: Alignment.centerRight,
color: Colors.white,
width: _kMoreWidth,
child: Text((_temLines > _maxLines) ? '展開' : '收起',
style: TextStyle(color: Theme.of(context).accentColor, fontSize: widget.textStyle?.fontSize ?? 14.0))));
1.2 Container BoxDecoration
第二種就是常用的 Container 配合設定 BoxDecoration 設定線性漸變色;該方式使用更為便捷;
_transparentWid01() => Container(
alignment: Alignment.centerRight,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [_bgColor.withOpacity(0.0), _bgColor],
end: FractionalOffset(0.5, 0.5))),
width: _kMoreWidth,
child: Text((_temLines > _maxLines) ? '展開' : '收起',
style: TextStyle(color: Theme.of(context).accentColor, fontSize: widget.textStyle?.fontSize ?? 14.0)));
2. Text 文本内容折疊
小菜想實作文本折疊,首先需要預先得知 Text 文本在範圍内占據的行數,一般都需要通過 TextPainter 等方式擷取;小菜嘗試了兩種方式進行判斷;
2.1 TextPainter.didExceedMaxLines
小菜之前也有簡單了解過 TextPainter 與 TextSpan 的應用,主要用于文本的繪制,當設定 maxLines 之後,可以通過 didExceedMaxLines 判斷文本内容是否已經超行;小菜之後會對 TextPainter 再深入研究一下;
_checkOverMaxLines01(maxLines, maxWidth) {
final textSpan = TextSpan(text: _textStr, style: widget.textStyle);
final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr, maxLines: maxLines);
textPainter.layout(maxWidth: widget.maxWidth ?? MediaQuery.of(context).size.width);
return textPainter.didExceedMaxLines;
}
2.2 LineMetrics
didExceedMaxLines 可以直接擷取文本内容是否超行,但無法擷取每行文本資訊等;于是小菜嘗試了 computeLineMetrics() 方式擷取 LineMetrics 基線度量;可以擷取每行内容所占據的寬高等;
當然 LineMetrics 也無法擷取每行文本内容,以及在兩種文本對齊方式共用時有注意事項,小菜之後會進一步研究;
Tips: 在使用 computeLineMetrics() 擷取 LineMetrics 資訊時,需要注意 TextPainter 必須設定好 textDirection 文本對齊方式,以及在 layout 布局之後才可以擷取;
_checkOverMaxLines02(maxWidth) {
final textSpan = TextSpan(text: _textStr, style: widget.textStyle);
final textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr);
textPainter.layout(maxWidth: widget.maxWidth ?? MediaQuery.of(context).size.width);
_lines = textPainter.computeLineMetrics();
return _lines;
}
3. ACEFoldTextView
有了前面兩步的基礎,小菜将其結合起來,生成自定義 ACEFoldTextView;通過 LinearBuilder 限制子 Text 延遲加載;通過 LineMetrics 擷取最後一行文本長度,與預設【展開】所在 Widget 計算總和,之後判斷是否占據超過限制最大寬度;當超過最大寬度時,小菜将文本添加一個 \n 強制換行;
return LayoutBuilder(builder: (context, size) {
_isOverFlow = _checkOverMaxLines01(_maxLines, widget.maxWidth);
_temLines = _checkOverMaxLines02(widget.maxWidth)?.length;
return (_temLines <= _maxLines)
? _itemText() : Stack(children: <Widget>[_itemText(), _moreText()]);
});
_moreText() => Positioned(
bottom: 0, right: 0,
child: GestureDetector(
child: _transparentWid02(),
onTap: () => setState(() {
if (_temLines > _maxLines) {
if (_lines.last.width + _kMoreWidth >= widget.maxWidth) {
_maxLines = _temLines + 1;
_textStr = '${widget.text}\n';
} else {
_maxLines = _temLines;
}
} else if (_temLines == _maxLines) {
_maxLines = widget.maxLines;
}
})));
小菜對 ACEFoldTextView 的繪制到此為止,其中涉及到 TextPainter 内容較淺顯,小菜之後會進一步學習研究;如有錯誤,請多多指導!
來源: 阿策小和尚