From 425c3ef8a8dca6c8ef420bca1d80296aec8d280b Mon Sep 17 00:00:00 2001 From: Anthony Whitford Date: Tue, 26 May 2020 03:55:43 -0700 Subject: [PATCH] Code Review (#81) * Code Review - Removed obsolete `new` keywords. - Added types to collection and function variables. - Added `final` and `const` keywords. - Replaced `null` guards with concise `?.` and `??` operators. - Added missing null check on `dispose` for `FadeAnimatedTextKit`. - In `fade.dart`, renamed the `_RotatingTextState` class to `_FadeTextState` to be consistent with the overall pattern and avoid confusion with `_RotatingTextState` in `rotate.dart`. **Warning**: - Removed `onNextBeforePause` from `ColorizeAnimatedTextKit` because it was not referenced. * Renamed more internal State classes to match their counterpart Stateful Widget name better. * Further optimization for AnimatedBuilder. * More consistency. Simplified some comparison expressions, added child to some AnimatedBuilders, and added const keyword. --- example/lib/main.dart | 12 +- lib/src/colorize.dart | 466 ++++++++++++------------ lib/src/fade.dart | 536 +++++++++++++-------------- lib/src/rotate.dart | 606 +++++++++++++++---------------- lib/src/scale.dart | 591 +++++++++++++++--------------- lib/src/text_liquid_fill.dart | 6 +- lib/src/typer.dart | 479 ++++++++++++------------ lib/src/typewriter.dart | 563 ++++++++++++++-------------- test/animated_text_kit_test.dart | 2 +- 9 files changed, 1565 insertions(+), 1696 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index f37d0b2..1be8abd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:animated_text_kit/animated_text_kit.dart'; -void main() => runApp(new MyApp()); +void main() => runApp(MyApp()); const List labels = [ "Rotate", @@ -17,18 +17,18 @@ class MyApp extends StatefulWidget { /// This widget is the root of your application. @override MyAppState createState() { - return new MyAppState(); + return MyAppState(); } } class MyAppState extends State { @override Widget build(BuildContext context) { - return new MaterialApp( + return MaterialApp( title: 'Animated Text Kit', debugShowCheckedModeBanner: false, theme: ThemeData.dark(), - home: new MyHomePage(title: 'Animated Text Kit'), + home: MyHomePage(title: 'Animated Text Kit'), ); } } @@ -42,7 +42,7 @@ class MyHomePage extends StatefulWidget { }) : super(key: key); @override - _MyHomePageState createState() => new _MyHomePageState(); + _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { @@ -168,7 +168,7 @@ class _MyHomePageState extends State { @override Widget build(BuildContext context) { - return new Scaffold( + return Scaffold( body: Column( children: [ SizedBox( diff --git a/lib/src/colorize.dart b/lib/src/colorize.dart index 9a41731..01850f4 100644 --- a/lib/src/colorize.dart +++ b/lib/src/colorize.dart @@ -1,243 +1,223 @@ -import 'package:flutter/material.dart'; -import 'dart:async'; - -class ColorizeAnimatedTextKit extends StatefulWidget { - /// List of [String] that would be displayed subsequently in the animation. - final List text; - - /// Gives [TextStyle] to the text strings. - final TextStyle textStyle; - - /// The [Duration] of the delay between the apparition of each characters - /// - /// By default it is set to 30 milliseconds. - final Duration speed; - - /// Define the [Duration] of the pause between texts - /// - /// By default it is set to 500 milliseconds. - final Duration pause; - - /// Adds the onTap [VoidCallback] to the animated widget. - final VoidCallback onTap; - - /// Adds the onFinished [VoidCallback] to the animated widget. - /// - /// This method will run only if [isRepeatingAnimation] is set to false. - final VoidCallback onFinished; - - /// Adds the onNext [VoidCallback] to the animated widget. - /// - /// Will be called right before the next text, after the pause parameter - final Function onNext; - - /// Adds the onNextBeforePause [VoidCallback] to the animated widget. - /// - /// Will be called at the end of n-1 animation, before the pause parameter - final Function onNextBeforePause; - - /// Adds [AlignmentGeometry] property to the text in the widget. - /// - /// By default it is set to [AlignmentDirectional.topStart] - final AlignmentGeometry alignment; - - /// Adds [TextAlign] property to the text in the widget. - /// - /// By default it is set to [TextAlign.start] - final TextAlign textAlign; - - /// Set if the animation should not repeat by changing the value of it to false. - /// - /// By default it is set to true. - final bool isRepeatingAnimation; - - /// Sets if the animation should repeat forever. [isRepeatingAnimation] also - /// needs to be set to true if you want to repeat forever. - /// - /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. - final bool repeatForever; - - /// Sets the number of times animation should repeat - /// - /// By default it is set to 3 - final double totalRepeatCount; - - /// Set the colors for the gradient animation of the text. - /// - /// The [List] should contain at least two values of [Color] in it. - final List colors; - - const ColorizeAnimatedTextKit( - {Key key, - @required this.text, - this.textStyle, - @required this.colors, - this.speed, - this.pause, - this.onTap, - this.onNext, - this.onNextBeforePause, - this.onFinished, - this.alignment = AlignmentDirectional.topStart, - this.textAlign = TextAlign.start, - this.totalRepeatCount = 3, - this.repeatForever = false, - this.isRepeatingAnimation = true}) - : super(key: key); - - @override - _RotatingTextState createState() => new _RotatingTextState(); -} - -class _RotatingTextState extends State - with TickerProviderStateMixin { - AnimationController _controller; - - Animation _colorShifter, _fadeIn, _fadeOut; - double _tuning; - - Duration _speed; - Duration _pause; - - List _texts = []; - - int _index; - - bool _isCurrentlyPausing = false; - - int _currentRepeatCount; - - @override - void initState() { - super.initState(); - - _speed = widget.speed ?? Duration(milliseconds: 200); - _pause = widget.pause ?? Duration(milliseconds: 1000); - - _index = -1; - - _currentRepeatCount = 0; - - for (int i = 0; i < widget.text.length; i++) { - try { - if (widget.text[i] is Map) { - if (!widget.text[i].containsKey('text')) throw new Error(); - } - - _texts.add({ - 'text': widget.text[i]['text'], - 'speed': widget.text[i].containsKey('speed') - ? widget.text[i]['speed'] - : _speed, - 'pause': widget.text[i].containsKey('pause') - ? widget.text[i]['pause'] - : _pause, - }); - } catch (e) { - _texts.add({'text': widget.text[i], 'speed': _speed, 'pause': _pause}); - } - } - - _nextAnimation(); - } - - @override - void dispose() { - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: widget.onTap, - child: _isCurrentlyPausing || !_controller.isAnimating - ? Text( - _texts[_index]['text'], - style: widget.textStyle, - textAlign: widget.textAlign, - ) - : AnimatedBuilder( - animation: _controller, - builder: (BuildContext context, Widget child) { - Shader linearGradient = LinearGradient(colors: widget.colors) - .createShader( - Rect.fromLTWH(0.0, 0.0, _colorShifter.value, 0.0)); - return Opacity( - opacity: !(_fadeIn.value == 1.0) - ? _fadeIn.value - : _fadeOut.value, - child: Text( - _texts[_index]['text'], - style: widget.textStyle != null - ? widget.textStyle.merge(TextStyle( - foreground: Paint()..shader = linearGradient)) - : widget.textStyle, - textAlign: widget.textAlign, - ), - ); - }, - )); - } - - void _nextAnimation() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = false; - - // Handling onNext callback - if (_index > -1) { - if (widget.onNext != null) widget.onNext(_index, isLast); - } - - if (isLast) { - if (widget.isRepeatingAnimation && - (widget.repeatForever || - _currentRepeatCount != (widget.totalRepeatCount - 1))) { - _index = 0; - if (!widget.repeatForever) { - _currentRepeatCount++; - } - } else { - if (widget.onFinished != null) widget.onFinished(); - return; - } - } else { - _index++; - } - - if (mounted) setState(() {}); - - _controller = new AnimationController( - duration: _texts[_index]['speed'] * _texts[_index]['text'].length, - vsync: this, - ); - - _tuning = (300.0 * widget.colors.length) * - (widget.textStyle.fontSize / 24.0) * - 0.75 * - (_texts[_index]['text'].length / 15.0); - - _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( - parent: _controller, curve: Interval(0.0, 0.1, curve: Curves.easeOut))); - - _fadeOut = Tween(begin: 1.0, end: 1.0).animate(CurvedAnimation( - parent: _controller, curve: Interval(0.9, 1.0, curve: Curves.easeIn))); - - _colorShifter = - Tween(begin: 0.0, end: widget.colors.length * _tuning).animate( - CurvedAnimation( - parent: _controller, - curve: Interval(0.0, 1.0, curve: Curves.easeIn))) - ..addStatusListener(_animationEndCallback); - - _controller?.forward(); - } - - void _animationEndCallback(state) { - if (state == AnimationStatus.completed) { - _isCurrentlyPausing = true; - Timer(_texts[_index]['pause'], _nextAnimation); - } - } -} +import 'package:flutter/material.dart'; +import 'dart:async'; + +class ColorizeAnimatedTextKit extends StatefulWidget { + /// List of [String] that would be displayed subsequently in the animation. + final List text; + + /// Gives [TextStyle] to the text strings. + final TextStyle textStyle; + + /// The [Duration] of the delay between the apparition of each characters + /// + /// By default it is set to 30 milliseconds. + final Duration speed; + + /// Define the [Duration] of the pause between texts + /// + /// By default it is set to 500 milliseconds. + final Duration pause; + + /// Adds the onTap [VoidCallback] to the animated widget. + final VoidCallback onTap; + + /// Adds the onFinished [VoidCallback] to the animated widget. + /// + /// This method will run only if [isRepeatingAnimation] is set to false. + final VoidCallback onFinished; + + /// Adds the onNext [VoidCallback] to the animated widget. + /// + /// Will be called right before the next text, after the pause parameter + final void Function(int, bool) onNext; + + /// Adds [AlignmentGeometry] property to the text in the widget. + /// + /// By default it is set to [AlignmentDirectional.topStart] + final AlignmentGeometry alignment; + + /// Adds [TextAlign] property to the text in the widget. + /// + /// By default it is set to [TextAlign.start] + final TextAlign textAlign; + + /// Set if the animation should not repeat by changing the value of it to false. + /// + /// By default it is set to true. + final bool isRepeatingAnimation; + + /// Sets if the animation should repeat forever. [isRepeatingAnimation] also + /// needs to be set to true if you want to repeat forever. + /// + /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. + final bool repeatForever; + + /// Sets the number of times animation should repeat + /// + /// By default it is set to 3 + final double totalRepeatCount; + + /// Set the colors for the gradient animation of the text. + /// + /// The [List] should contain at least two values of [Color] in it. + final List colors; + + const ColorizeAnimatedTextKit( + {Key key, + @required this.text, + this.textStyle, + @required this.colors, + this.speed, + this.pause, + this.onTap, + this.onNext, + this.onFinished, + this.alignment = AlignmentDirectional.topStart, + this.textAlign = TextAlign.start, + this.totalRepeatCount = 3, + this.repeatForever = false, + this.isRepeatingAnimation = true}) + : super(key: key); + + @override + _ColorizeTextState createState() => _ColorizeTextState(); +} + +class _ColorizeTextState extends State + with TickerProviderStateMixin { + AnimationController _controller; + + Animation _colorShifter, _fadeIn, _fadeOut; + double _tuning; + + Duration _speed; + Duration _pause; + + List _texts = []; + + int _index; + + bool _isCurrentlyPausing = false; + + int _currentRepeatCount; + + @override + void initState() { + super.initState(); + + _speed = widget.speed ?? const Duration(milliseconds: 200); + _pause = widget.pause ?? const Duration(milliseconds: 1000); + + _index = -1; + + _currentRepeatCount = 0; + + widget.text.forEach((text) { + _texts.add({'text': text, 'speed': _speed, 'pause': _pause}); + }); + + _nextAnimation(); + } + + @override + void dispose() { + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onTap, + child: _isCurrentlyPausing || !_controller.isAnimating + ? Text( + _texts[_index]['text'], + style: widget.textStyle, + textAlign: widget.textAlign, + ) + : AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget child) { + Shader linearGradient = LinearGradient(colors: widget.colors) + .createShader( + Rect.fromLTWH(0.0, 0.0, _colorShifter.value, 0.0)); + return Opacity( + opacity: + _fadeIn.value != 1.0 ? _fadeIn.value : _fadeOut.value, + child: Text( + _texts[_index]['text'], + style: widget.textStyle != null + ? widget.textStyle.merge(TextStyle( + foreground: Paint()..shader = linearGradient)) + : widget.textStyle, + textAlign: widget.textAlign, + ), + ); + }, + ), + ); + } + + void _nextAnimation() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = false; + + // Handling onNext callback + if (_index > -1) { + widget.onNext?.call(_index, isLast); + } + + if (isLast) { + if (widget.isRepeatingAnimation && + (widget.repeatForever || + _currentRepeatCount != (widget.totalRepeatCount - 1))) { + _index = 0; + if (!widget.repeatForever) { + _currentRepeatCount++; + } + } else { + widget.onFinished?.call(); + return; + } + } else { + _index++; + } + + if (mounted) setState(() {}); + + _controller = AnimationController( + duration: _texts[_index]['speed'] * _texts[_index]['text'].length, + vsync: this, + ); + + _tuning = (300.0 * widget.colors.length) * + (widget.textStyle.fontSize / 24.0) * + 0.75 * + (_texts[_index]['text'].length / 15.0); + + _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.1, curve: Curves.easeOut))); + + _fadeOut = Tween(begin: 1.0, end: 1.0).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.9, 1.0, curve: Curves.easeIn))); + + _colorShifter = + Tween(begin: 0.0, end: widget.colors.length * _tuning).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 1.0, curve: Curves.easeIn))) + ..addStatusListener(_animationEndCallback); + + _controller?.forward(); + } + + void _animationEndCallback(state) { + if (state == AnimationStatus.completed) { + _isCurrentlyPausing = true; + Timer(_texts[_index]['pause'], _nextAnimation); + } + } +} diff --git a/lib/src/fade.dart b/lib/src/fade.dart index 489b292..e7b4d85 100644 --- a/lib/src/fade.dart +++ b/lib/src/fade.dart @@ -1,279 +1,257 @@ -import 'package:flutter/material.dart'; -import 'dart:async'; -import 'dart:math'; - -class FadeAnimatedTextKit extends StatefulWidget { - /// List of [String] that would be displayed subsequently in the animation. - final List text; - - /// Gives [TextStyle] to the text strings. - final TextStyle textStyle; - - /// Define the [Duration] of the pause between texts - /// - /// By default it is set to 500 milliseconds. - final Duration pause; - - /// Override the [Duration] of the animation by setting the duration parameter. - /// - /// This will set the total duration for the animated widget. - /// For example, if text = ["a", "b", "c"] and if you want that each animation - /// should take 3 seconds then you have to set [duration] to 3 seconds. - final Duration duration; - - /// Adds the onTap [VoidCallback] to the animated widget. - final VoidCallback onTap; - - /// Adds the onFinished [VoidCallback] to the animated widget. - /// - /// This method will run only if [isRepeatingAnimation] is set to false. - final VoidCallback onFinished; - - /// Adds the onNext [VoidCallback] to the animated widget. - /// - /// Will be called right before the next text, after the pause parameter - final Function onNext; - - /// Adds the onNextBeforePause [VoidCallback] to the animated widget. - /// - /// Will be called at the end of n-1 animation, before the pause parameter - final Function onNextBeforePause; - - /// Adds [AlignmentGeometry] property to the text in the widget. - /// - /// By default it is set to [AlignmentDirectional.topStart] - final AlignmentGeometry alignment; - - /// Adds [TextAlign] property to the text in the widget. - /// - /// By default it is set to [TextAlign.start] - final TextAlign textAlign; - - /// Sets the number of times animation should repeat - /// - /// By default it is set to 3 - final int totalRepeatCount; - - /// Sets if the animation should repeat forever. [isRepeatingAnimation] also - /// needs to be set to true if you want to repeat forever. - /// - /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. - final bool repeatForever; - - /// Set if the animation should not repeat by changing the value of it to false. - /// - /// By default it is set to true. - final bool isRepeatingAnimation; - - /// Should the animation ends up early and display full text if you tap on it ? - /// - /// By default it is set to false. - final bool displayFullTextOnTap; - - /// If on pause, should a tap remove the remaining pause time ? - /// - /// By default it is set to false. - final bool stopPauseOnTap; - - const FadeAnimatedTextKit( - {Key key, - @required this.text, - this.duration, - this.textStyle, - this.pause, - this.displayFullTextOnTap = false, - this.stopPauseOnTap = false, - this.onTap, - this.onNext, - this.onNextBeforePause, - this.onFinished, - this.totalRepeatCount = 3, - this.alignment = AlignmentDirectional.topStart, - this.textAlign = TextAlign.start, - this.repeatForever = false, - this.isRepeatingAnimation = true}) - : super(key: key); - - @override - _RotatingTextState createState() => new _RotatingTextState(); -} - -class _RotatingTextState extends State - with TickerProviderStateMixin { - Animation _fadeIn, _fadeOut; - - AnimationController _controller; - List textWidgetList = []; - - Duration _pause; - - List _texts = []; - - int _index; - - bool _isCurrentlyPausing = false; - - Timer _timer; - - int _currentRepeatCount; - - Duration _duration; - - @override - void initState() { - super.initState(); - - _pause = widget.pause ?? Duration(milliseconds: 500); - - _index = -1; - - _currentRepeatCount = 0; - - if (widget.duration == null) { - _duration = Duration(milliseconds: 2000); - } else { - _duration = widget.duration; - } - - for (int i = 0; i < widget.text.length; i++) { - try { - if (widget.text[i] is Map) { - if (!widget.text[i].containsKey('text')) throw new Error(); - } - - _texts.add({ - 'text': widget.text[i]['text'], - 'pause': widget.text[i].containsKey('pause') - ? widget.text[i]['pause'] - : _pause - }); - } catch (e) { - _texts.add({'text': widget.text[i], 'pause': _pause}); - } - } - _nextAnimation(); - } - - @override - void dispose() { - _controller?.stop(); - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: _onTap, - child: _isCurrentlyPausing || !_controller.isAnimating - ? Text( - _texts[_index]['text'], - style: widget.textStyle, - textAlign: widget.textAlign, - ) - : AnimatedBuilder( - animation: _controller, - builder: (BuildContext context, Widget child) { - return Opacity( - opacity: !(_fadeIn.value == 1.0) - ? _fadeIn.value - : _fadeOut.value, - child: Text( - _texts[_index]['text'], - style: widget.textStyle, - textAlign: widget.textAlign, - ), - ); - }, - )); - } - - void _nextAnimation() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = false; - - // Handling onNext callback - if (_index > -1) { - if (widget.onNext != null) widget.onNext(_index, isLast); - } - - if (isLast) { - if (widget.isRepeatingAnimation && - (widget.repeatForever || - _currentRepeatCount != (widget.totalRepeatCount - 1))) { - _index = 0; - if (!widget.repeatForever) { - _currentRepeatCount++; - } - } else { - if (widget.onFinished != null) widget.onFinished(); - return; - } - } else { - _index++; - } - - if (mounted) setState(() {}); - - _controller = new AnimationController( - duration: _duration, - vsync: this, - ); - - _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( - parent: _controller, curve: Interval(0.0, 0.5, curve: Curves.linear))); - - _fadeOut = Tween(begin: 1.0, end: 0.0).animate(CurvedAnimation( - parent: _controller, curve: Interval(0.8, 1.0, curve: Curves.linear))) - ..addStatusListener(_animationEndCallback); - - _controller.forward(); - } - - void _setPause() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = true; - if (mounted) setState(() {}); - - // Handle onNextBeforePause callback - if (widget.onNextBeforePause != null) - widget.onNextBeforePause(_index, isLast); - } - - void _animationEndCallback(state) { - if (state == AnimationStatus.completed) { - _isCurrentlyPausing = true; - _timer = Timer(_texts[_index]['pause'], _nextAnimation); - } - } - - void _onTap() { - int pause; - int left; - - if (widget.displayFullTextOnTap) { - if (_isCurrentlyPausing) { - if (widget.stopPauseOnTap) { - _timer?.cancel(); - _nextAnimation(); - } - } else { - pause = _texts[_index]['pause'].inMilliseconds; - left = widget.duration.inMilliseconds; - - _controller?.stop(); - - _setPause(); - - _timer = - Timer(Duration(milliseconds: max(pause, left)), _nextAnimation); - } - } - - if (widget.onTap != null) { - widget.onTap(); - } - } -} +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:math'; + +class FadeAnimatedTextKit extends StatefulWidget { + /// List of [String] that would be displayed subsequently in the animation. + final List text; + + /// Gives [TextStyle] to the text strings. + final TextStyle textStyle; + + /// Define the [Duration] of the pause between texts + /// + /// By default it is set to 500 milliseconds. + final Duration pause; + + /// Override the [Duration] of the animation by setting the duration parameter. + /// + /// This will set the total duration for the animated widget. + /// For example, if text = ["a", "b", "c"] and if you want that each animation + /// should take 3 seconds then you have to set [duration] to 3 seconds. + final Duration duration; + + /// Adds the onTap [VoidCallback] to the animated widget. + final VoidCallback onTap; + + /// Adds the onFinished [VoidCallback] to the animated widget. + /// + /// This method will run only if [isRepeatingAnimation] is set to false. + final VoidCallback onFinished; + + /// Adds the onNext [VoidCallback] to the animated widget. + /// + /// Will be called right before the next text, after the pause parameter + final void Function(int, bool) onNext; + + /// Adds the onNextBeforePause [VoidCallback] to the animated widget. + /// + /// Will be called at the end of n-1 animation, before the pause parameter + final void Function(int, bool) onNextBeforePause; + + /// Adds [AlignmentGeometry] property to the text in the widget. + /// + /// By default it is set to [AlignmentDirectional.topStart] + final AlignmentGeometry alignment; + + /// Adds [TextAlign] property to the text in the widget. + /// + /// By default it is set to [TextAlign.start] + final TextAlign textAlign; + + /// Sets the number of times animation should repeat + /// + /// By default it is set to 3 + final int totalRepeatCount; + + /// Sets if the animation should repeat forever. [isRepeatingAnimation] also + /// needs to be set to true if you want to repeat forever. + /// + /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. + final bool repeatForever; + + /// Set if the animation should not repeat by changing the value of it to false. + /// + /// By default it is set to true. + final bool isRepeatingAnimation; + + /// Should the animation ends up early and display full text if you tap on it ? + /// + /// By default it is set to false. + final bool displayFullTextOnTap; + + /// If on pause, should a tap remove the remaining pause time ? + /// + /// By default it is set to false. + final bool stopPauseOnTap; + + const FadeAnimatedTextKit( + {Key key, + @required this.text, + this.duration, + this.textStyle, + this.pause, + this.displayFullTextOnTap = false, + this.stopPauseOnTap = false, + this.onTap, + this.onNext, + this.onNextBeforePause, + this.onFinished, + this.totalRepeatCount = 3, + this.alignment = AlignmentDirectional.topStart, + this.textAlign = TextAlign.start, + this.repeatForever = false, + this.isRepeatingAnimation = true}) + : super(key: key); + + @override + _FadeTextState createState() => _FadeTextState(); +} + +class _FadeTextState extends State + with TickerProviderStateMixin { + Animation _fadeIn, _fadeOut; + + AnimationController _controller; + List textWidgetList = []; + + Duration _pause; + + List> _texts = []; + + int _index; + + bool _isCurrentlyPausing = false; + + Timer _timer; + + int _currentRepeatCount; + + Duration _duration; + + @override + void initState() { + super.initState(); + + _pause = widget.pause ?? const Duration(milliseconds: 500); + + _index = -1; + + _currentRepeatCount = 0; + + _duration = widget.duration ?? const Duration(milliseconds: 2000); + + widget.text.forEach((text) { + _texts.add({'text': text, 'pause': _pause}); + }); + + _nextAnimation(); + } + + @override + void dispose() { + _controller?.stop(); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textWidget = Text( + _texts[_index]['text'], + style: widget.textStyle, + textAlign: widget.textAlign, + ); + return GestureDetector( + onTap: _onTap, + child: _isCurrentlyPausing || !_controller.isAnimating + ? textWidget + : AnimatedBuilder( + animation: _controller, + child: textWidget, + builder: (BuildContext context, Widget child) { + return Opacity( + opacity: + _fadeIn.value != 1.0 ? _fadeIn.value : _fadeOut.value, + child: child, + ); + }, + ), + ); + } + + void _nextAnimation() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = false; + + // Handling onNext callback + if (_index > -1) { + widget.onNext?.call(_index, isLast); + } + + if (isLast) { + if (widget.isRepeatingAnimation && + (widget.repeatForever || + _currentRepeatCount != (widget.totalRepeatCount - 1))) { + _index = 0; + if (!widget.repeatForever) { + _currentRepeatCount++; + } + } else { + widget.onFinished?.call(); + return; + } + } else { + _index++; + } + + if (mounted) setState(() {}); + + _controller = AnimationController( + duration: _duration, + vsync: this, + ); + + _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.linear))); + + _fadeOut = Tween(begin: 1.0, end: 0.0).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.8, 1.0, curve: Curves.linear))) + ..addStatusListener(_animationEndCallback); + + _controller.forward(); + } + + void _setPause() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = true; + if (mounted) setState(() {}); + + // Handle onNextBeforePause callback + widget.onNextBeforePause?.call(_index, isLast); + } + + void _animationEndCallback(state) { + if (state == AnimationStatus.completed) { + _isCurrentlyPausing = true; + _timer = Timer(_texts[_index]['pause'], _nextAnimation); + } + } + + void _onTap() { + if (widget.displayFullTextOnTap) { + if (_isCurrentlyPausing) { + if (widget.stopPauseOnTap) { + _timer?.cancel(); + _nextAnimation(); + } + } else { + final int pause = _texts[_index]['pause'].inMilliseconds; + final int left = widget.duration.inMilliseconds; + + _controller?.stop(); + + _setPause(); + + _timer = + Timer(Duration(milliseconds: max(pause, left)), _nextAnimation); + } + } + + widget.onTap?.call(); + } +} diff --git a/lib/src/rotate.dart b/lib/src/rotate.dart index c75c23b..7a8d5fe 100644 --- a/lib/src/rotate.dart +++ b/lib/src/rotate.dart @@ -1,315 +1,291 @@ -import 'package:flutter/material.dart'; -import 'dart:async'; - -class RotateAnimatedTextKit extends StatefulWidget { - /// List of [String] that would be displayed subsequently in the animation. - final List text; - - /// Gives [TextStyle] to the text strings. - final TextStyle textStyle; - - /// Define the [Duration] of the pause between texts - /// - /// By default it is set to 500 milliseconds. - final Duration pause; - - /// Override the [Duration] of the animation by setting the duration parameter. - /// - /// This will set the total duration for the animated widget. - /// For example, if text = ["a", "b", "c"] and if you want that each animation - /// should take 3 seconds then you have to set [duration] to 3 seconds. - final Duration duration; - - /// Override the transition height by setting the value of parameter transitionHeight. - /// - /// By default it is set to [TextStyle.fontSize] * 10 / 3. - final double transitionHeight; - - /// Adds the onTap [VoidCallback] to the animated widget. - final VoidCallback onTap; - - /// Adds the onFinished [VoidCallback] to the animated widget. - /// - /// This method will run only if [isRepeatingAnimation] is set to false. - final VoidCallback onFinished; - - /// Adds the onNext [VoidCallback] to the animated widget. - /// - /// Will be called right before the next text, after the pause parameter - final Function onNext; - - /// Adds the onNextBeforePause [VoidCallback] to the animated widget. - /// - /// Will be called at the end of n-1 animation, before the pause parameter - final Function onNextBeforePause; - - /// Adds [AlignmentGeometry] property to the text in the widget. - /// - /// By default it is set to [AlignmentDirectional.topStart] - final AlignmentGeometry alignment; - - /// Adds [TextAlign] property to the text in the widget. - /// - /// By default it is set to [TextAlign.start] - final TextAlign textAlign; - - /// Sets the number of times animation should repeat - /// - /// By default it is set to 3 - final int totalRepeatCount; - - /// Sets if the animation should repeat forever. [isRepeatingAnimation] also - /// needs to be set to true if you want to repeat forever. - /// - /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. - final bool repeatForever; - - /// Set if the animation should not repeat by changing the value of it to false. - /// - /// By default it is set to true. - final bool isRepeatingAnimation; - - /// Should the animation ends up early and display full text if you tap on it ? - /// - /// By default it is set to false. - final bool displayFullTextOnTap; - - const RotateAnimatedTextKit( - {Key key, - @required this.text, - this.textStyle, - this.transitionHeight, - this.pause, - this.onNext, - this.onNextBeforePause, - this.onFinished, - this.totalRepeatCount = 3, - this.duration, - this.onTap, - this.alignment = const Alignment(0.0, 0.0), - this.textAlign = TextAlign.start, - this.displayFullTextOnTap = false, - this.repeatForever = false, - this.isRepeatingAnimation = true}) - : super(key: key); - - @override - _RotatingTextState createState() => new _RotatingTextState(); -} - -class _RotatingTextState extends State - with TickerProviderStateMixin { - AnimationController _controller; - - double _transitionHeight; - - Animation _fadeIn, _fadeOut, _slideIn, _slideOut; - - List textWidgetList = []; - - Duration _pause; - - List _texts = []; - - int _index; - - bool _isCurrentlyPausing = false; - - Timer _timer; - - int _currentRepeatCount; - - Duration _duration; - - @override - void initState() { - super.initState(); - - _pause = widget.pause ?? Duration(milliseconds: 500); - - _index = -1; - - _currentRepeatCount = 0; - - if (widget.duration == null) { - _duration = Duration(milliseconds: 2000); - } else { - _duration = widget.duration; - } - - for (int i = 0; i < widget.text.length; i++) { - try { - if (widget.text[i] is Map) { - if (!widget.text[i].containsKey('text')) throw new Error(); - } - - _texts.add({ - 'text': widget.text[i]['text'], - 'pause': widget.text[i].containsKey('pause') - ? widget.text[i]['pause'] - : _pause - }); - } catch (e) { - _texts.add({'text': widget.text[i], 'pause': _pause}); - } - } - - _nextAnimation(); - } - - @override - void dispose() { - _controller?.stop(); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: _onTap, - child: SizedBox( - height: _transitionHeight, - child: _isCurrentlyPausing || !_controller.isAnimating - ? Text( - _texts[_index]['text'], - style: widget.textStyle, - textAlign: widget.textAlign, - ) - : AnimatedBuilder( - animation: _controller, - child: Text( - _texts[_index]['text'], - style: widget.textStyle, - textAlign: widget.textAlign, - ), - builder: (BuildContext context, Widget child) { - return AlignTransition( - alignment: - !(_slideIn.value.y == 0.0) ? _slideIn : _slideOut, - child: Opacity( - opacity: !(_fadeIn.value == 1.0) - ? _fadeIn.value - : _fadeOut.value, - child: child), - ); - }, - ))); - } - - void _nextAnimation() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = false; - - // Handling onNext callback - if (_index > -1) { - if (widget.onNext != null) widget.onNext(_index, isLast); - } - - if (isLast) { - if (widget.isRepeatingAnimation && - (widget.repeatForever || - _currentRepeatCount != (widget.totalRepeatCount - 1))) { - _index = 0; - if (!widget.repeatForever) { - _currentRepeatCount++; - } - } else { - if (widget.onFinished != null) widget.onFinished(); - return; - } - } else { - _index++; - } - - if (mounted) setState(() {}); - - if (widget.transitionHeight == null) { - _transitionHeight = widget.textStyle.fontSize * 10 / 3; - } else { - _transitionHeight = widget.transitionHeight; - } - - _controller = new AnimationController( - duration: _duration, - vsync: this, - ); - - if (_index == 0) { - _slideIn = AlignmentTween( - begin: Alignment(-1.0, -1.0).add(widget.alignment), - end: Alignment(-1.0, 0.0).add(widget.alignment)) - .animate(CurvedAnimation( - parent: _controller, - curve: Interval(0.0, 0.4, curve: Curves.linear))); - - _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( - parent: _controller, - curve: Interval(0.0, 0.4, curve: Curves.easeOut))); - } else { - _slideIn = AlignmentTween( - begin: Alignment(-1.0, -1.0).add(widget.alignment), - end: Alignment(-1.0, 0.0).add(widget.alignment)) - .animate(CurvedAnimation( - parent: _controller, - curve: Interval(0.0, 0.4, curve: Curves.linear))); - - _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( - parent: _controller, - curve: Interval(0.0, 0.4, curve: Curves.easeOut))); - } - - _slideOut = AlignmentTween( - begin: Alignment(-1.0, 0.0).add(widget.alignment), - end: new Alignment(-1.0, 1.0).add(widget.alignment), - ).animate(CurvedAnimation( - parent: _controller, curve: Interval(0.7, 1.0, curve: Curves.linear))); - - _fadeOut = Tween(begin: 1.0, end: 0.0).animate(CurvedAnimation( - parent: _controller, curve: Interval(0.7, 1.0, curve: Curves.easeIn))) - ..addStatusListener(_animationEndCallback); - - _controller.forward(); - } - - void _setPause() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = true; - if (mounted) setState(() {}); - - // Handle onNextBeforePause callback - if (widget.onNextBeforePause != null) - widget.onNextBeforePause(_index, isLast); - } - - void _animationEndCallback(state) { - if (state == AnimationStatus.completed) { - _timer = Timer(_texts[_index]['pause'], _nextAnimation); - } - } - - void _onTap() { - int pause; - - if (widget.displayFullTextOnTap) { - if (_isCurrentlyPausing) { - _timer?.cancel(); - _nextAnimation(); - } else { - pause = _texts[_index]['pause'].inMilliseconds; - - _controller?.stop(); - - _setPause(); - - _timer = Timer(Duration(milliseconds: pause), _nextAnimation); - } - } - - if (widget.onTap != null) { - widget.onTap(); - } - } -} +import 'package:flutter/material.dart'; +import 'dart:async'; + +class RotateAnimatedTextKit extends StatefulWidget { + /// List of [String] that would be displayed subsequently in the animation. + final List text; + + /// Gives [TextStyle] to the text strings. + final TextStyle textStyle; + + /// Define the [Duration] of the pause between texts + /// + /// By default it is set to 500 milliseconds. + final Duration pause; + + /// Override the [Duration] of the animation by setting the duration parameter. + /// + /// This will set the total duration for the animated widget. + /// For example, if text = ["a", "b", "c"] and if you want that each animation + /// should take 3 seconds then you have to set [duration] to 3 seconds. + final Duration duration; + + /// Override the transition height by setting the value of parameter transitionHeight. + /// + /// By default it is set to [TextStyle.fontSize] * 10 / 3. + final double transitionHeight; + + /// Adds the onTap [VoidCallback] to the animated widget. + final VoidCallback onTap; + + /// Adds the onFinished [VoidCallback] to the animated widget. + /// + /// This method will run only if [isRepeatingAnimation] is set to false. + final VoidCallback onFinished; + + /// Adds the onNext [VoidCallback] to the animated widget. + /// + /// Will be called right before the next text, after the pause parameter + final void Function(int, bool) onNext; + + /// Adds the onNextBeforePause [VoidCallback] to the animated widget. + /// + /// Will be called at the end of n-1 animation, before the pause parameter + final void Function(int, bool) onNextBeforePause; + + /// Adds [AlignmentGeometry] property to the text in the widget. + /// + /// By default it is set to [AlignmentDirectional.topStart] + final AlignmentGeometry alignment; + + /// Adds [TextAlign] property to the text in the widget. + /// + /// By default it is set to [TextAlign.start] + final TextAlign textAlign; + + /// Sets the number of times animation should repeat + /// + /// By default it is set to 3 + final int totalRepeatCount; + + /// Sets if the animation should repeat forever. [isRepeatingAnimation] also + /// needs to be set to true if you want to repeat forever. + /// + /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. + final bool repeatForever; + + /// Set if the animation should not repeat by changing the value of it to false. + /// + /// By default it is set to true. + final bool isRepeatingAnimation; + + /// Should the animation ends up early and display full text if you tap on it ? + /// + /// By default it is set to false. + final bool displayFullTextOnTap; + + const RotateAnimatedTextKit( + {Key key, + @required this.text, + this.textStyle, + this.transitionHeight, + this.pause, + this.onNext, + this.onNextBeforePause, + this.onFinished, + this.totalRepeatCount = 3, + this.duration, + this.onTap, + this.alignment = const Alignment(0.0, 0.0), + this.textAlign = TextAlign.start, + this.displayFullTextOnTap = false, + this.repeatForever = false, + this.isRepeatingAnimation = true}) + : super(key: key); + + @override + _RotatingTextState createState() => _RotatingTextState(); +} + +class _RotatingTextState extends State + with TickerProviderStateMixin { + AnimationController _controller; + + double _transitionHeight; + + Animation _fadeIn, _fadeOut, _slideIn, _slideOut; + + List textWidgetList = []; + + Duration _pause; + + List> _texts = []; + + int _index; + + bool _isCurrentlyPausing = false; + + Timer _timer; + + int _currentRepeatCount; + + Duration _duration; + + @override + void initState() { + super.initState(); + + _pause = widget.pause ?? const Duration(milliseconds: 500); + + _index = -1; + + _currentRepeatCount = 0; + + _duration = widget.duration ?? const Duration(milliseconds: 2000); + + widget.text.forEach((text) { + _texts.add({'text': text, 'pause': _pause}); + }); + + _nextAnimation(); + } + + @override + void dispose() { + _controller?.stop(); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textWidget = Text( + _texts[_index]['text'], + style: widget.textStyle, + textAlign: widget.textAlign, + ); + return GestureDetector( + onTap: _onTap, + child: SizedBox( + height: _transitionHeight, + child: _isCurrentlyPausing || !_controller.isAnimating + ? textWidget + : AnimatedBuilder( + animation: _controller, + child: textWidget, + builder: (BuildContext context, Widget child) { + return AlignTransition( + alignment: _slideIn.value.y != 0.0 ? _slideIn : _slideOut, + child: Opacity( + opacity: _fadeIn.value != 1.0 + ? _fadeIn.value + : _fadeOut.value, + child: child), + ); + }, + ), + ), + ); + } + + void _nextAnimation() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = false; + + // Handling onNext callback + if (_index > -1) { + widget.onNext?.call(_index, isLast); + } + + if (isLast) { + if (widget.isRepeatingAnimation && + (widget.repeatForever || + _currentRepeatCount != (widget.totalRepeatCount - 1))) { + _index = 0; + if (!widget.repeatForever) { + _currentRepeatCount++; + } + } else { + widget.onFinished?.call(); + return; + } + } else { + _index++; + } + + if (mounted) setState(() {}); + + if (widget.transitionHeight == null) { + _transitionHeight = widget.textStyle.fontSize * 10 / 3; + } else { + _transitionHeight = widget.transitionHeight; + } + + _controller = AnimationController( + duration: _duration, + vsync: this, + ); + + if (_index == 0) { + _slideIn = AlignmentTween( + begin: Alignment(-1.0, -1.0).add(widget.alignment), + end: Alignment(-1.0, 0.0).add(widget.alignment)) + .animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.4, curve: Curves.linear))); + + _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.4, curve: Curves.easeOut))); + } else { + _slideIn = AlignmentTween( + begin: Alignment(-1.0, -1.0).add(widget.alignment), + end: Alignment(-1.0, 0.0).add(widget.alignment)) + .animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.4, curve: Curves.linear))); + + _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.4, curve: Curves.easeOut))); + } + + _slideOut = AlignmentTween( + begin: Alignment(-1.0, 0.0).add(widget.alignment), + end: Alignment(-1.0, 1.0).add(widget.alignment), + ).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.linear))); + + _fadeOut = Tween(begin: 1.0, end: 0.0).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.7, 1.0, curve: Curves.easeIn))) + ..addStatusListener(_animationEndCallback); + + _controller.forward(); + } + + void _setPause() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = true; + if (mounted) setState(() {}); + + // Handle onNextBeforePause callback + widget.onNextBeforePause?.call(_index, isLast); + } + + void _animationEndCallback(state) { + if (state == AnimationStatus.completed) { + _timer = Timer(_texts[_index]['pause'], _nextAnimation); + } + } + + void _onTap() { + if (widget.displayFullTextOnTap) { + if (_isCurrentlyPausing) { + _timer?.cancel(); + _nextAnimation(); + } else { + _controller?.stop(); + + _setPause(); + + _timer = Timer(_texts[_index]['pause'], _nextAnimation); + } + } + + widget.onTap?.call(); + } +} diff --git a/lib/src/scale.dart b/lib/src/scale.dart index 393faf4..288bb20 100644 --- a/lib/src/scale.dart +++ b/lib/src/scale.dart @@ -1,307 +1,284 @@ -import 'package:flutter/material.dart'; -import 'dart:async'; -import 'dart:math'; - -class ScaleAnimatedTextKit extends StatefulWidget { - /// List of [String] that would be displayed subsequently in the animation. - final List text; - - /// Gives [TextStyle] to the text strings. - final TextStyle textStyle; - - /// Define the [Duration] of the pause between texts - /// - /// By default it is set to 500 milliseconds. - final Duration pause; - - /// Override the [Duration] of the animation by setting the duration parameter. - /// - /// This will set the total duration for the animated widget. - /// For example, if text = ["a", "b", "c"] and if you want that each animation - /// should take 3 seconds then you have to set [duration] to 3 seconds. - final Duration duration; - - /// Adds the onTap [VoidCallback] to the animated widget. - final VoidCallback onTap; - - /// Adds the onFinished [VoidCallback] to the animated widget. - /// - /// This method will run only if [isRepeatingAnimation] is set to false. - final VoidCallback onFinished; - - /// Adds the onNext [VoidCallback] to the animated widget. - /// - /// Will be called right before the next text, after the pause parameter - final Function onNext; - - /// Adds the onNextBeforePause [VoidCallback] to the animated widget. - /// - /// Will be called at the end of n-1 animation, before the pause parameter - final Function onNextBeforePause; - - /// Adds [AlignmentGeometry] property to the text in the widget. - /// - /// By default it is set to [AlignmentDirectional.topStart] - final AlignmentGeometry alignment; - - /// Adds [TextAlign] property to the text in the widget. - /// - /// By default it is set to [TextAlign.start] - final TextAlign textAlign; - - /// Sets the number of times animation should repeat - /// - /// By default it is set to 3 - final int totalRepeatCount; - - /// Sets if the animation should repeat forever. [isRepeatingAnimation] also - /// needs to be set to true if you want to repeat forever. - /// - /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. - final bool repeatForever; - - /// Set if the animation should not repeat by changing the value of it to false. - /// - /// By default it is set to true. - final bool isRepeatingAnimation; - - /// Set the scaling factor of the text for the animation. - /// - /// By default it is set to [double] value 0.5 - final double scalingFactor; - - /// Should the animation ends up early and display full text if you tap on it ? - /// - /// By default it is set to false. - final bool displayFullTextOnTap; - - /// If on pause, should a tap remove the remaining pause time ? - /// - /// By default it is set to false. - final bool stopPauseOnTap; - - const ScaleAnimatedTextKit( - {Key key, - @required this.text, - this.textStyle, - this.scalingFactor = 0.5, - this.pause, - this.duration, - this.onTap, - this.onNext, - this.onNextBeforePause, - this.onFinished, - this.totalRepeatCount = 3, - this.alignment = AlignmentDirectional.topStart, - this.textAlign = TextAlign.start, - this.repeatForever = false, - this.isRepeatingAnimation = true, - this.displayFullTextOnTap = false, - this.stopPauseOnTap = false}) - : super(key: key); - - @override - _RotatingTextState createState() => new _RotatingTextState(); -} - -class _RotatingTextState extends State - with TickerProviderStateMixin { - AnimationController _controller; - - Animation _fadeIn, _fadeOut, _scaleIn, _scaleOut; - List textWidgetList = []; - - Duration _pause; - - List _texts = []; - - int _index; - - bool _isCurrentlyPausing = false; - - Timer _timer; - - int _currentRepeatCount; - - Duration _duration; - - @override - void initState() { - super.initState(); - - _pause = widget.pause ?? Duration(milliseconds: 500); - - _index = -1; - - _currentRepeatCount = 0; - - if (widget.duration == null) { - _duration = Duration(milliseconds: 2000); - } else { - _duration = widget.duration; - } - - for (int i = 0; i < widget.text.length; i++) { - try { - if (widget.text[i] is Map) { - if (!widget.text[i].containsKey('text')) throw new Error(); - } - - _texts.add({ - 'text': widget.text[i]['text'], - 'pause': widget.text[i].containsKey('pause') - ? widget.text[i]['pause'] - : _pause - }); - } catch (e) { - _texts.add({'text': widget.text[i], 'pause': _pause}); - } - } - - // Start animation - _nextAnimation(); - } - - @override - void dispose() { - _controller?.stop(); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: _onTap, - child: _isCurrentlyPausing || !_controller.isAnimating - ? Text( - _texts[_index]['text'], - style: widget.textStyle, - textAlign: widget.textAlign, - ) - : AnimatedBuilder( - animation: _controller, - builder: (BuildContext context, Widget child) { - return ScaleTransition( - scale: !(_scaleIn.value == 1.0) ? _scaleIn : _scaleOut, - child: Opacity( - opacity: !(_fadeIn.value == 1.0) - ? _fadeIn.value - : _fadeOut.value, - child: Text( - _texts[_index]['text'], - style: widget.textStyle, - textAlign: widget.textAlign, - ), - ), - ); - }, - )); - } - - void _nextAnimation() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = false; - - // Handling onNext callback - if (_index > -1) { - if (widget.onNext != null) widget.onNext(_index, isLast); - } - - if (isLast) { - if (widget.isRepeatingAnimation && - (widget.repeatForever || - _currentRepeatCount != (widget.totalRepeatCount - 1))) { - _index = 0; - if (!widget.repeatForever) { - _currentRepeatCount++; - } - } else { - if (widget.onFinished != null) widget.onFinished(); - return; - } - } else { - _index++; - } - - setState(() {}); - - _controller = new AnimationController( - duration: _duration, - vsync: this, - ); - - _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( - parent: _controller, curve: Interval(0.0, 0.5, curve: Curves.easeOut))); - - _fadeOut = Tween(begin: 1.0, end: 0.0).animate(CurvedAnimation( - parent: _controller, curve: Interval(0.5, 1.0, curve: Curves.easeOut))); - - _scaleIn = Tween(begin: widget.scalingFactor, end: 1.0) - .animate(CurvedAnimation( - parent: _controller, - curve: Interval( - 0.0, - 0.5, - curve: Curves.easeOut, - ))); - _scaleOut = Tween(begin: 1.0, end: widget.scalingFactor) - .animate(CurvedAnimation( - parent: _controller, - curve: Interval( - 0.5, - 1.0, - curve: Curves.easeIn, - ))) - ..addStatusListener(_animationEndCallback); - - _controller.forward(); - } - - void _setPause() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = true; - setState(() {}); - - // Handle onNextBeforePause callback - if (widget.onNextBeforePause != null) - widget.onNextBeforePause(_index, isLast); - } - - void _animationEndCallback(state) { - if (state == AnimationStatus.completed) { - _isCurrentlyPausing = true; - _timer = Timer(_texts[_index]['pause'], _nextAnimation); - } - } - - void _onTap() { - int pause; - int left; - - if (widget.displayFullTextOnTap) { - if (_isCurrentlyPausing) { - if (widget.stopPauseOnTap) { - _timer?.cancel(); - _nextAnimation(); - } - } else { - pause = _texts[_index]['pause'].inMilliseconds; - left = _duration.inMilliseconds; - - _controller.stop(); - - _setPause(); - - _timer = - Timer(Duration(milliseconds: max(pause, left)), _nextAnimation); - } - } - - if (widget.onTap != null) { - widget.onTap(); - } - } -} +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:math'; + +class ScaleAnimatedTextKit extends StatefulWidget { + /// List of [String] that would be displayed subsequently in the animation. + final List text; + + /// Gives [TextStyle] to the text strings. + final TextStyle textStyle; + + /// Define the [Duration] of the pause between texts + /// + /// By default it is set to 500 milliseconds. + final Duration pause; + + /// Override the [Duration] of the animation by setting the duration parameter. + /// + /// This will set the total duration for the animated widget. + /// For example, if text = ["a", "b", "c"] and if you want that each animation + /// should take 3 seconds then you have to set [duration] to 3 seconds. + final Duration duration; + + /// Adds the onTap [VoidCallback] to the animated widget. + final VoidCallback onTap; + + /// Adds the onFinished [VoidCallback] to the animated widget. + /// + /// This method will run only if [isRepeatingAnimation] is set to false. + final VoidCallback onFinished; + + /// Adds the onNext [VoidCallback] to the animated widget. + /// + /// Will be called right before the next text, after the pause parameter + final void Function(int, bool) onNext; + + /// Adds the onNextBeforePause [VoidCallback] to the animated widget. + /// + /// Will be called at the end of n-1 animation, before the pause parameter + final void Function(int, bool) onNextBeforePause; + + /// Adds [AlignmentGeometry] property to the text in the widget. + /// + /// By default it is set to [AlignmentDirectional.topStart] + final AlignmentGeometry alignment; + + /// Adds [TextAlign] property to the text in the widget. + /// + /// By default it is set to [TextAlign.start] + final TextAlign textAlign; + + /// Sets the number of times animation should repeat + /// + /// By default it is set to 3 + final int totalRepeatCount; + + /// Sets if the animation should repeat forever. [isRepeatingAnimation] also + /// needs to be set to true if you want to repeat forever. + /// + /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. + final bool repeatForever; + + /// Set if the animation should not repeat by changing the value of it to false. + /// + /// By default it is set to true. + final bool isRepeatingAnimation; + + /// Set the scaling factor of the text for the animation. + /// + /// By default it is set to [double] value 0.5 + final double scalingFactor; + + /// Should the animation ends up early and display full text if you tap on it ? + /// + /// By default it is set to false. + final bool displayFullTextOnTap; + + /// If on pause, should a tap remove the remaining pause time ? + /// + /// By default it is set to false. + final bool stopPauseOnTap; + + const ScaleAnimatedTextKit( + {Key key, + @required this.text, + this.textStyle, + this.scalingFactor = 0.5, + this.pause, + this.duration, + this.onTap, + this.onNext, + this.onNextBeforePause, + this.onFinished, + this.totalRepeatCount = 3, + this.alignment = AlignmentDirectional.topStart, + this.textAlign = TextAlign.start, + this.repeatForever = false, + this.isRepeatingAnimation = true, + this.displayFullTextOnTap = false, + this.stopPauseOnTap = false}) + : super(key: key); + + @override + _ScaleTextState createState() => _ScaleTextState(); +} + +class _ScaleTextState extends State + with TickerProviderStateMixin { + AnimationController _controller; + + Animation _fadeIn, _fadeOut, _scaleIn, _scaleOut; + List textWidgetList = []; + + Duration _pause; + + List> _texts = []; + + int _index; + + bool _isCurrentlyPausing = false; + + Timer _timer; + + int _currentRepeatCount; + + Duration _duration; + + @override + void initState() { + super.initState(); + + _pause = widget.pause ?? const Duration(milliseconds: 500); + + _index = -1; + + _currentRepeatCount = 0; + + _duration = widget.duration ?? const Duration(milliseconds: 2000); + + widget.text.forEach((text) { + _texts.add({'text': text, 'pause': _pause}); + }); + + // Start animation + _nextAnimation(); + } + + @override + void dispose() { + _controller?.stop(); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final textWidget = Text( + _texts[_index]['text'], + style: widget.textStyle, + textAlign: widget.textAlign, + ); + return GestureDetector( + onTap: _onTap, + child: _isCurrentlyPausing || !_controller.isAnimating + ? textWidget + : AnimatedBuilder( + animation: _controller, + child: textWidget, + builder: (BuildContext context, Widget child) { + return ScaleTransition( + scale: _scaleIn.value != 1.0 ? _scaleIn : _scaleOut, + child: Opacity( + opacity: + _fadeIn.value != 1.0 ? _fadeIn.value : _fadeOut.value, + child: child, + ), + ); + }, + ), + ); + } + + void _nextAnimation() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = false; + + // Handling onNext callback + if (_index > -1) { + widget.onNext?.call(_index, isLast); + } + + if (isLast) { + if (widget.isRepeatingAnimation && + (widget.repeatForever || + _currentRepeatCount != (widget.totalRepeatCount - 1))) { + _index = 0; + if (!widget.repeatForever) { + _currentRepeatCount++; + } + } else { + widget.onFinished?.call(); + return; + } + } else { + _index++; + } + + setState(() {}); + + _controller = AnimationController( + duration: _duration, + vsync: this, + ); + + _fadeIn = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.0, 0.5, curve: Curves.easeOut))); + + _fadeOut = Tween(begin: 1.0, end: 0.0).animate(CurvedAnimation( + parent: _controller, + curve: const Interval(0.5, 1.0, curve: Curves.easeOut))); + + _scaleIn = Tween(begin: widget.scalingFactor, end: 1.0) + .animate(CurvedAnimation( + parent: _controller, + curve: const Interval( + 0.0, + 0.5, + curve: Curves.easeOut, + ))); + _scaleOut = Tween(begin: 1.0, end: widget.scalingFactor) + .animate(CurvedAnimation( + parent: _controller, + curve: const Interval( + 0.5, + 1.0, + curve: Curves.easeIn, + ))) + ..addStatusListener(_animationEndCallback); + + _controller.forward(); + } + + void _setPause() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = true; + setState(() {}); + + // Handle onNextBeforePause callback + widget.onNextBeforePause?.call(_index, isLast); + } + + void _animationEndCallback(state) { + if (state == AnimationStatus.completed) { + _isCurrentlyPausing = true; + _timer = Timer(_texts[_index]['pause'], _nextAnimation); + } + } + + void _onTap() { + if (widget.displayFullTextOnTap) { + if (_isCurrentlyPausing) { + if (widget.stopPauseOnTap) { + _timer?.cancel(); + _nextAnimation(); + } + } else { + final int pause = _texts[_index]['pause'].inMilliseconds; + final int left = _duration.inMilliseconds; + + _controller.stop(); + + _setPause(); + + _timer = + Timer(Duration(milliseconds: max(pause, left)), _nextAnimation); + } + } + + widget.onTap?.call(); + } +} diff --git a/lib/src/text_liquid_fill.dart b/lib/src/text_liquid_fill.dart index 66a8c99..e31380c 100644 --- a/lib/src/text_liquid_fill.dart +++ b/lib/src/text_liquid_fill.dart @@ -53,7 +53,7 @@ class TextLiquidFill extends StatefulWidget { : super(key: key); @override - _TextLiquidFillState createState() => new _TextLiquidFillState(); + _TextLiquidFillState createState() => _TextLiquidFillState(); } class _TextLiquidFillState extends State @@ -79,9 +79,9 @@ class _TextLiquidFillState extends State _boxWidth = widget.boxWidth ?? 400; - _waveDuration = widget.waveDuration ?? Duration(milliseconds: 2000); + _waveDuration = widget.waveDuration ?? const Duration(milliseconds: 2000); - _loadDuration = widget.loadDuration ?? Duration(milliseconds: 6000); + _loadDuration = widget.loadDuration ?? const Duration(milliseconds: 6000); _waveController = AnimationController(vsync: this, duration: _waveDuration); diff --git a/lib/src/typer.dart b/lib/src/typer.dart index 70c657a..cf958db 100644 --- a/lib/src/typer.dart +++ b/lib/src/typer.dart @@ -1,250 +1,229 @@ -import 'package:flutter/material.dart'; -import 'dart:math'; -import 'dart:async'; - -class TyperAnimatedTextKit extends StatefulWidget { - /// List of [String] that would be displayed subsequently in the animation. - final List text; - - /// Gives [TextStyle] to the text strings. - final TextStyle textStyle; - - /// The [Duration] of the delay between the apparition of each characters - /// - /// By default it is set to 40 milliseconds. - final Duration speed; - - /// Define the [Duration] of the pause between texts - /// - /// By default it is set to 1000 milliseconds. - final Duration pause; - - /// Adds the onTap [VoidCallback] to the animated widget. - final VoidCallback onTap; - - /// Adds the onFinished [VoidCallback] to the animated widget. - /// - /// This method will run only if [isRepeatingAnimation] is set to false. - final VoidCallback onFinished; - - /// Adds the onNext [VoidCallback] to the animated widget. - /// - /// Will be called right before the next text, after the pause parameter - final Function onNext; - - /// Adds the onNextBeforePause [VoidCallback] to the animated widget. - /// - /// Will be called at the end of n-1 animation, before the pause parameter - final Function onNextBeforePause; - - /// Adds [AlignmentGeometry] property to the text in the widget. - /// - /// By default it is set to [AlignmentDirectional.topStart] - final AlignmentGeometry alignment; - - /// Adds [TextAlign] property to the text in the widget. - /// - /// By default it is set to [TextAlign.start] - final TextAlign textAlign; - - /// Set if the animation should not repeat by changing the value of it to false. - /// - /// By default it is set to true. - final bool isRepeatingAnimation; - - /// Should the animation ends up early and display full text if you tap on it ? - /// - /// By default it is set to false. - final bool displayFullTextOnTap; - - /// If on pause, should a tap remove the remaining pause time ? - /// - /// By default it is set to false. - final bool stopPauseOnTap; - - const TyperAnimatedTextKit({ - Key key, - @required this.text, - this.textStyle, - this.onTap, - this.onNext, - this.onNextBeforePause, - this.onFinished, - this.alignment = AlignmentDirectional.topStart, - this.textAlign = TextAlign.start, - this.isRepeatingAnimation = true, - this.speed, - this.pause, - this.displayFullTextOnTap = false, - this.stopPauseOnTap = false, - }) : super(key: key); - - @override - _TyperState createState() => new _TyperState(); -} - -class _TyperState extends State - with TickerProviderStateMixin { - AnimationController _controller; - Animation _typingText; - List textWidgetList = []; - - Duration _speed; - Duration _pause; - - List _texts = []; - - int _index; - - bool _isCurrentlyPausing = false; - - Timer _timer; - - @override - void initState() { - super.initState(); - - _speed = widget.speed ?? Duration(milliseconds: 40); - _pause = widget.pause ?? Duration(milliseconds: 1000); - - _index = -1; - - for (int i = 0; i < widget.text.length; i++) { - try { - if (widget.text[i] is Map) { - if (!widget.text[i].containsKey('text')) throw new Error(); - } - - _texts.add({ - 'text': widget.text[i]['text'], - 'speed': widget.text[i].containsKey('speed') - ? widget.text[i]['speed'] - : _speed, - 'pause': widget.text[i].containsKey('pause') - ? widget.text[i]['pause'] - : _pause, - }); - } catch (e) { - _texts.add({'text': widget.text[i], 'speed': _speed, 'pause': _pause}); - } - } - - // Start animation - _nextAnimation(); - } - - @override - void dispose() { - _controller?.stop(); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: _onTap, - child: _isCurrentlyPausing || !_controller.isAnimating - ? Text( - _texts[_index]['text'], - style: widget.textStyle, - textAlign: widget.textAlign, - ) - : AnimatedBuilder( - animation: _controller, - builder: (BuildContext context, Widget child) { - int offset = _texts[_index]['text'].length < _typingText.value - ? _texts[_index]['text'].length - : _typingText.value; - - return Text( - _texts[_index]['text'].substring(0, offset), - style: widget.textStyle, - textAlign: widget.textAlign, - ); - }, - )); - } - - void _nextAnimation() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = false; - - // Handling onNext callback - if (_index > -1) { - if (widget.onNext != null) widget.onNext(_index, isLast); - } - - if (isLast) { - if (widget.isRepeatingAnimation) { - _index = 0; - } else { - if (widget.onFinished != null) widget.onFinished(); - return; - } - } else { - _index++; - } - - if (mounted) setState(() {}); - - _controller = new AnimationController( - duration: _texts[_index]['speed'] * _texts[_index]['text'].length, - vsync: this, - ); - - _typingText = StepTween(begin: 0, end: _texts[_index]['text'].length) - .animate(_controller) - ..addStatusListener(_animationEndCallback); - - _controller.forward(); - } - - void _setPause() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = true; - if (mounted) setState(() {}); - - // Handle onNextBeforePause callback - if (widget.onNextBeforePause != null) - widget.onNextBeforePause(_index, isLast); - } - - void _animationEndCallback(state) { - if (state == AnimationStatus.completed) { - _setPause(); - _timer = Timer(_texts[_index]['pause'], _nextAnimation); - } - } - - void _onTap() { - int pause; - int left; - - if (widget.displayFullTextOnTap) { - if (_isCurrentlyPausing) { - if (widget.stopPauseOnTap) { - _timer?.cancel(); - _nextAnimation(); - } - } else { - pause = _texts[_index]['pause'].inMilliseconds; - left = _texts[_index]['speed'].inMilliseconds * - (_texts[_index]['text'].length - _typingText.value); - - _controller.stop(); - - _setPause(); - - _timer = - Timer(Duration(milliseconds: max(pause, left)), _nextAnimation); - } - } - - if (widget.onTap != null) { - widget.onTap(); - } - } -} +import 'package:flutter/material.dart'; +import 'dart:math'; +import 'dart:async'; + +class TyperAnimatedTextKit extends StatefulWidget { + /// List of [String] that would be displayed subsequently in the animation. + final List text; + + /// Gives [TextStyle] to the text strings. + final TextStyle textStyle; + + /// The [Duration] of the delay between the apparition of each characters + /// + /// By default it is set to 40 milliseconds. + final Duration speed; + + /// Define the [Duration] of the pause between texts + /// + /// By default it is set to 1000 milliseconds. + final Duration pause; + + /// Adds the onTap [VoidCallback] to the animated widget. + final VoidCallback onTap; + + /// Adds the onFinished [VoidCallback] to the animated widget. + /// + /// This method will run only if [isRepeatingAnimation] is set to false. + final VoidCallback onFinished; + + /// Adds the onNext [VoidCallback] to the animated widget. + /// + /// Will be called right before the next text, after the pause parameter + final void Function(int, bool) onNext; + + /// Adds the onNextBeforePause [VoidCallback] to the animated widget. + /// + /// Will be called at the end of n-1 animation, before the pause parameter + final void Function(int, bool) onNextBeforePause; + + /// Adds [AlignmentGeometry] property to the text in the widget. + /// + /// By default it is set to [AlignmentDirectional.topStart] + final AlignmentGeometry alignment; + + /// Adds [TextAlign] property to the text in the widget. + /// + /// By default it is set to [TextAlign.start] + final TextAlign textAlign; + + /// Set if the animation should not repeat by changing the value of it to false. + /// + /// By default it is set to true. + final bool isRepeatingAnimation; + + /// Should the animation ends up early and display full text if you tap on it ? + /// + /// By default it is set to false. + final bool displayFullTextOnTap; + + /// If on pause, should a tap remove the remaining pause time ? + /// + /// By default it is set to false. + final bool stopPauseOnTap; + + const TyperAnimatedTextKit({ + Key key, + @required this.text, + this.textStyle, + this.onTap, + this.onNext, + this.onNextBeforePause, + this.onFinished, + this.alignment = AlignmentDirectional.topStart, + this.textAlign = TextAlign.start, + this.isRepeatingAnimation = true, + this.speed, + this.pause, + this.displayFullTextOnTap = false, + this.stopPauseOnTap = false, + }) : super(key: key); + + @override + _TyperState createState() => _TyperState(); +} + +class _TyperState extends State + with TickerProviderStateMixin { + AnimationController _controller; + Animation _typingText; + List textWidgetList = []; + + Duration _speed; + Duration _pause; + + List> _texts = []; + + int _index; + + bool _isCurrentlyPausing = false; + + Timer _timer; + + @override + void initState() { + super.initState(); + + _speed = widget.speed ?? const Duration(milliseconds: 40); + _pause = widget.pause ?? const Duration(milliseconds: 1000); + + _index = -1; + + widget.text.forEach((text) { + _texts.add({'text': text, 'speed': _speed, 'pause': _pause}); + }); + + // Start animation + _nextAnimation(); + } + + @override + void dispose() { + _controller?.stop(); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _onTap, + child: _isCurrentlyPausing || !_controller.isAnimating + ? Text( + _texts[_index]['text'], + style: widget.textStyle, + textAlign: widget.textAlign, + ) + : AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget child) { + final int offset = + _texts[_index]['text'].length < _typingText.value + ? _texts[_index]['text'].length + : _typingText.value; + + return Text( + _texts[_index]['text'].substring(0, offset), + style: widget.textStyle, + textAlign: widget.textAlign, + ); + }, + )); + } + + void _nextAnimation() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = false; + + // Handling onNext callback + if (_index > -1) { + widget.onNext?.call(_index, isLast); + } + + if (isLast) { + if (widget.isRepeatingAnimation) { + _index = 0; + } else { + widget.onFinished?.call(); + return; + } + } else { + _index++; + } + + if (mounted) setState(() {}); + + _controller = AnimationController( + duration: _texts[_index]['speed'] * _texts[_index]['text'].length, + vsync: this, + ); + + _typingText = StepTween(begin: 0, end: _texts[_index]['text'].length) + .animate(_controller) + ..addStatusListener(_animationEndCallback); + + _controller.forward(); + } + + void _setPause() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = true; + if (mounted) setState(() {}); + + // Handle onNextBeforePause callback + widget.onNextBeforePause?.call(_index, isLast); + } + + void _animationEndCallback(state) { + if (state == AnimationStatus.completed) { + _setPause(); + _timer = Timer(_texts[_index]['pause'], _nextAnimation); + } + } + + void _onTap() { + if (widget.displayFullTextOnTap) { + if (_isCurrentlyPausing) { + if (widget.stopPauseOnTap) { + _timer?.cancel(); + _nextAnimation(); + } + } else { + final int pause = _texts[_index]['pause'].inMilliseconds; + final int left = _texts[_index]['speed'].inMilliseconds * + (_texts[_index]['text'].length - _typingText.value); + + _controller.stop(); + + _setPause(); + + _timer = + Timer(Duration(milliseconds: max(pause, left)), _nextAnimation); + } + } + + widget.onTap?.call(); + } +} diff --git a/lib/src/typewriter.dart b/lib/src/typewriter.dart index bf45016..20a9439 100644 --- a/lib/src/typewriter.dart +++ b/lib/src/typewriter.dart @@ -1,292 +1,271 @@ -import 'package:flutter/material.dart'; -import 'dart:async'; -import 'dart:math'; - -class TypewriterAnimatedTextKit extends StatefulWidget { - /// List of [String] that would be displayed subsequently in the animation. - final List text; - - /// Gives [TextStyle] to the text strings. - final TextStyle textStyle; - - /// The [Duration] of the delay between the apparition of each characters. - /// - /// By default it is set to 30 milliseconds - final Duration speed; - - /// Define the [Duration] of the pause between texts - /// - /// By default it is set to 1000 milliseconds. - final Duration pause; - - /// Adds the onTap [VoidCallback] to the animated widget. - final VoidCallback onTap; - - /// Adds the onFinished [VoidCallback] to the animated widget. - /// - /// This method will run only if [isRepeatingAnimation] is set to false. - final VoidCallback onFinished; - - /// Adds the onNext [VoidCallback] to the animated widget. - /// - /// Will be called right before the next text, after the pause parameter - final Function onNext; - - /// Adds the onNextBeforePause [VoidCallback] to the animated widget. - /// - /// Will be called at the end of n-1 animation, before the pause parameter - final Function onNextBeforePause; - - /// Adds [AlignmentGeometry] property to the text in the widget. - /// - /// By default it is set to [AlignmentDirectional.topStart] - final AlignmentGeometry alignment; - - /// Adds [TextAlign] property to the text in the widget. - /// - /// By default it is set to [TextAlign.start] - final TextAlign textAlign; - - /// Sets the number of times animation should repeat - /// - /// By default it is set to 3 - final int totalRepeatCount; - - /// Sets if the animation should repeat forever. [isRepeatingAnimation] also - /// needs to be set to true if you want to repeat forever. - /// - /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. - final bool repeatForever; - - /// Set if the animation should not repeat by changing the value of it to false. - /// - /// By default it is set to true. - final bool isRepeatingAnimation; - - /// Should the animation ends up early and display full text if you tap on it ? - /// - /// By default it is set to false. - final bool displayFullTextOnTap; - - /// If on pause, should a tap remove the remaining pause time ? - /// - /// By default it is set to false. - final bool stopPauseOnTap; - - TypewriterAnimatedTextKit( - {Key key, - @required this.text, - this.textStyle, - this.speed, - this.pause, - this.displayFullTextOnTap = false, - this.stopPauseOnTap = false, - this.onTap, - this.onNext, - this.onNextBeforePause, - this.onFinished, - this.totalRepeatCount = 3, - this.alignment = AlignmentDirectional.topStart, - this.textAlign = TextAlign.start, - this.repeatForever = false, - this.isRepeatingAnimation = true}) - : assert(!(text == null), 'You should specify the list of text'), - super(key: key); - - @override - _TypewriterState createState() => new _TypewriterState(); -} - -class _TypewriterState extends State - with TickerProviderStateMixin { - AnimationController _controller; - - Animation _typewriterText; - List textWidgetList = []; - - Duration _speed; - Duration _pause; - - List _texts = []; - - int _index; - - bool _isCurrentlyPausing = false; - - Timer _timer; - - int _currentRepeatCount; - - @override - void initState() { - super.initState(); - - _speed = widget.speed ?? Duration(milliseconds: 30); - _pause = widget.pause ?? Duration(milliseconds: 1000); - - _index = -1; - - _currentRepeatCount = 0; - - for (int i = 0; i < widget.text.length; i++) { - try { - if (widget.text[i] is Map) { - if (!widget.text[i].containsKey('text')) throw new Error(); - } - - _texts.add({ - 'text': widget.text[i]['text'], - 'speed': widget.text[i].containsKey('speed') - ? widget.text[i]['speed'] - : _speed, - 'pause': widget.text[i].containsKey('pause') - ? widget.text[i]['pause'] - : _pause - }); - } catch (e) { - _texts.add({'text': widget.text[i], 'speed': _speed, 'pause': _pause}); - } - } - - _nextAnimation(); - } - - @override - void dispose() { - _controller?.stop(); - _controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: _onTap, - child: _isCurrentlyPausing || !_controller.isAnimating - ? Text( - _texts[_index]['text'], - style: widget.textStyle, - textAlign: widget.textAlign, - ) - : AnimatedBuilder( - animation: _controller, - builder: (BuildContext context, Widget child) { - String visibleString = _texts[_index]['text']; - if (_typewriterText.value == 0) { - visibleString = ""; - } else if (_typewriterText.value > - _texts[_index]['text'].length) { - if ((_typewriterText.value - - _texts[_index]['text'].length) % - 2 == - 0) { - visibleString = _texts[_index]['text'] - .substring(0, _texts[_index]['text'].length) + - '_'; - } else { - visibleString = _texts[_index]['text'] - .substring(0, _texts[_index]['text'].length); - } - } else { - visibleString = _texts[_index]['text'] - .substring(0, _typewriterText.value) + - '_'; - } - - return Text( - visibleString, - style: widget.textStyle, - textAlign: widget.textAlign, - ); - }, - )); - } - - void _nextAnimation() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = false; - - // Handling onNext callback - if (_index > -1) { - if (widget.onNext != null) widget.onNext(_index, isLast); - } - - if (isLast) { - if (widget.isRepeatingAnimation && - (widget.repeatForever || - _currentRepeatCount != (widget.totalRepeatCount - 1))) { - _index = 0; - if (!widget.repeatForever) { - _currentRepeatCount++; - } - } else { - if (widget.onFinished != null) widget.onFinished(); - return; - } - } else { - _index++; - } - - if (mounted) setState(() {}); - - _controller = new AnimationController( - duration: _texts[_index]['speed'] * _texts[_index]['text'].length, - vsync: this, - ); - - _typewriterText = - StepTween(begin: 0, end: _texts[_index]['text'].length + 8) - .animate(_controller) - ..addStatusListener(_animationEndCallback); - - _controller.forward(); - } - - void _setPause() { - bool isLast = _index == widget.text.length - 1; - - _isCurrentlyPausing = true; - if (mounted) setState(() {}); - - // Handle onNextBeforePause callback - if (widget.onNextBeforePause != null) - widget.onNextBeforePause(_index, isLast); - } - - void _animationEndCallback(state) { - if (state == AnimationStatus.completed) { - _setPause(); - _timer = Timer(_texts[_index]['pause'], _nextAnimation); - } - } - - void _onTap() { - int pause; - int left; - - if (widget.displayFullTextOnTap) { - if (_isCurrentlyPausing) { - if (widget.stopPauseOnTap) { - _timer?.cancel(); - _nextAnimation(); - } - } else { - pause = _texts[_index]['pause'].inMilliseconds; - left = _texts[_index]['speed'].inMilliseconds * - (_texts[_index]['text'].length - _typewriterText.value); - - _controller.stop(); - - _setPause(); - - _timer = - Timer(Duration(milliseconds: max(pause, left)), _nextAnimation); - } - } - - if (widget.onTap != null) { - widget.onTap(); - } - } -} +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:math'; + +class TypewriterAnimatedTextKit extends StatefulWidget { + /// List of [String] that would be displayed subsequently in the animation. + final List text; + + /// Gives [TextStyle] to the text strings. + final TextStyle textStyle; + + /// The [Duration] of the delay between the apparition of each characters. + /// + /// By default it is set to 30 milliseconds + final Duration speed; + + /// Define the [Duration] of the pause between texts + /// + /// By default it is set to 1000 milliseconds. + final Duration pause; + + /// Adds the onTap [VoidCallback] to the animated widget. + final VoidCallback onTap; + + /// Adds the onFinished [VoidCallback] to the animated widget. + /// + /// This method will run only if [isRepeatingAnimation] is set to false. + final VoidCallback onFinished; + + /// Adds the onNext [VoidCallback] to the animated widget. + /// + /// Will be called right before the next text, after the pause parameter + final void Function(int, bool) onNext; + + /// Adds the onNextBeforePause [VoidCallback] to the animated widget. + /// + /// Will be called at the end of n-1 animation, before the pause parameter + final void Function(int, bool) onNextBeforePause; + + /// Adds [AlignmentGeometry] property to the text in the widget. + /// + /// By default it is set to [AlignmentDirectional.topStart] + final AlignmentGeometry alignment; + + /// Adds [TextAlign] property to the text in the widget. + /// + /// By default it is set to [TextAlign.start] + final TextAlign textAlign; + + /// Sets the number of times animation should repeat + /// + /// By default it is set to 3 + final int totalRepeatCount; + + /// Sets if the animation should repeat forever. [isRepeatingAnimation] also + /// needs to be set to true if you want to repeat forever. + /// + /// By default it is set to false, if set to true, [totalRepeatCount] is ignored. + final bool repeatForever; + + /// Set if the animation should not repeat by changing the value of it to false. + /// + /// By default it is set to true. + final bool isRepeatingAnimation; + + /// Should the animation ends up early and display full text if you tap on it ? + /// + /// By default it is set to false. + final bool displayFullTextOnTap; + + /// If on pause, should a tap remove the remaining pause time ? + /// + /// By default it is set to false. + final bool stopPauseOnTap; + + TypewriterAnimatedTextKit( + {Key key, + @required this.text, + this.textStyle, + this.speed, + this.pause, + this.displayFullTextOnTap = false, + this.stopPauseOnTap = false, + this.onTap, + this.onNext, + this.onNextBeforePause, + this.onFinished, + this.totalRepeatCount = 3, + this.alignment = AlignmentDirectional.topStart, + this.textAlign = TextAlign.start, + this.repeatForever = false, + this.isRepeatingAnimation = true}) + : assert(text != null, 'You must specify the list of text'), + super(key: key); + + @override + _TypewriterState createState() => _TypewriterState(); +} + +class _TypewriterState extends State + with TickerProviderStateMixin { + AnimationController _controller; + + Animation _typewriterText; + List textWidgetList = []; + + Duration _speed; + Duration _pause; + + List> _texts = []; + + int _index; + + bool _isCurrentlyPausing = false; + + Timer _timer; + + int _currentRepeatCount; + + @override + void initState() { + super.initState(); + + _speed = widget.speed ?? const Duration(milliseconds: 30); + _pause = widget.pause ?? const Duration(milliseconds: 1000); + + _index = -1; + + _currentRepeatCount = 0; + + widget.text.forEach((text) { + _texts.add({'text': text, 'speed': _speed, 'pause': _pause}); + }); + + _nextAnimation(); + } + + @override + void dispose() { + _controller?.stop(); + _controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: _onTap, + child: _isCurrentlyPausing || !_controller.isAnimating + ? Text( + _texts[_index]['text'], + style: widget.textStyle, + textAlign: widget.textAlign, + ) + : AnimatedBuilder( + animation: _controller, + builder: (BuildContext context, Widget child) { + String visibleString = _texts[_index]['text']; + if (_typewriterText.value == 0) { + visibleString = ""; + } else if (_typewriterText.value > + _texts[_index]['text'].length) { + if ((_typewriterText.value - + _texts[_index]['text'].length) % + 2 == + 0) { + visibleString = _texts[_index]['text'] + .substring(0, _texts[_index]['text'].length) + + '_'; + } else { + visibleString = _texts[_index]['text'] + .substring(0, _texts[_index]['text'].length); + } + } else { + visibleString = _texts[_index]['text'] + .substring(0, _typewriterText.value) + + '_'; + } + + return Text( + visibleString, + style: widget.textStyle, + textAlign: widget.textAlign, + ); + }, + )); + } + + void _nextAnimation() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = false; + + // Handling onNext callback + if (_index > -1) { + widget.onNext?.call(_index, isLast); + } + + if (isLast) { + if (widget.isRepeatingAnimation && + (widget.repeatForever || + _currentRepeatCount != (widget.totalRepeatCount - 1))) { + _index = 0; + if (!widget.repeatForever) { + _currentRepeatCount++; + } + } else { + widget.onFinished?.call(); + return; + } + } else { + _index++; + } + + if (mounted) setState(() {}); + + _controller = AnimationController( + duration: _texts[_index]['speed'] * _texts[_index]['text'].length, + vsync: this, + ); + + _typewriterText = + StepTween(begin: 0, end: _texts[_index]['text'].length + 8) + .animate(_controller) + ..addStatusListener(_animationEndCallback); + + _controller.forward(); + } + + void _setPause() { + final bool isLast = _index == widget.text.length - 1; + + _isCurrentlyPausing = true; + if (mounted) setState(() {}); + + // Handle onNextBeforePause callback + if (widget.onNextBeforePause != null) + widget.onNextBeforePause(_index, isLast); + } + + void _animationEndCallback(state) { + if (state == AnimationStatus.completed) { + _setPause(); + _timer = Timer(_texts[_index]['pause'], _nextAnimation); + } + } + + void _onTap() { + if (widget.displayFullTextOnTap) { + if (_isCurrentlyPausing) { + if (widget.stopPauseOnTap) { + _timer?.cancel(); + _nextAnimation(); + } + } else { + final int pause = _texts[_index]['pause'].inMilliseconds; + final int left = _texts[_index]['speed'].inMilliseconds * + (_texts[_index]['text'].length - _typewriterText.value); + + _controller.stop(); + + _setPause(); + + _timer = + Timer(Duration(milliseconds: max(pause, left)), _nextAnimation); + } + } + + widget.onTap?.call(); + } +} diff --git a/test/animated_text_kit_test.dart b/test/animated_text_kit_test.dart index d11231d..4d8aa19 100644 --- a/test/animated_text_kit_test.dart +++ b/test/animated_text_kit_test.dart @@ -5,7 +5,7 @@ import '../example/lib/main.dart'; void main() { testWidgets('Check button smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(new MyApp()); + await tester.pumpWidget(MyApp()); expect(find.text(labels[0]), findsOneWidget);