介绍
对长文本进行省略展示,支持展开/收起,对省略位置可以自定义

c2b2bc33-ed77-4ad3-b50d-51d003bf5c52.gif
代码演示
基础用法
      HcTextEllipsis(
            content:"生态文明建设是关系中华民族永续发展的根本大计。总书记指出,要把建设美丽中国摆在强国建设、民族复兴的突出位置,以高品质生态环境支撑高质量发展,加快推进人与自然和谐共生的现代化。各地坚持以习近平生态文明思想为指引,牢固树立和践行绿水青山就是金山银山的理念,以更高站位、更宽视野、更大力度,全面推进美丽中国建设。"
          ),
最大显示行数
          HcTextEllipsis(
            maxLines: 2,
            content:"生态文明建设是关系中华民族永续发展的根本大计。总书记指出,要把建设美丽中国摆在强国建设、民族复兴的突出位置,以高品质生态环境支撑高质量发展,加快推进人与自然和谐共生的现代化。各地坚持以习近平生态文明思想为指引,牢固树立和践行绿水青山就是金山银山的理念,以更高站位、更宽视野、更大力度,全面推进美丽中国建设。"
          ),
省略符位置
 HcTextEllipsis(
            maxLines: 2,
            position: HcTextEllipsisPosition.center,
            content:"生态文明建设是关系中华民族永续发展的根本大计。总书记指出,要把建设美丽中国摆在强国建设、民族复兴的突出位置,以高品质生态环境支撑高质量发展,加快推进人与自然和谐共生的现代化。各地坚持以习近平生态文明思想为指引,牢固树立和践行绿水青山就是金山银山的理念,以更高站位、更宽视野、更大力度,全面推进美丽中国建设。"
          ),
替换省略符
          HcTextEllipsis(
            maxLines: 2,
            ellipsisDots: "~~~",
            position: HcTextEllipsisPosition.center,
            content:"生态文明建设是关系中华民族永续发展的根本大计。总书记指出,要把建设美丽中国摆在强国建设、民族复兴的突出位置,以高品质生态环境支撑高质量发展,加快推进人与自然和谐共生的现代化。各地坚持以习近平生态文明思想为指引,牢固树立和践行绿水青山就是金山银山的理念,以更高站位、更宽视野、更大力度,全面推进美丽中国建设。"
          ),
展开按钮单独显示
          HcTextEllipsis(
            maxLines: 2,
            ellipsisDots: "~~~",
            separateShow: false,
            position: HcTextEllipsisPosition.center,
            content:"生态文明建设是关系中华民族永续发展的根本大计。总书记指出,要把建设美丽中国摆在强国建设、民族复兴的突出位置,以高品质生态环境支撑高质量发展,加快推进人与自然和谐共生的现代化。各地坚持以习近平生态文明思想为指引,牢固树立和践行绿水青山就是金山银山的理念,以更高站位、更宽视野、更大力度,全面推进美丽中国建设。"
          ),
点击按钮的回调
       HcTextEllipsis(
              maxLines: 5,
              ellipsisDots: "...",
              separateShow: false,
              showEllipsisBtn: true,
              position: HcTextEllipsisPosition.center,
              callback: (bool state) {
                //设定回调后手动控制是否展开 返回true为展开 false为不展开
                return Future.value(!state);
              },
              content:
                  "生态文明建设是关系中华民族永续发展的根本大计。总书记指出,要把建设美丽中国摆在强国建设、民族复兴的突出位置,以高品质生态环境支撑高质量发展,加快推进人与自然和谐共生的现代化。各地坚持以习近平生态文明思想为指引,牢固树立和践行绿水青山就是金山银山的理念,以更高站位、更宽视野、更大力度,全面推进美丽中国建设。"),
API
props
| 参数 | 说明 | 类型 | 默认值 | 是否必填 | 
|---|---|---|---|---|
| maxLines | 需要展示的行数 | int | 1 | true | 
| content | 文字内容 | String | - | true | 
| expandText | 展开按钮的文字内容 | String | 展开 | true | 
| collapseText | 收起按钮的文字内容 | String | 收起 | true | 
| ellipsisDots | 省略符 | String | ... | true | 
| position | 分隔符的位置 | HcTextEllipsisPosition | end | true | 
| isExpand | 默认是否展开 | bool | false | true | 
| showEllipsisBtn | 是否展示更多的按钮 | bool | false | true | 
| separateShow | 按钮是否单独展示 | bool | false | true | 
| contentStyle | 文字的字体样式 | TextStyle? | - | false | 
| btnTextStyle | 按钮的文字样式 | TextStyle? | - | false | 
| callback | 点击的时候回调 | HcTextEllipsisCallback | - | false | 
HcTextEllipsisPosition
展示省略符号位置
| 参数名 | 说明 | 
|---|---|
| start | 开头 | 
| center | 中部 | 
| end | 结尾 | 
Function
| 方法名 | 说明 | 参数 | 返回类型 | 
|---|---|---|---|
| HcTextEllipsisCallback | 点击按钮后的回调 | Function(bool) | Future<bool?> | 
项目源码
enum HcTextEllipsisPosition { start, center, end }
typedef HcTextEllipsisCallback = Future<bool?> Function(bool);
class HcTextEllipsis extends StatefulWidget {
  //需要展示的行数
  final int maxLines;
  //文字内容
  final String content;
  //展开的文字
  final String expandText;
  //折叠的文字
  final String collapseText;
  ///分隔符
  final String ellipsisDots;
  /// 文本的样式
  final TextStyle contentStyle;
  //按钮的 文字样式
  final TextStyle btnTextStyle;
  //点击按钮的回调
  final HcTextEllipsisCallback? callback;
  //分割符的位置
  final HcTextEllipsisPosition position;
  //是否展开
  final bool isExpand;
  //是否单独显示
  final bool separateShow;
  //是否展示拓展按钮
  final bool showEllipsisBtn;
  const HcTextEllipsis(
      {Key? key,
      this.isExpand = false,
      this.maxLines = 1,
      required this.content,
      this.expandText = "展开",
      this.collapseText = "收起",
      this.ellipsisDots = "...",
      this.position = HcTextEllipsisPosition.end,
      this.contentStyle = const TextStyle(color: Colors.black, fontSize: 16),
      this.callback,
      this.btnTextStyle = const TextStyle(
          color: Colors.blue, fontSize: 14, fontWeight: FontWeight.w500),
      this.separateShow = false,
      this.showEllipsisBtn = true})
      : super(key: key);
  @override
  State<HcTextEllipsis> createState() => _HcTextEllipsisState();
}
class _HcTextEllipsisState extends State<HcTextEllipsis> {
  //是否展开
  bool _isExpand = false;
  //组件的大小
  Size _viewSize = const Size(0, 0);
  //按钮组件的宽度
  double btnWidth = 0.0;
  //省略符号的宽度
  double dotWidth = 0.0;
  //文字高度
  double fontHeight = 0.0;
  //ellipsisLineWidth;
  double ellipsisLineWidth = 0.0;
  @override
  void initState() {
    super.initState();
    _isExpand = widget.isExpand;
  }
  @override
  void didUpdateWidget(covariant HcTextEllipsis oldWidget) {
    super.didUpdateWidget(oldWidget);
    _isExpand = widget.isExpand;
  }
  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, size) {
      //按钮的宽度
      btnWidth =
          HcStringUtil.boundingTextSize(widget.expandText, widget.btnTextStyle)
              .width;
      //文字的宽度与高度
      Size fontSize = HcStringUtil.boundingTextSize(
          widget.ellipsisDots, widget.contentStyle);
      dotWidth = fontSize.width;
      fontHeight = fontSize.height;
      //组件最大长度
      _viewSize = Size(size.maxWidth, size.maxHeight);
      //获取painter信息
      TextPainter painter = HcStringUtil.getTextInfo(
          widget.content, widget.contentStyle,
          maxLines: widget.maxLines, width: _viewSize.width);
      // 如果不满一行直接显示
      if (!painter.didExceedMaxLines) {
        return Text(widget.content, style: widget.contentStyle);
      }
      String result = "";
      String content = widget.content;
      switch (widget.position) {
        case HcTextEllipsisPosition.start:
          result = _buildEllipsisStartText(content, widget.maxLines);
          break;
        case HcTextEllipsisPosition.center:
          if (widget.maxLines == 1) {
            ellipsisLineWidth = _viewSize.width -
                (!widget.separateShow && widget.showEllipsisBtn
                    ? btnWidth
                    : 0) -
                dotWidth;
            int start = HcStringUtil.getTextInfo(content, widget.contentStyle,
                    width: (ellipsisLineWidth ~/ 2).toDouble())
                .getPositionForOffset(
                    Offset((ellipsisLineWidth ~/ 2).toDouble(), 0))
                .offset;
            result = content.substring(0, start);
            result += widget.ellipsisDots;
            content = HcStringUtil.reverseString(content);
            int end = HcStringUtil.getTextInfo(content, widget.contentStyle,
                    width:
                        ellipsisLineWidth - (ellipsisLineWidth ~/ 2).toDouble())
                .getPositionForOffset(Offset(
                    ellipsisLineWidth - (ellipsisLineWidth ~/ 2).toDouble(), 0))
                .offset;
            result += HcStringUtil.reverseString(content.substring(0, end));
          } else {
            double halfLine = (widget.maxLines ~/ 2).toDouble();
            result += _buildEllipsisEndText(content, widget.maxLines - halfLine,
                showEllipsis: false);
            result += _buildEllipsisStartText(content, halfLine);
          }
          break;
        case HcTextEllipsisPosition.end:
          result = _buildEllipsisEndText(content, widget.maxLines);
          break;
      }
      return RichText(
        text: TextSpan(
            text: _isExpand ? widget.content : result,
            style: widget.contentStyle,
            children: [if (widget.showEllipsisBtn) _ellipsisBtn()]),
      );
    });
  }
  InlineSpan _ellipsisBtn() {
    return TextSpan(
        text: _isExpand ? widget.collapseText : widget.expandText,
        style: widget.btnTextStyle,
        recognizer: TapGestureRecognizer()
          ..onTap = () async {
            bool isExpand = !_isExpand;
            if (widget.callback != null) {
              try {
                isExpand = await widget.callback!.call(_isExpand) ?? isExpand;
              } finally {}
            }
            setState(() {
              _isExpand = isExpand;
            });
          });
  }
  String _buildEllipsisStartText(content, maxLines) {
    String result = "";
    // 反转文字截取最后
    content = HcStringUtil.reverseString(content);
    // 获取文字展示宽度
    ellipsisLineWidth = _viewSize.width -
        (!widget.separateShow && widget.showEllipsisBtn ? btnWidth : 0) -
        (widget.maxLines == 1 ? dotWidth : 0);
    // 获取阶段位置
    int lastLineIndex = HcStringUtil.getTextInfo(content, widget.contentStyle,
            width: _viewSize.width)
        .getPositionForOffset(Offset(ellipsisLineWidth, 0))
        .offset;
    // 判断阶段位置展示的文字是否超过最大限度
    double resultWidth = HcStringUtil.boundingTextSize(
            content.substring(0, lastLineIndex), widget.contentStyle)
        .width;
    // 修正最大截取距离
    lastLineIndex =
        resultWidth > ellipsisLineWidth ? lastLineIndex - 1 : lastLineIndex;
    String temp = content.substring(0, lastLineIndex);
    if (maxLines > 1) {
      content = content.substring(lastLineIndex);
      if (maxLines > 2) {
        int centerIndex = HcStringUtil.getTextInfo(content, widget.contentStyle,
                width: _viewSize.width)
            .getPositionForOffset(
                Offset(_viewSize.width, fontHeight * (maxLines - 3)))
            .offset;
        temp += content.substring(0, centerIndex);
        content = content.substring(centerIndex);
      }
      int firstIndex = HcStringUtil.getTextInfo(content, widget.contentStyle,
              width: _viewSize.width - dotWidth)
          .getPositionForOffset(Offset(_viewSize.width - dotWidth, 0))
          .offset;
      temp += content.substring(0, firstIndex);
    }
    result = widget.ellipsisDots;
    result += HcStringUtil.reverseString(temp);
    return result;
  }
  String _buildEllipsisEndText(content, maxLines, {bool showEllipsis = true}) {
    String result = "";
    if (maxLines > 1) {
      int position = HcStringUtil.getTextInfo(content, widget.contentStyle,
              width: _viewSize.width)
          .getPositionForOffset(
              Offset(_viewSize.width, fontHeight * (maxLines - 2)))
          .offset;
      result = content.substring(0, position);
      content = content.substring(position);
    }
    ellipsisLineWidth = _viewSize.width -
        (showEllipsis && (!widget.separateShow && widget.showEllipsisBtn)
            ? btnWidth
            : 0) -
        dotWidth;
    int position = HcStringUtil.getTextInfo(content, widget.contentStyle,
            width: ellipsisLineWidth)
        .getPositionForOffset(Offset(ellipsisLineWidth, 0))
        .offset;
    result += content.substring(0, position);
    if (showEllipsis) {
      result += widget.ellipsisDots;
    }
    return result;
  }
}
 static Size boundingTextSize(String text, TextStyle style,
      {int maxLines = 2 ^ 31,
      double maxWidth = double.infinity,
      textDirection = TextDirection.ltr}) {
    if (text.isEmpty) {
      return Size.zero;
    }
    final TextPainter textPainter = TextPainter(
        textDirection: textDirection,
        text: TextSpan(text: text, style: style),
        maxLines: maxLines)
      ..layout(maxWidth: maxWidth);
    return textPainter.size;
  }
  /// 获取文字信息
  static TextPainter getTextInfo(String text, TextStyle style,
      {int maxLines = 2 ^ 23,
      double width = double.infinity,
      String? ellipsis,
      textDirection = TextDirection.ltr}) {
    TextSpan span = TextSpan(text: text, style: style);
    return TextPainter(
      text: span,
      maxLines: maxLines,
      ellipsis: ellipsis,
      textDirection: textDirection,
    )..layout(maxWidth: width);
  }
  ///翻转文字
  static String reverseString(String str) {
    return str.split("").reversed.join();
  }