Flutter-自定義可展開(kāi)文本控件的實(shí)現(xiàn)

Flutter

在移動(dòng)開(kāi)發(fā)中,常常需要處理一些長(zhǎng)文本顯示的場(chǎng)景,如何優(yōu)雅地展示這些文本并允許用戶展開(kāi)和收起是一個(gè)常見(jiàn)的需求。在本文中,我將分享如何使用Flutter實(shí)現(xiàn)一個(gè)可展開(kāi)和收起的文本控件。

效果

我們將實(shí)現(xiàn)一個(gè)可展開(kāi)和收起的文本控件。當(dāng)文本超過(guò)指定的最大行數(shù)時(shí),會(huì)顯示省略號(hào)和“展開(kāi)”按鈕。點(diǎn)擊“展開(kāi)”按鈕后,文本會(huì)全部顯示,并且按鈕變成“收起”,點(diǎn)擊“收起”按鈕后,文本會(huì)恢復(fù)到初始的折疊狀態(tài)。

屏幕錄制2024-06-02 01.07.52.gif

需求

  1. 文本內(nèi)容可以動(dòng)態(tài)展開(kāi)和收起。
  2. 當(dāng)文本內(nèi)容超過(guò)指定的最大行數(shù)時(shí),顯示“展開(kāi)”按鈕。
  3. 當(dāng)文本內(nèi)容全部顯示時(shí),顯示“收起”按鈕。
  4. 具有自定義文本樣式的能力。

實(shí)現(xiàn)思路

  1. 使用LayoutBuilder來(lái)獲取文本控件的最大寬度。
  2. 使用TextPainter來(lái)計(jì)算文本的高度和是否超過(guò)最大行數(shù)。
  3. 通過(guò)判斷文本是否超出最大行數(shù)來(lái)決定顯示“展開(kāi)”或“收起”按鈕。
  4. 使用RichTextTextSpan來(lái)動(dòng)態(tài)構(gòu)建可點(diǎn)擊的“展開(kāi)”和“收起”按鈕。

實(shí)現(xiàn)代碼

以下是實(shí)現(xiàn)可展開(kāi)文本控件的完整代碼:

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
詳情見(jiàn):github.com/yixiaolunhui/flutter_xy
void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Expandable Text View'),
        ),
        body: const Padding(
          padding: EdgeInsets.all(16.0),
          child: ExpandableTextView(
            text: '我是一個(gè)測(cè)試的數(shù)據(jù),我是一個(gè)測(cè)試的數(shù)據(jù),我是一個(gè)測(cè)試的數(shù)據(jù),我是一個(gè)測(cè)試的數(shù)據(jù),我是一個(gè)測(cè)試的數(shù)據(jù),我是一個(gè)測(cè)試的數(shù)據(jù),我是一個(gè)測(cè)試的數(shù)據(jù),我是一個(gè)測(cè)試的數(shù)據(jù)',
            maxLines: 2,
          ),
        ),
      ),
    );
  }
}

class ExpandableText extends StatefulWidget {
  final String text;
  final int maxLines;
  final TextStyle? textStyle;

  const ExpandableText({
    Key? key,
    required this.text,
    required this.maxLines,
    this.textStyle,
  }) : super(key: key);

  @override
  ExpandableTextState createState() => ExpandableTextState();
}

class ExpandableTextState extends State<ExpandableText> {
  bool isExpanded = false;

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(
      builder: (BuildContext context, BoxConstraints constraints) {
        final maxWidth = constraints.maxWidth;
        final textSpan = TextSpan(
          text: widget.text,
          style: widget.textStyle ?? const TextStyle(color: Colors.black),
        );

        final textPainter = TextPainter(
          text: textSpan,
          maxLines: isExpanded ? null : widget.maxLines,
          textDirection: TextDirection.ltr,
        );
        textPainter.layout(maxWidth: maxWidth);

        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            isExpanded
                ? _buildExpandedText()
                : _buildCollapsedText(textPainter, maxWidth),
          ],
        );
      },
    );
  }

  Widget _buildCollapsedText(TextPainter textPainter, double maxWidth) {
    final expandSpan = TextSpan(
      text: " 展開(kāi)",
      style: const TextStyle(color: Colors.blue),
      recognizer: TapGestureRecognizer()
        ..onTap = () {
          setState(() {
            isExpanded = !isExpanded;
          });
        },
    );

    final linkTextSpan = TextSpan(
      text: '...',
      style: widget.textStyle ?? const TextStyle(color: Colors.black),
      children: [expandSpan],
    );

    final linkPainter = TextPainter(
      text: linkTextSpan,
      textDirection: TextDirection.ltr,
    );
    linkPainter.layout(maxWidth: maxWidth);

    final position = textPainter.getPositionForOffset(
        Offset(maxWidth - linkPainter.width, textPainter.height));
    final endOffset =
        textPainter.getOffsetBefore(position.offset) ?? position.offset;
    final truncatedText = widget.text.substring(0, endOffset);

    return RichText(
      text: TextSpan(
        text: truncatedText,
        style: widget.textStyle ?? const TextStyle(color: Colors.black),
        children: [linkTextSpan],
      ),
      maxLines: widget.maxLines,
      overflow: TextOverflow.ellipsis,
    );
  }

  Widget _buildExpandedText() {
    final collapseSpan = TextSpan(
      text: " 收起",
      style: const TextStyle(color: Colors.blue),
      recognizer: TapGestureRecognizer()
        ..onTap = () {
          setState(() {
            isExpanded = !isExpanded;
          });
        },
    );

    return RichText(
      text: TextSpan(
        text: widget.text,
        style: widget.textStyle ?? const TextStyle(color: Colors.black),
        children: [collapseSpan],
      ),
    );
  }
}

代碼解析

  • ExpandableText Widget: 自定義的文本控件,接收文本內(nèi)容和最大行數(shù)作為輸入?yún)?shù)。
  • isExpanded: 控制文本是否展開(kāi)的狀態(tài)變量。
  • LayoutBuilder: 用于獲取父容器的最大寬度,以便于后續(xù)的文本布局計(jì)算。
  • TextPainter: 用于計(jì)算文本的高度和是否超過(guò)最大行數(shù)。
  • RichText: 用于顯示帶有點(diǎn)擊事件的文本(“展開(kāi)”和“收起”)。

總結(jié)

通過(guò)以上實(shí)現(xiàn),我們可以輕松地在Flutter應(yīng)用中使用可展開(kāi)和收起的文本控件,提升用戶體驗(yàn)。這種實(shí)現(xiàn)方式不僅簡(jiǎn)潔高效,還具備良好的可維護(hù)性和擴(kuò)展性。如果你有更復(fù)雜的需求,可以在此基礎(chǔ)上進(jìn)行進(jìn)一步的定制和優(yōu)化。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容