Flutter 137: 圖解自定義 ACEFoldTextView 折疊文本

????小菜在學(xué)習(xí) Flutter 過程中,有特別需求是對(duì)于文本過長(zhǎng)的內(nèi)容需要展示固定行數(shù),而在文本右下角有提示用戶點(diǎn)擊展開和收起;小菜嘗試自定義一個(gè)可折疊收縮的 ACEFoldTextView

ACEFoldTextView

????小菜首先簡(jiǎn)單梳理了一下設(shè)計(jì)流程,如下圖所示;

  • 當(dāng)文本內(nèi)容所占據(jù)行數(shù)小于等于限制的最大行數(shù)時(shí),默認(rèn)展示整個(gè)文本內(nèi)容,不會(huì)有【展開/收起】;
  • 當(dāng)文本內(nèi)容所占據(jù)行數(shù)大于限制的最大行數(shù)時(shí),默認(rèn)展示最大行數(shù)內(nèi)容,并在右下角顯示【展開】提示;
  • 點(diǎn)擊【展開】區(qū)域時(shí),當(dāng)文本內(nèi)容最后一行內(nèi)容與【展開】區(qū)域占據(jù)內(nèi)容寬度之和小于最大寬度時(shí),默認(rèn)展示【收起】;
  • 點(diǎn)擊【展開】區(qū)域時(shí),當(dāng)文本內(nèi)容最后一行內(nèi)容與【展開】區(qū)域占據(jù)內(nèi)容寬度之和大于等于最大寬度時(shí),【收起】區(qū)域換行展示;


1. 透明漸變【展開/收起】

????小菜整體通過 Stack 層級(jí)嵌套方式在右下角顯示可點(diǎn)擊的【展開/收起】文本區(qū),為了提高顯示效果,并防止完全遮擋內(nèi)容文本,小菜嘗試了兩種方式來實(shí)現(xiàn)顏色透明度漸變;

1.1 ShaderMask 著色器

????小菜之前有重點(diǎn)介紹過 ShaderMask 著色器,可以對(duì)子 Widget 進(jìn)行顏色處理,包括遮罩層特效展示;小菜設(shè)置了一個(gè) LinearGradient 線性漸變,但 ShaderMask 是對(duì)整個(gè)子 Widget 遮罩層生效,可能會(huì)影響 Text 文本顯示效果,需要 Stack 層級(jí)使用;

_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 配合設(shè)置 BoxDecoration 設(shè)置線性漸變色;該方式使用更為便捷;

_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 文本內(nèi)容折疊

????小菜想實(shí)現(xiàn)文本折疊,首先需要預(yù)先得知 Text 文本在范圍內(nèi)占據(jù)的行數(shù),一般都需要通過 TextPainter 等方式獲取;小菜嘗試了兩種方式進(jìn)行判斷;

2.1 TextPainter.didExceedMaxLines

????小菜之前也有簡(jiǎn)單了解過 TextPainterTextSpan 的應(yīng)用,主要用于文本的繪制,當(dāng)設(shè)置 maxLines 之后,可以通過 didExceedMaxLines 判斷文本內(nèi)容是否已經(jīng)超行;小菜之后會(huì)對(duì) 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 可以直接獲取文本內(nèi)容是否超行,但無法獲取每行文本信息等;于是小菜嘗試了 computeLineMetrics() 方式獲取 LineMetrics 基線度量;可以獲取每行內(nèi)容所占據(jù)的寬高等;

????當(dāng)然 LineMetrics 也無法獲取每行文本內(nèi)容,以及在兩種文本對(duì)齊方式共用時(shí)有注意事項(xiàng),小菜之后會(huì)進(jìn)一步研究;

????Tips: 在使用 computeLineMetrics() 獲取 LineMetrics 信息時(shí),需要注意 TextPainter 必須設(shè)置好 textDirection 文本對(duì)齊方式,以及在 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

????有了前面兩步的基礎(chǔ),小菜將其結(jié)合起來,生成自定義 ACEFoldTextView;通過 LinearBuilder 約束子 Text 延遲加載;通過 LineMetrics 獲取最后一行文本長(zhǎng)度,與默認(rèn)【展開】所在 Widget 計(jì)算總和,之后判斷是否占據(jù)超過限制最大寬度;當(dāng)超過最大寬度時(shí),小菜將文本添加一個(gè) \n 強(qiáng)制換行;

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 案例源碼


????小菜對(duì) ACEFoldTextView 的繪制到此為止,其中涉及到 TextPainter 內(nèi)容較淺顯,小菜之后會(huì)進(jìn)一步學(xué)習(xí)研究;如有錯(cuò)誤,請(qǐng)多多指導(dǎo)!

來源: 阿策小和尚

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容