How to chain animation and non-animation functions in Flutter? - flutter

I have developed an anagram solving game, which presents an anagram to the user that they have to solve as per below.
The layout is a table with a series of table rows, each row containing table cells. Each table cell has a container with a Text widget that can be empty or contain a character. The characters themselves are held in a List that is the same length as the number of table cells on the screen. Each Text widget in the table cells draws its text value from the corresponding item in the List.
The player can shuffle the characters if they want to display them in a different order. I have added animations to the Text widgets so that they fade out of view, the underlying List is randomly shuffled, and then the characters fade back into view in their new positions. The basic workflow is:
User presses shuffle
The program iterates over the List of characters and triggers the fade out animation for
any Text widgets that have a text value The fade out animations last for 800 milliseconds
The programme then shuffles the target word in the text List The programme iterates of the
List of characters again and triggers the fade-in animations for any Text widgets that have
a text value
My problem is that the animations do not always perform as planned. Sometimes the characters disappear and then fade-in. Sometimes they fade-out and remain hidden. Sometimes they work as planned above. I am assuming that this is because of the timing of the animations and my code. Currently I have a sequence of code in one class that executes the activities above in one go, as per the pseudo-code below
For each table cell {
if (table cell Text widget has a value) then {
trigger the Text widget fade-out animation;
}
}
Shuffle the text List;
For each table cell {
if (table cell Text widget has a value) then {
trigger the Text widget fade-in animation;
}
}
I assume that executing the code this way is causing the problem because it means that my fade-out animations will be triggered, the underlying text List will be shuffled whilst those animations are still running and the fade-out animations will also be triggered before the fade-out animations have finished.
My question is, what is the correct design pattern to control the execution timing of the animations and the shuffle function so that they execute sequentially without overlapping?
I have looked at creating a type of stack where I push the animations and shuffle functions onto a stack and then execute them, but that feels clunky because I need to differentiate between a number of parallel fade-out animations (for example, if the word to be guessed has 8 characters then my program triggers 8 fade-out animations) and then calling the shuffle function.
As per this post, I have also looked at using the .whenComplete() method:
_animationController.forward().whenComplete(() {
// put here the stuff you wanna do when animation completed!
});
But have the same issue that I would with a stack approach in terms of coordinating this for a number of parallel animations.
I have thought about designing my Text character widget so that I could pass a flag that would trigger the .whenComplete() method for the first Text widget in the grid with a value, and just let the other Text widget fade-out animations run separately. I could then shuffle the text at the end of the first Text widget fade-out animation using a callback and trigger the fade-in animations after the shuffle function.
Again, this feels kind of clunky and I want to know if I am missing something. Is there anything built into Flutter to support animation->non-animation function->animation chaining or is there a design pattern that would specifically address this problem in a graceful way?

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;
}
}

Related

How to manage multiple ScrollView widgets using one useScrollController() hook?

Flutter documentation for ScrollController has this paragraph:
Scroll controllers are typically stored as member variables in State objects and are reused in each State.build. A single scroll controller can be used to control multiple scrollable widgets, but some operations, such as reading the scroll offset, require the controller to be used with a single scrollable widget.
Does this mean that we cannot pass the same ScrollController to different ScrollView widgets to read ScrollController.offset?
What I'm trying to accomplish is this:
There are two screens. Each screen has a ListView.builder() widget.
Through parameters I pass from screen 1 to screen 2 an object ScrollController and apply it to ListView.
I use scrolling and the offset value changes, but as soon as I move/return to another screen, the offset is knocked down to 0.0 and I see the beginning of the list.
The same ScrollController object is used all the time (hashcode is the same)
How can we use one ScrollController object for different ScrollView widgets, so that the offset is not knocked down when moving from screen to screen?
This problem can be solved a bit if, when switching to another screen, we create a new ScrollController object with initialScrollOffset = oldScrollController.offset and pass it to ScrollView.
Update:
I don't seem to understand how to use flutter_hooks. I created a simple example showing that if we use separate widgets and specify ScrollController as a parameter, the scroll is reset to position 0.0.
Reference for an example:
https://dartpad.dev/?id=d31f4714ce95869716c18b911fee80c1
How do we overcome this?
For now, the best solution I can offer is to pass final ValueNotifier<double> offsetState; instead of final ScrollController controller; as a widget parameter.
Then, in each widget we create a ScrollController. By listening to it via the useListenableSelector hook we change the offsetState.
To avoid unnecessary rebuilding, we use the useValueNotifier hook.
A complete example looks like this:
void main() => runApp(
const MaterialApp(
debugShowCheckedModeBanner: false,
home: MyApp(),
),
);
class MyApp extends HookWidget {
const MyApp();
#override
Widget build(BuildContext context) {
print('#build $MyApp');
final isPrimaries = useState(true);
final offsetState = useValueNotifier(0.0);
return Scaffold(
appBar: AppBar(
title: Text(isPrimaries.value
? 'Colors.primaries List'
: 'Colors.accents List'),
actions: [
IconButton(
onPressed: () => isPrimaries.value = !isPrimaries.value,
icon: const Icon(Icons.ac_unit_sharp),
)
],
),
body: isPrimaries.value
? ListPrimaries(offsetState: offsetState)
: ListAccents(offsetState: offsetState),
);
}
}
class ListAccents extends HookConsumerWidget {
const ListAccents({
Key? key,
required this.offsetState,
}) : super(key: key);
final ValueNotifier<double> offsetState;
#override
Widget build(BuildContext context, WidgetRef ref) {
print('#build $ListAccents');
final controller =
useScrollController(initialScrollOffset: offsetState.value);
useListenableSelector(controller, () {
print(controller.positions);
if (controller.hasClients) {
offsetState.value = controller.offset;
}
return null;
});
return ListView(
primary: false,
controller: controller,
children: Colors.accents
.map((color) => Container(color: color, height: 100))
.toList(),
);
}
}
class ListPrimaries extends HookConsumerWidget {
const ListPrimaries({
Key? key,
required this.offsetState,
}) : super(key: key);
final ValueNotifier<double> offsetState;
#override
Widget build(BuildContext context, WidgetRef ref) {
print('#build $ListPrimaries');
final controller =
useScrollController(initialScrollOffset: offsetState.value);
useListenableSelector(controller, () {
print(controller.positions);
if (controller.hasClients) {
offsetState.value = controller.offset;
}
return null;
});
return ListView(
primary: false,
controller: controller,
children: Colors.primaries
.map((color) => Container(color: color, height: 100))
.toList(),
);
}
}
Another idea was to use useEffect hook and give it a function to save the last value at the moment of dispose():
useEffect(() {
return () {
offsetState.value = controller.offset;
};
}, const []);
But the problem is that at this point, we no longer have clients.
Bonus:
If our task is to synchronize the scroll of the ListView, another useListenableSelector hook added to each of the widgets solves this problem. Remind that we cannot use the same `ScrollController' for two or more lists at the same time.
useListenableSelector(offsetState, () {
if (controller.hasClients) {
// if the contents of the ListView are of different lengths, then do nothing
if (controller.position.maxScrollExtent < offsetState.value) {
return;
}
controller.jumpTo(offsetState.value);
}
});

How to get StatefulWidget's state?

I am new to flutter and the way I get StatefulWidget's state is add a state property to widget, eg:
// ignore: must_be_immutable
class _CustomContainer extends StatefulWidget {
_CustomContainer({Key key}) : super(key: key);
#override
__CustomContainerState createState() {
state = __CustomContainerState();
return state;
}
__CustomContainerState state;
void changeColor() {
if (state == null) return;
// call state's function
this.state.changeColor();
}
}
class __CustomContainerState extends State<_CustomContainer> {
var bgColor = Colors.red;
#override
Widget build(BuildContext context) {
return Container(
width: 200,
height: 200,
color: bgColor,
);
}
void changeColor() {
setState(() {
bgColor = Colors.blue;
});
}
}
usage:
final redContainer = _CustomContainer();
final button = FlatButton(
onPressed: () {
// call widget function
redContainer.changeColor();
},
child: Text('change color'),
);
It works, but I wonder is there any hidden danger?
You'll notice it's very awkward to manipulate Flutter widgets in an imperative fashion, like in the question. This is because of the declarative approach Flutter has taken to building screens.
Declarative vs. Imperative
The approach / philosophy of Flutter UI is a declarative UI vs. an imperative UI.
The example in the question above leans toward an imperative approach.
create an object
object holds state (information)
object exposes method
use method to impose change on object → UI changes
A declarative approach:
there is state (information) above your object
your object is declared (created) from that state
if the state changes...
your object is recreated with the changed state
Below I've tried to convert the imperative approach above, into a declarative one.
CustomContainer is declared with a color; state known / kept outsideCustomContainer & used in its construction.
After construction, you cannot impose a color change on CustomContainer. In an imperative framework you would expose a method, changeColor(color) and call that method and the framework would do magic to show a new color.
In Flutter, to change color of CustomContainer, you declare CustomContainer with a new color.
import 'package:flutter/material.dart';
/// UI object holds no modifiable state.
/// It configures itself once based on a declared color.
/// If color needs to change, pass a new color for rebuild
class CustomContainer extends StatelessWidget {
final Color color;
CustomContainer(this.color);
#override
Widget build(BuildContext context) {
return Container(
color: color,
child: Text('this is a colored Container'),
);
}
}
/// A StatefulWidget / screen to hold state above your UI object
class DeclarativePage extends StatefulWidget {
#override
_DeclarativePageState createState() => _DeclarativePageState();
}
class _DeclarativePageState extends State<DeclarativePage> {
var blue = Colors.blueAccent.withOpacity(.3);
var red = Colors.redAccent.withOpacity(.3);
Color color;
// state (e.g. color) is held in a context above your UI object
#override
void initState() {
super.initState();
color = blue;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Declarative Page'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomContainer(color),
// color "state" ↑ is passed in to build/rebuild your UI object
RaisedButton(
child: Text('Swap Color'),
onPressed: () {
setState(() {
toggleColor();
});
}
)
],
),
),
);
}
void toggleColor() {
color = color == blue ? red : blue;
}
}
Read more on declarative vs imperative on Flutter.dev.
setState() Rebuilds & Performance
Performance-wise it seems wasteful to rebuild the entire screen when a single widget, way down in the widget tree needs rebuilding.
When possible (and sensible) it's better to wrap the particular elements that have state & need rebuilding in a StatefulWidget, rather than wrapping your entire page in a StatefulWidget and rebuilding everything. (Likely, this wouldn't even be a problem, I'll discuss further below.)
Below I've modified the above example, moving the StatefulWidget from being the entire DeclarativePage, to a ChangeWrapper widget.
ChangeWrapper will wrap the CustomContainer (which changes color).
DeclarativePage is now a StatelessWidget and won't be rebuilt when toggling color of CustomContainer.
import 'package:flutter/material.dart';
class ChangeWrapper extends StatefulWidget {
#override
_ChangeWrapperState createState() => _ChangeWrapperState();
}
class _ChangeWrapperState extends State<ChangeWrapper> {
final blue = Colors.blueAccent.withOpacity(.3);
final red = Colors.redAccent.withOpacity(.3);
Color _color; // this is state that changes
#override
void initState() {
super.initState();
_color = blue;
}
#override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomContainer(_color),
RaisedButton(
child: Text('Swap Color'),
onPressed: () {
setState(() {
toggleColor();
});
}
)
],
);
}
void toggleColor() {
_color = _color == blue ? red : blue;
}
}
/// UI object holds no state, it configures itself once based on input (color).
/// If color needs to change, pass a new color for rebuild
class CustomContainer extends StatelessWidget {
final Color color;
CustomContainer(this.color);
#override
Widget build(BuildContext context) {
return Container(
color: color,
child: Text('this is a colored Container'),
);
}
}
class DeclarativePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
print('Declarative Page re/built');
return Scaffold(
appBar: AppBar(
title: Text('Declarative Page'),
),
body: Center(
child: ChangeWrapper(),
),
);
}
}
When running this version of the code, only the ChangeWrapper widget is rebuilt when swapping colors via button press. You can watch the console output for "Declarative Page re/built" which is written to debug console only once upon first screen build/view.
If DeclarativePage was huge with hundreds of widgets, isolating widget rebuilds in the above manner could be significant or useful. On small screens like in the first example at top or even or average screens with a couple dozen widgets, the difference in savings are likely negligible.
Flutter was designed to operate at 60 frames per second. If your screen can build / rebuild all widgets within 16 milliseconds (1000 milliseconds / 60 frames = 16.67 ms per frame), the user will not see any jankiness.
When you use animations, those are designed to run at 60 frames (ticks) per second. i.e. the widgets in your animation will be rebuilt 60 times each second the animation runs.
This is normal Flutter operation for which it was designed & built. So when you're considering whether your widget architecture could be optimized it's useful to think about its context or how that group of widgets will be used. If the widget group isn't in an animation, built once per screen render or once per human button tap... optimization is likely not a big concern.
A large group of widgets within an animation... likely a good candidate to consider optimization & performance.
Btw, this video series is a good overview of the Flutter architecture. I'm guessing Flutter has a bunch of hidden optimizations such as Element re-use when a Widget hasn't materially/substantially changed, in order to save on CPU cycles instantiating, rendering & positioning Element objects.
add setState() method where you want to add state

Change color in rive (flare)

I have a .flr animation of a minion. Is it possible to change colors of his body, pants, eyes, etc dynamicaly and separately in flutter app?
PS Minion is just an example i found on rive.app .There will be another character with lots of different parts.
PPS Maybe there is a better way to make a simple animated character in flutter? For now, i have a stack with positioned colorfilterd images, but i guess it should be easier with rive.
Yes you can. There is an example in the Flare github:
https://github.com/2d-inc/Flare-Flutter/tree/master/example/change_color
import 'package:flare_flutter/flare_controller.dart';
import 'package:flare_flutter/flare.dart';
import 'package:flare_dart/math/mat2d.dart';
import 'package:flare_flutter/flare_actor.dart';
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
List<Color> exampleColors = <Color>[Colors.red, Colors.green, Colors.blue];
class _MyHomePageState extends State<MyHomePage> with FlareController {
FlutterColorFill _fill;
void initialize(FlutterActorArtboard artboard) {
// Find our "Num 2" shape and get its fill so we can change it programmatically.
FlutterActorShape shape = artboard.getNode("Num 2");
_fill = shape?.fill as FlutterColorFill;
}
void setViewTransform(Mat2D viewTransform) {}
bool advance(FlutterActorArtboard artboard, double elapsed) {
// advance is called whenever the flare artboard is about to update (before it draws).
Color nextColor = exampleColors[_counter % exampleColors.length];
if (_fill != null) {
_fill.uiColor = nextColor;
}
// Return false as we don't need to be called again. You'd return true if you wanted to manually animate some property.
return false;
}
// We're going to use the counter to iterate the color.
int _counter = 0;
void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
// advance the controller
isActive.value = true;
});
}
#override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: FlareActor("assets/change_color_example.flr", // You can find the example project here: https://www.2dimensions.com/a/castor/files/flare/change-color-example
fit: BoxFit.contain, alignment: Alignment.center, controller: this),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
If you're using the beta of Rive it's a bit different. I'm not sure if it's the best approach but I'm doing the following:
artboard.forEachComponent((child){
if (child.name == 'Fill') {
Fill fill = child;
fill.paint.color = Colors.red.withOpacity(0.25);
}
else if (child.name == 'Stroke') {
Stroke stroke = child;
stroke.paint.color = Colors.red;
}
});
As Kretin already outlined with the Flutter rive package it is possible even though it's beta as of writing.
To get this to work, you have to know thinks about your animation, for example the name of the shape you want to change (first), and the fill color you want to change.
Let's assume, I have an animation with a shape which name is 'Button1'.
This shape has one fill color that I'd like to change, then my code looks like this:
artboard.forEachComponent((child) {
if (child is Shape && child.name == 'Button1') {
final Shape shape = child;
shape.fills.first.paint.color = Colors.red;
}
});
I have tried the forEachComponent method that the other answers here mention, but it didn't work exactly like I expected. It iterated the components over and over again, spending a lot of process power and also because I was testing merging colors, it ended up merging again and again.
So I ended up checking the properties of the classes to find how to get the colors precisely and changing.
In my code, I'm changing a part of a color of linear gradients. But to get a solid color you can change
shape.fills.first.paintMutator.children.[first/last].color
to
shape.fills.first.paint.color
My code is like this:
final rootBone =
artboard.children.firstWhereOrNull((e) => e.name == 'Root Bone');
final rootBoneChildren = rootBone?.artboard?.drawables;
// Changing hair color
final hair =
rootBoneChildren?.firstWhereOrNull((e) => e.name == 'hair');
if (hair is Shape) {
((hair.fills.first.paintMutator as dynamic)?.children.first
as dynamic)
.color = AppColors.accent;
}
// Changing head color
final head =
rootBoneChildren?.firstWhereOrNull((e) => e.name == 'head');
if (head is Shape) {
final colorParts =
(head.fills.first.paintMutator as dynamic)?.children;
const mergeColor = AppColors.brown;
const timeline = 0.9;
// center
colorParts.first.color =
Color.lerp(colorParts.first.color, mergeColor, timeline);
// border
colorParts.last.color =
Color.lerp(colorParts.last.color, mergeColor, timeline);
}
ps: I'm casting as dynamic because it is a class called LinearGradient, extended from the paintMutator original class, and the class LinearGradient from rive has the same name as LinearGradient from flutter and I didn't want to use aliases everywhere in the code just because of those lines.

Flutter on tap Bounce fixed at one position when scrolled

I am working On Tap Bounce feature on my widget. I have referred to these links for information:
Bouncing Button Flutter
Create on tap bounce on flutter
I got this package named as bouncing_widget, which was good enough for the workaround, but there is one limitation to it, i.e., It is not good for scrolling widget. You cannot scroll the page by tapping on the widget which uses this flutter bouncing widget
Somebody has already created a bug for the above widget, and no solution is found. Here is where you can find the issue: Bouncing Widget GitHub Issue
So, I decided to make my own widget which does the job for me. I have made the widget, and it works fine, but there is a limitation, i.e.,
When you scroll the page by holding on the widget, the widget stays there in one fixed position, that means, like it remains in pressed position
Here is my code:
import 'package:flutter/material.dart';
class CLBounceWidget extends StatefulWidget {
final Function onPressed;
final Widget child;
CLBounceWidget({Key key, #required this.onPressed, #required this.child}): super(key:key);
#override
CLBounceWidgetState createState() => CLBounceWidgetState();
}
class CLBounceWidgetState extends State<CLBounceWidget> with SingleTickerProviderStateMixin{
double _scale;
final Duration duration = Duration(milliseconds: 200);
AnimationController _animationController;
//Getting onPressed Calback
VoidCallback get onPressed => widget.onPressed;
#override
void initState() {
_animationController = AnimationController(
vsync: this,
duration: duration,
lowerBound: 0.0,
upperBound: 0.1
)..addListener((){ setState((){}); });
super.initState();
}
#override
void dispose() {
// TODO: implement dispose
_animationController?.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
_scale = 1 - _animationController.value;
return GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
child: Transform.scale(
scale: _scale,
child: widget.child
)
);
}
// Triggering the onPressed callback with a check
void _onTapTrigger(){
if(onPressed != null){
onPressed();
}
}
//Start the animation
_onTapDown(TapDownDetails details){
_animationController.forward();
}
// We revise the animation and notify the user of the event
_onTapUp(TapUpDetails details){
Future.delayed(Duration(milliseconds: 100), (){
_animationController.reverse();
});
//Finally calling the callback function
_onTapTrigger();
}
}
Also: I have tried doing this, without using Future, but with that, Animation only happens when long pressed, just _onTapTrigger() works:
_onTapUp(TapUpDetails details){
_animationController.reverse();
_onTapTrigger();
}
Tried: I have tried using onLongPress, onLongPressEnd, onLongPressMoveUpdate to at least do the _animationController.reverse();, but nothing worked out for me.
I want the widget to stay normal when I scroll the page while holding the widget, like a normal widget performs.
Since, after a long more researches, and hours and hours of toil, I finally found out what I was willing to have in my project. I have created a package on flutter pub.dev to help other developers in need of what the package flutter_bounce has to offer.
You can search for flutter_bounce on the above link, or you can directly go to this: flutter_bounce
To make use of it, you just need to do this:
Bounce(
duration: Duration(milliseconds: 110),
onPressed: (){ YOUR_FUNCTION },
child: YOUR_WIDGET
)
For more details, please feel free to read the README of the link. It has all necessary details. Happy coding :)
Another new package that you guys can try is flutter_bounceable that I've developed. If you guys prefer a much more simple, interactive and customizable bounce widget, then this package is just for you.
Bounceable(
onTap: yourFunction, // set to null if want to disable bounceable
child: YourWidget(),
duration: const Duration(milliseconds: 200), // optional
reverseDuration: const Duration(milliseconds: 200), // optional
curve: Curves.decelerate, // optional
reverseCurve: Curves.decelerate, // optional
);

flutter hover-like touch handling - detect touch from different widgets without lifting the finger

Assume I have 3 Containers on the screen that react touching by changing their color. When user's finger is on them, they should change color and when touching ends they should turn back to normal. What I want is that those containers to react when the user finger/pointer on them even if the touch started on a random area on the screen, outside of the containers. So basically what i am looking for is something just like css hover.
If i wrap each container with GestureDetector seperately, they will not react to touch events which start outside of them. On another question (unfortunately i dont have the link now) it is suggested to wrap all the containers with one GestureDetector and assign a GlobalKey to each to differ them from each other.
Here is the board that detects touch gestures:
class MyBoard extends StatefulWidget {
static final keys = List<GlobalKey>.generate(3, (ndx) => GlobalKey());
...
}
class _MyBoardState extends State<MyBoard> {
...
/// I control the number of buttons by the number of keys,
/// since all the buttons should have a key
List<Widget> makeButtons() {
return MyBoard.keys.map((key) {
// preapre some funcitonality here
var someFunctionality;
return MyButton(key, someFunctionality);
}).toList();
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (tapDownDetails) {
handleTouch(context, tapDownDetails.globalPosition);
},
onTapUp: (tapUpDetails) {
finishTouch(context);
},
onPanUpdate: (dragUpdateDetails) {
handleTouch(context, dragUpdateDetails.globalPosition);
},
onPanEnd: (panEndDetails) {
finishTouch(context);
},
onPanCancel: () {
finishTouch(context);
},
child: Container(
color: Colors.green,
width: 300.0,
height: 600.0,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: makeButtons(),
),
),
);
}
Here is the simple MyButton:
class MyButton extends StatelessWidget {
final GlobalKey key;
final Function someFunctionality;
MyButton(this.key, this.someFunctionality) : super(key: key);
#override
Widget build(BuildContext context) {
return Consumer<KeyState>(
builder: (buildContext, keyState, child) {
return Container(
width: 100.0,
height: 40.0,
color: keyState.touchedKey == this.key ? Colors.red : Colors.white,
);
},
);
}
}
In the _MyBoardState that's how i handle detecting which MyButton is touched:
MyButton getTouchingButton(Offset globalPosition) {
MyButton currentButton;
// result is outside of [isTouchingMyButtonKey] for performance
final result = BoxHitTestResult();
bool isTouchingButtonKey(GlobalKey key) {
RenderBox renderBox = key.currentContext.findRenderObject();
Offset offset = renderBox.globalToLocal(globalPosition);
return renderBox.hitTest(result, position: offset);
}
var key = MyBoard.keys.firstWhere(isTouchingButtonKey, orElse: () => null);
if (key != null) currentButton = key.currentWidget;
return currentButton;
}
I am using provider package and instead of rebuilding the whole MyBoard with setState only the related part of related MyButton will be rebuilded. Still each button has a listener which rebuilds at every update and I am using GlobalKey. On the flutter docs it says:
Global keys are relatively expensive. If you don't need any of the features listed above, consider using a Key, ValueKey, ObjectKey, or UniqueKey instead.
However if we look at getTouchingButton method I need GlobalKey to get renderBox object and to get currentWidget. I also have to make a hitTest for each GlobalKey. This computation repeats when the onPanUpdate is triggered, which means when the user's finger moves a pixel.
UI is not updating fast enough. With a single tap (tapDown and tapUp in regular speed) you usually do not see any change on the MyButtons.
If I need to sum up my question, How can i detect same single touch (no lifting) from different widgets when finger is hoverin on them in more efficient and elegant way?
Since no one answered yet, I am sharing my own solution which I figured out lately. Now I can see the visual reaction everytime i touch. It is fast enough, but still it feels like there is a little delay on the UI and it feels a little hacky instead of a concrete solution. If someone can give better solution, I can mark it as accepted answer.
My solution:
First things first; since we have to detect Pan gesture and we are using onPanUpdate and onPanEnd, I can erase onTapDown and onTapUp and instead just use onPanStart. This can also detect non-moving simple tap touches.
I also do not use Provider and Consumer anymore, since it rebuilds all the Consumers at every update. This is really a big problem when the the number of MyButtons increase. Instead of keeping MyButton simple and dummy, I moved touch handling work into it. Each MyButton hold the data of if they are touched at the moment.
Updated button is something like this:
class NewButton extends StatefulWidget {
NewButton(Key key) : super(key: key);
#override
NewButtonState createState() => NewButtonState();
}
class NewButtonState extends State<NewButton> {
bool isCurrentlyTouching = false;
void handleTouch(bool isTouching) {
setState(() {
isCurrentlyTouching = isTouching;
});
// some other functionality on touch
}
#override
Widget build(BuildContext context) {
return Container(
width: 100.0,
height: 40.0,
color: isTouched ? Colors.red : Colors.white,
);
}
}
Notice that NewButtonState is not private. We will be using globalKey.currentState.handleTouch(bool) where we detect the touch gesture.