????小菜在學習 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 內容較淺顯,小菜之后會進一步學習研究;如有錯誤,請多多指導!
來源: 阿策小和尚