dev
dart 169 lines 4.02 KB
Raw
1 import 'package:flutter/material.dart';
2
3 class AnimatedTypingText extends StatefulWidget {
4 final List<String> words;
5 final TextStyle? style;
6 final Duration typingSpeed;
7 final Duration pauseDuration;
8 final Color cursorColor;
9 final double cursorWidth;
10 final double cursorHeight;
11 final Duration cursorBlinkDuration;
12
13 const AnimatedTypingText({
14 Key? key,
15 required this.words,
16 this.style,
17 this.typingSpeed = const Duration(milliseconds: 100),
18 this.pauseDuration = const Duration(milliseconds: 3000),
19 this.cursorColor = Colors.black,
20 this.cursorWidth = 2.0,
21 this.cursorHeight = 20.0,
22 this.cursorBlinkDuration = const Duration(milliseconds: 500),
23 }) : super(key: key);
24
25 @override
26 State<AnimatedTypingText> createState() => _AnimatedTypingTextState();
27 }
28
29 class _AnimatedTypingTextState extends State<AnimatedTypingText> with TickerProviderStateMixin {
30 late AnimationController _controller;
31 late AnimationController _cursorController;
32 late Animation<double> _cursorAnimation;
33
34 String _displayText = '';
35
36 int _currentWordIndex = 0;
37 int _currentCharIndex = 0;
38
39 bool _isTyping = true;
40 bool _isPaused = false;
41 bool _isWaitingForPause = false;
42
43 @override
44 void initState() {
45 super.initState();
46 _controller = AnimationController(
47 vsync: this,
48 duration: widget.typingSpeed,
49 );
50
51 _cursorController = AnimationController(
52 vsync: this,
53 duration: widget.cursorBlinkDuration,
54 )..repeat(reverse: true);
55
56 _cursorAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
57 CurvedAnimation(
58 parent: _cursorController,
59 curve: Curves.easeInOut,
60 ),
61 );
62
63 _controller.addStatusListener((status) {
64 if (status == AnimationStatus.completed) {
65 _updateText();
66 if (!_isWaitingForPause) {
67 _controller.reset();
68 _controller.forward();
69 }
70 }
71 });
72
73 _controller.forward();
74 }
75
76 void _updateText() {
77 if (_isWaitingForPause) return;
78
79 if (_isPaused) {
80 if (_isTyping) {
81 _isTyping = false;
82 _isWaitingForPause = true;
83 Future.delayed(widget.pauseDuration, () {
84 if (mounted) {
85 setState(() {
86 _isPaused = false;
87 _isWaitingForPause = false;
88 _controller.reset();
89 _controller.forward();
90 });
91 }
92 });
93 } else {
94 _deletePrevChar();
95 }
96 } else {
97 if (_isTyping) {
98 _typeNextChar();
99 } else {
100 _deletePrevChar();
101 }
102 }
103
104 setState(() {
105 _displayText = widget.words[_currentWordIndex].substring(0, _currentCharIndex);
106 });
107 }
108
109 void _typeNextChar() {
110 if (_currentCharIndex < widget.words[_currentWordIndex].length) {
111 _currentCharIndex++;
112 } else {
113 _isPaused = true;
114 }
115 }
116
117 void _deletePrevChar() {
118 if (_currentCharIndex > 0) {
119 _currentCharIndex--;
120 } else {
121 _currentWordIndex = (_currentWordIndex + 1) % widget.words.length;
122 _currentCharIndex = 0;
123 _isTyping = true;
124 _isPaused = false;
125 }
126 }
127
128 @override
129 void dispose() {
130 _controller.dispose();
131 _cursorController.dispose();
132 super.dispose();
133 }
134
135 @override
136 Widget build(BuildContext context) {
137 return Stack(
138 clipBehavior: Clip.none,
139 children: [
140 // Fixed cursor position
141 Positioned(
142 right: 0,
143 bottom: 0,
144 child: AnimatedBuilder(
145 animation: _cursorAnimation,
146 builder: (context, child) {
147 return Opacity(
148 opacity: _cursorAnimation.value,
149 child: Container(
150 width: widget.cursorWidth,
151 height: widget.cursorHeight,
152 color: widget.cursorColor,
153 ),
154 );
155 },
156 ),
157 ),
158 // Animated text
159 Align(
160 alignment: Alignment.centerRight,
161 child: Text(
162 _displayText + ' ',
163 style: widget.style,
164 ),
165 ),
166 ],
167 );
168 }
169 }