Skip to content

Instantly share code, notes, and snippets.

@ltvu93
Last active April 8, 2025 04:04
Show Gist options
  • Save ltvu93/cf896869e73d8df74283e48b8bc16e43 to your computer and use it in GitHub Desktop.
Save ltvu93/cf896869e73d8df74283e48b8bc16e43 to your computer and use it in GitHub Desktop.
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(
const MaterialApp(
home: TextAnimatedTextScreen(),
),
);
}
class TextAnimatedTextScreen extends StatelessWidget {
const TextAnimatedTextScreen({super.key});
final text =
'This is a very long text that will be broken into multiple lines based on the screen width. Each line will be positioned individually in a Stack widget using LineMetrics to calculate the exact position. You can add more text here to see how it wraps and positions each line.';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Animated Pharse Text'),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: AnimatedPharseText(
text: text,
startStyle: TextStyle(fontSize: 16),
endStyle: TextStyle(fontSize: 20),
),
),
),
),
);
}
}
class AnimatedPharseText extends StatefulWidget {
final String text;
final TextStyle startStyle;
final TextStyle endStyle;
const AnimatedPharseText({
super.key,
required this.text,
required this.startStyle,
required this.endStyle,
});
@override
State<AnimatedPharseText> createState() => _AnimatedPharseTextState();
}
class _AnimatedPharseTextState extends State<AnimatedPharseText> {
late TextPainter _startTextPainter;
late TextPainter _endTextPainter;
List<AnimatedPharse> _animatedPharses = [];
double _animatedValue = 0;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_startTextPainter = TextPainter(
text: TextSpan(
text: widget.text,
style: _mergeTextStyle(context, widget.startStyle),
),
textDirection: TextDirection.ltr,
);
_endTextPainter = TextPainter(
text: TextSpan(
text: widget.text,
style: _mergeTextStyle(context, widget.endStyle),
),
textDirection: TextDirection.ltr,
);
}
@override
void dispose() {
_startTextPainter.dispose();
_endTextPainter.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
_startTextPainter.layout(maxWidth: width);
final startTextRanges = _calculateLineTextRanges(_startTextPainter);
_endTextPainter.layout(maxWidth: width);
final endTextRanges = _calculateLineTextRanges(_endTextPainter);
final intersectTextRanges = _calculateIntersectTextRanges(startTextRanges, endTextRanges);
_animatedPharses = _calculateAnimatedPharses(intersectTextRanges);
final widgetWidth = max(_startTextPainter.width, _endTextPainter.width);
final widgetHeight = max(_startTextPainter.height, _endTextPainter.height);
return Column(
children: [
SizedBox(
width: widgetWidth,
height: widgetHeight,
child: Stack(
children: [
..._animatedPharses.map((pharse) {
return AnimatedPositioned(
duration: const Duration(milliseconds: 500),
left: lerpDouble(
pharse.startRect.left,
pharse.endRect.left,
_animatedValue / 100,
),
top: lerpDouble(
pharse.startRect.top,
pharse.endRect.top,
_animatedValue / 100,
),
child: Text(
pharse.text,
style: TextStyle.lerp(
widget.startStyle,
widget.endStyle,
_animatedValue / 100,
),
),
);
}),
],
),
),
const SizedBox(height: 10),
Slider(
value: _animatedValue,
min: 0,
max: 100,
divisions: 100,
label: _animatedValue.round().toString(),
onChanged: (value) {
setState(() {
_animatedValue = value;
});
},
),
],
);
},
);
}
TextStyle _mergeTextStyle(BuildContext context, TextStyle style) {
if (style.inherit) {
return DefaultTextStyle.of(context).style.merge(style);
}
return style;
}
List<TextRange> _calculateLineTextRanges(TextPainter textPainter) {
final lineMetrics = textPainter.computeLineMetrics();
final List<TextRange> textRanges = [];
for (final lineMetric in lineMetrics) {
final baseline = lineMetric.baseline;
final left = lineMetric.left;
final right = left + lineMetric.width;
final startPosition = textPainter.getPositionForOffset(Offset(left, baseline));
final endPosition = textPainter.getPositionForOffset(Offset(right, baseline));
textRanges.add(TextRange(start: startPosition.offset, end: endPosition.offset));
}
return textRanges;
}
List<TextRange> _calculateIntersectTextRanges(List<TextRange> textRanges1, List<TextRange> textRanges2) {
List<TextRange> result = [];
int i = 0, j = 0;
while (i < textRanges1.length && j < textRanges2.length) {
final textRange1 = textRanges1[i];
final textRange2 = textRanges2[j];
if (textRange1.end >= textRange2.start && textRange2.end >= textRange1.start) {
result.add(TextRange(
start: max(textRange1.start, textRange2.start),
end: min(textRange1.end, textRange2.end),
));
}
if (textRange1.end < textRange2.end) {
i++;
} else {
j++;
}
}
return result;
}
List<AnimatedPharse> _calculateAnimatedPharses(List<TextRange> intersectTextRanges) {
final animatedPharses = <AnimatedPharse>[];
for (final intersection in intersectTextRanges) {
final startTextBoxes = _startTextPainter.getBoxesForSelection(
TextSelection(baseOffset: intersection.start, extentOffset: intersection.end),
boxHeightStyle: BoxHeightStyle.max,
boxWidthStyle: BoxWidthStyle.max,
);
final endTextBoxes = _endTextPainter.getBoxesForSelection(
TextSelection(baseOffset: intersection.start, extentOffset: intersection.end),
boxHeightStyle: BoxHeightStyle.max,
boxWidthStyle: BoxWidthStyle.max,
);
final startRect = startTextBoxes.first.toRect();
final endRect = endTextBoxes.first.toRect();
animatedPharses.add(
AnimatedPharse(
startRect: startRect,
endRect: endRect,
text: widget.text.substring(intersection.start, intersection.end),
),
);
}
return animatedPharses;
}
}
class AnimatedPharse {
final Rect startRect;
final Rect endRect;
final String text;
AnimatedPharse({
required this.startRect,
required this.endRect,
required this.text,
});
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment