Skip to content

Instantly share code, notes, and snippets.

@ltvu93
Last active June 7, 2024 04:29
Show Gist options
  • Save ltvu93/81c6a62b939ab739d8b2fd14f80de176 to your computer and use it in GitHub Desktop.
Save ltvu93/81c6a62b939ab739d8b2fd14f80de176 to your computer and use it in GitHub Desktop.
import 'dart:ui';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
const text =
'Flutter is an open-source UI software development kit created by Google. It is used to develop cross platform applications from a single codebase for any web browser, Fuchsia, Android, iOS, Linux, macOS, and Windows. First described in 2015, Flutter was released in May 2017.';
return MaterialApp(
home: Scaffold(
body: SafeArea(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
text,
// maxLines: 2,
),
SizedBox(height: 20),
ReadMoreText(
content: text,
maxLines: 2,
),
],
),
),
),
);
}
}
class ReadMoreText extends StatefulWidget {
final TextStyle? style;
final String content;
final String showMoreText;
final String showLessText;
final int maxLines;
const ReadMoreText({
super.key,
this.style,
required this.content,
this.showMoreText = 'show more',
this.showLessText = 'show less',
this.maxLines = 2,
});
@override
State<ReadMoreText> createState() => _ReadMoreTextState();
}
const String _kEllipsis = '\u2026';
class _ReadMoreTextState extends State<ReadMoreText> {
bool _isExpanded = false;
TextStyle get style {
final defaultTextStyle = DefaultTextStyle.of(context);
TextStyle effectiveTextStyle = defaultTextStyle.style;
if (widget.style == null || widget.style!.inherit) {
effectiveTextStyle = defaultTextStyle.style.merge(widget.style);
}
return effectiveTextStyle;
}
TextDirection get textDirection => Directionality.of(context);
TextSpan get contentTextSpan => TextSpan(
text: widget.content,
style: style,
);
TextSpan get ellipsisTextSpan => TextSpan(
text: _kEllipsis,
style: style,
);
TextSpan get spaceTextSpan => TextSpan(
text: ' ',
style: style,
);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final contentTextPainter = TextPainter(
text: contentTextSpan,
textDirection: textDirection,
);
contentTextPainter.layout(maxWidth: width);
final contentLines = contentTextPainter.computeLineMetrics();
final spaceTextPainter = TextPainter(
text: spaceTextSpan,
textDirection: textDirection,
);
spaceTextPainter.layout(maxWidth: width);
final spaceWidth = spaceTextPainter.width;
if (_isExpanded) {
return _buildShowLessContent(width, contentLines, spaceWidth);
}
return _buildShowMoreContent(width, contentLines, spaceWidth, contentTextPainter);
},
);
}
Widget _buildShowLessContent(double width, List<LineMetrics> contentLines, double spaceWidth) {
final showLessTextSpan = TextSpan(
text: widget.showLessText,
style: style.copyWith(color: Colors.pinkAccent),
recognizer: TapGestureRecognizer()
..onTap = () {
setState(() {
_isExpanded = false;
});
},
);
final showLessTextPainter = TextPainter(
text: showLessTextSpan,
textDirection: textDirection,
);
showLessTextPainter.layout(maxWidth: width);
final showLessWidth = showLessTextPainter.width;
final lastLine = contentLines.last;
final isShowLessFitOnLastLine = lastLine.width + spaceWidth + showLessWidth <= width;
return Text.rich(
TextSpan(
children: [
TextSpan(
text: widget.content,
),
isShowLessFitOnLastLine ? spaceTextSpan : TextSpan(text: '\n'),
showLessTextSpan,
],
style: style,
),
);
}
Widget _buildShowMoreContent(
double width, List<LineMetrics> contentLines, double spaceWidth, TextPainter contentTextPainter) {
final showMoreTextSpan = TextSpan(
text: widget.showMoreText,
style: style.copyWith(color: Colors.pinkAccent),
recognizer: TapGestureRecognizer()
..onTap = () {
setState(() {
_isExpanded = true;
});
},
);
final isExceedMaxLine = contentLines.length > widget.maxLines;
String content = widget.content;
if (isExceedMaxLine) {
final ellipsisTextPainter = TextPainter(
text: ellipsisTextSpan,
textDirection: textDirection,
);
ellipsisTextPainter.layout(maxWidth: width);
final ellipsisWidth = ellipsisTextPainter.width;
final spaceTextPainter = TextPainter(
text: spaceTextSpan,
textDirection: textDirection,
);
spaceTextPainter.layout(maxWidth: width);
final showMoreTextPainter = TextPainter(
text: showMoreTextSpan,
textDirection: textDirection,
);
showMoreTextPainter.layout(maxWidth: width);
final showMoreWidth = showMoreTextPainter.width;
final collapseLastLine = contentLines[widget.maxLines];
final collapseLastLineLeft = width - (ellipsisWidth + spaceWidth + showMoreWidth);
final collapseLastLineTop = collapseLastLine.baseline - collapseLastLine.ascent;
final collapseLastLineEndPos =
contentTextPainter.getPositionForOffset(Offset(collapseLastLineLeft, collapseLastLineTop));
final collapseTextEndIndex = contentTextPainter.getOffsetBefore(collapseLastLineEndPos.offset) ?? 0;
final collapseText = widget.content.substring(0, collapseTextEndIndex);
content = collapseText.trimRight();
}
return Text.rich(
TextSpan(
children: [
TextSpan(
text: content,
),
if (isExceedMaxLine) ...[
ellipsisTextSpan,
spaceTextSpan,
showMoreTextSpan,
]
],
),
style: style,
);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment