I have implemented this using both callback functions and a stack since I felt that this would give me the most flexibility, e.g. if I wanted to hide/show the Text widgets with different start/end times to give it a more organic feel. This works, but as per my original question I am open to suggestions if there is a better way to implement this.
The basic execution workflow in pseudo-code is:
Grid Display
shuffle.onPressed() {
disable user input;
iterate over the grid {
if (cell contains a text value) {
push Text widget key onto a stack (List);
trigger the hide animation (pass callback #1);
}
}
}
Text widget hide animation
hide animation.whenComplete() {
call the next function (callback #1 - pass widget key);
}
Callback function #1
remove Text widget key from the stack;
if (stack is empty) {
executive shuffle function;
iterate over the grid;
if (cell contains a text value) {
push Text widget key onto a stack (List);
trigger the show animation (pass callback #2);
}
}
Text widget show animation
show animation.whenComplete() {
call the next function (callback #2 - pass widget key);
}
Callback function #2
remove Text widget key from the stack
if (stack is empty) {
enable user input;
}
I have included extracts of the code below to show how I have implemented this.
The main class showing the grid on screen has the following variables and functions.
class GridState extends State<Grid> {
// List containing Text widgets to displays in cells including unique
// keys and text values
final List<TextWidget> _letterList = _generateList(_generateKeys());
// Keys of animated widgets - used to track when these finish
final List<GlobalKey<TextWidgetState>> _animations = [];
bool _isInputEnabled = true; // Flag to control user input
@override
Widget build(BuildContext context) {
…
ElevatedButton(
onPressed: () {
if (_isInputEnabled) {
_hideTiles();
}
},
child: Text('shuffle', style: TextStyle(fontSize: _fontSize)),
),
…
}
// Function to hide the tiles on the screen using their animation
void _hideTiles() {
_isInputEnabled = false; // Disable user input
// Hide the existing tiles using animation
for (int i = 0; i < _letterList.length; i++) {
// Only animate if the tile has a text value
if (_letterList[i].hasText()) {
_animations.add(_letterList[i].key as GlobalKey<LetterTileState>);
_letterList[i].hide(_shuffleAndShow);
}
}
}
// Function to shuffle the text on screen and then re-show the tiles using
// their animations
void _shuffleAndShow() {
_animations.remove(key);
if (_animations.isEmpty) {
widget._letterGrid.shuffleText(
widget._letterGrid.getInputText(), widget._options.getCharType());
// Update the tiles with the new characters and show the new tile locations using animation
for (int i = 0; i < _letterList.length; i++) {
// Update tile with new character
_letterList[i].setText(
widget._letterGrid.getCell(i, widget._options.getCharType()));
// If the tile has a character then animate it
if (_letterList[i].hasText()) {
_animations.add(_letterList[i].key as GlobalKey<LetterTileState>);
_letterList[i].show(_enableInput);
}
}
}
}
// Function re-enable user input following the shuffle animations
void _enableInput(GlobalKey<LetterTileState> key) {
_animations.remove(key);
if (_animations.isEmpty) {
_isInputEnabled = true;
}
}
The Text widgets held in _letterList have the following animation functions, which call the callback function when they have finished. Note this code is in the State of a Statefulwidget.
// Animation variables
final Duration _timer = const Duration(milliseconds: 700);
late AnimationController _animationController;
late Animation<double> _rotateAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
// Set up the animation variables
_animationController = AnimationController(vsync: this, duration: _timer);
_rotateAnimation = Tween<double>(begin: 0, end: 6 * pi).animate(
CurvedAnimation(
parent: _animationController,
curve: const Interval(0, 1, curve: Curves.easeIn)));
_rotateAnimation.addListener(() {
setState(() {});
});
_scaleAnimation = Tween<double>(begin: 1, end: 0).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0, 0.95, curve: Curves.ease)));
_scaleAnimation.addListener(() {
setState(() {});
});
}
///
/// Animation functions
///
// Function to hide the tile - spin and shrink to nothing
void hide(Function callback) {s
_animationController.forward(from: 0).whenComplete(() {
_animationController.reset();
callback(widget.key);
});
}
// Function to show the tile - spin and grow from nothing
void show(Function callback) {
_animationController.reverse(from: 1).whenComplete(() {
_animationController.reset();
callback(widget.key);
});
}
UPDATE:
Having done more reading and built an experimental app using the default counter example, I have found that added Listeners and StatusListeners are another, perhaps better, way to do what I want. This would also work with the stack approach I used in my earlier answer as well.
Example code below:
main class:
import 'package:flutter/material.dart';
import 'counter.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> with TickerProviderStateMixin {
Counter counter = Counter();
late AnimationController animationController;
late Animation<double> shrinkAnimation;
@override
void initState() {
animationController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 500));
shrinkAnimation = Tween<double>(begin: 1.0, end: 0.0).animate(
CurvedAnimation(
parent: animationController,
curve: const Interval(0.0, 1.0, curve: Curves.linear)));
shrinkAnimation.addListener(() {
setState(() {}); // Refresh the screen
});
shrinkAnimation.addStatusListener((status) {
switch (status) {
// Completed status is after the end of forward animation
case AnimationStatus.completed:
{
// Increment the counter
counter.increment();
// Do some work that isn't related to animation
int value = 0;
for (int i = 0; i < 1000; i++) {
value++;
}
print('finishing value is $value');
// Then reverse the animation
animationController.reverse();
}
break;
// Dismissed status is after the end of reverse animation
case AnimationStatus.dismissed:
{
animationController.reset();
}
break;
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
AnimatedBuilder(
animation: animationController,
builder: (context, child) {
return Transform.scale(
alignment: Alignment.center,
scale: shrinkAnimation.value,
child: Text(
'${counter.get()}',
style: Theme.of(context).textTheme.headline4,
),
);
}),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
animationController.forward(); // Shrink current value first
},
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
counter class:
import 'package:flutter/cupertino.dart';
class Counter {
int _counter = 0;
void increment() {
_counter++;
}
int get() {
return _counter;
}
}