import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; enum TrimMode { Length, Line, } class ReadMoreText extends StatefulWidget { const ReadMoreText( this.data, { Key key, this.trimExpandedText = ' read less', this.trimCollapsedText = ' ...read more', this.colorClickableText, this.trimLength = 240, this.trimLines = 2, this.trimMode = TrimMode.Length, this.style, this.textAlign, this.textDirection, this.locale, this.textScaleFactor, this.semanticsLabel, }) : assert(data != null), super(key: key); final String data; final String trimExpandedText; final String trimCollapsedText; final Color colorClickableText; final int trimLength; final int trimLines; final TrimMode trimMode; final TextStyle style; final TextAlign textAlign; final TextDirection textDirection; final Locale locale; final double textScaleFactor; final String semanticsLabel; @override ReadMoreTextState createState() => ReadMoreTextState(); } const String _kEllipsis = '\u2026'; const String _kLineSeparator = '\u2028'; class ReadMoreTextState extends State { bool _readMore = true; void _onTapLink() { setState(() => _readMore = !_readMore); } @override Widget build(BuildContext context) { final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); TextStyle effectiveTextStyle = widget.style; if (widget.style == null || widget.style.inherit) { effectiveTextStyle = defaultTextStyle.style.merge(widget.style); } final textAlign = widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start; final textDirection = widget.textDirection ?? Directionality.of(context); final textScaleFactor = widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context); final overflow = defaultTextStyle.overflow; final locale = widget.locale ?? Localizations.localeOf(context); final colorClickableText = widget.colorClickableText ?? Theme.of(context).accentColor; TextSpan link = TextSpan( text: _readMore ? widget.trimCollapsedText : widget.trimExpandedText, style: effectiveTextStyle.copyWith( color: colorClickableText, ), recognizer: TapGestureRecognizer()..onTap = _onTapLink, ); Widget result = LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { assert(constraints.hasBoundedWidth); final double maxWidth = constraints.maxWidth; // Create a TextSpan with data final text = TextSpan( style: effectiveTextStyle, text: widget.data, ); // Layout and measure link TextPainter textPainter = TextPainter( text: link, textAlign: textAlign, textDirection: textDirection, textScaleFactor: textScaleFactor, maxLines: widget.trimLines, ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null, locale: locale, ); textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth); final linkSize = textPainter.size; // Layout and measure text textPainter.text = text; textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth); final textSize = textPainter.size; print('linkSize $linkSize textSize $textSize'); // Get the endIndex of data bool linkLongerThanLine = false; int endIndex; if (linkSize.width < maxWidth) { final pos = textPainter.getPositionForOffset(Offset( textSize.width - linkSize.width, textSize.height, )); endIndex = textPainter.getOffsetBefore(pos.offset); } else { var pos = textPainter.getPositionForOffset( textSize.bottomLeft(Offset.zero), ); endIndex = pos.offset; linkLongerThanLine = true; } var textSpan; switch (widget.trimMode) { case TrimMode.Length: if (widget.trimLength < widget.data.length) { textSpan = TextSpan( style: effectiveTextStyle, text: _readMore ? widget.data.substring(0, widget.trimLength) : widget.data, children: [link], ); } else { textSpan = TextSpan( style: effectiveTextStyle, text: widget.data, ); } break; case TrimMode.Line: if (textPainter.didExceedMaxLines) { textSpan = TextSpan( style: effectiveTextStyle, text: _readMore ? widget.data.substring(0, endIndex) + (linkLongerThanLine ? _kLineSeparator : '') : widget.data, children: [link], ); } else { textSpan = TextSpan( style: effectiveTextStyle, text: widget.data, ); } break; default: throw Exception( 'TrimMode type: ${widget.trimMode} is not supported'); } return RichText( textAlign: textAlign, textDirection: textDirection, softWrap: true, //softWrap, overflow: TextOverflow.clip, //overflow, textScaleFactor: textScaleFactor, text: textSpan, ); }, ); if (widget.semanticsLabel != null) { result = Semantics( textDirection: widget.textDirection, label: widget.semanticsLabel, child: ExcludeSemantics( child: result, ), ); } return result; } }