flutter_animate
flutter_animate copied to clipboard
Add support for declarative playback using `end` field
There are certain cases where it would be nice to just set the end
value for a set of tweens, and have the tweens run when that end value changes.
For example:
GridView(
children: imageWidgets.map((i){
bool selected = imageWidgets.indexOf(i) == selectedIndex;
return i.animate().scale(
duration: 200.ms,
curve: Curves.easeOut,
begin: 1,
end: selected? 1.2 : 1);
})
We could then easily create this effect where the selected image scales in, while the previously selected image scales out. The others do nothing.
https://user-images.githubusercontent.com/736973/188086611-fff77915-6be6-4236-9b8e-54700c8533a3.mp4
Currently the easiest way to do this is still with the built in TweenAnimationBuilder
, which is a fine API, but it cant hook into any of Animate
s effects:
imageWidgets.map((i){
bool selected = imageWidgets.indexOf(i) == selectedIndex;
return TweenAnimationBuilder<double>(
tween: Tween(begin: 1, end: selected ? 1.15 : 1),
duration: 200.ms,
curve: Curves.easeOut,
builder: (_, value, child) => Transform.scale(scale: value, child: img),
)
}
If you matched the declarative behavior of TAB, then the old end
would become the new begin
if end
ever changes, which is quite nice for continuous effects (go from A > B > A). Changes to begin
are ignored until the widget state is reloaded.
I think the easiest way to deal with this would be to optionally have the ValueAdapter
animate it's value to a new target.
As a super rough example:
foo.animate(
adapter: ValueAdapter(animated: true, value: selected ? 1 : 0)
).slide().fade()
Conveniently, @thithip has submitted a PR for something very similar (I still need to review it). https://github.com/gskinner/flutter_animate/pull/9
Hm, it's quite a bit of boilerplate (40 chars?), and also wouldn't play nicely with different effects:
TitleText().animate()
.fade(end: _hideTitle? 0 : 1)
.scale(end: _isTitleFocused? 1.2 : 1)
Changing end
and just having the tweens re-run really would be the ticket here, from a usage standpoint. And it would mirror most other implicitly animated widgets AnimatedOpacity
, TweenAnimationBuilder
etc
I want to be careful to keep individual Effect implementations as simple as possible, both to make them easy for devs to write and contribute, but also to keep the maintenance burden reasonable (any additional complexity per effect is multiplied by 20+ effects).
I'm not really sure how you would implement this without some fairly extensive work in individual effects. You can't just look at end
values, because you need to detect if an effect has changed in other ways and should be considered a "new" effect. At minimum, I think you'd need two different comparison hashes for each effect. An equality one to quickly check if an effect is identical, and a "similarity" one, that excludes any values that are allowed to change, but wouldn't cause the effect to be considered a whole new effect.
If the latter equality check was guaranteed to only exclude end
, then Animate
could probably handle diffing and updating the effect chain on its own. But if there was an expectation that other properties could also change in a similar way (ex. color
on TintEffect
) then every effect would also need some ability to generate some kind of untyped delta package to pass into the EffectEntry
, then accept that back out and modify its behavior based on it. In this case, implementing a new basic effect would go from a couple of lines of very simple custom code to 10s of lines of fairly specialized / esoteric code.
Right now, any dev with an idea for an effect can easily build one. If we add a ton of complexity, it narrows it down to only me, or devs that are really dedicated to the task.
Ya I think that is the core design challenge here. How to do it in a way that doesn't put any huge amt of complexity into the effects themselves. I think a small amount of additional complexity is fine, because of the massive benefit this would bring (essentially the whole lib becomes declarative, which opens up myriad of use cases). It just can't get too crazy.
I'm hoping the common behavior of "if end changes, the controller needs to be re-run, and the old end becomes new begin" can be abstracted away without imparting too much load on 90% of the effects, and then we need to look at specific edge cases like Tint etc, where there are values other than end
that might trigger a re-start.
Might even be worth ignoring those edge cases for now, just being able to re-run tweens by changing end is extremely powerful. I'm not sure we need to make it any more complicated than that.
To add another canonical use case to this issue.
We have a view that hides some content if a dialog is open above it. The simplest way to do this currently is with the built-in declarative AnimatedOpacity
widget:
AnimatedOpacity(
opacity: _isMenuOpen ? 0 : 1,
child: ...
Doing this with flutter_animate
is more cludgy, requiring onPlay
callback, a controller field, and some logic to call forward/reverse. In some cases it may not be possible at all to do the effect you want.
As a result of this, we end up switching back and forth between the built-in widgets, and flutter_animate
to drive animations.
It would be better if we could just consolidate all animations to use flutter_animate
by writing:
Animate(
effects: [ FadeEffect( begin: 1, end: _isMenuOpen ? 0 : 1) ]
child: ...
)
Allowing animate to support declarative playback would make views more consistent (one way to do animations, across the entire app), and also not limit us to the presets included in the SDK which are fairly limited / cludgy to use. For example, a fade-and-slide-and-scale effect with SDK widgets is multiple nested builders, which is hard to read and consumes a bunch of precious hz space.
AnimatedOpacity(
opacity: _isMenuOpen ? 0 : 1
child: AnimatedScale(
scale: _isMenuOpen ? 1.2 : 1
child: AnimatedSlide(
offset: Offset(0, _isMenuOpen ? .2 : 0);
child: ...
With animate we could just do:
Animate(
effects: [
FadeEffect( begin: 1, end: _isMenuOpen ? 0 : 1),
SlideEffect( end: Offset(0, _isMenuOpen ? 0.2 : 0)),
ScaleEffect( begin: 1, end: _isMenuOpen ? 1.2 : 1),
]
child: ...
)
imo this ^ is significantly more readable.