Enable declarative effect variations
What this does
Initial pass at a solution for #31 and beginnings of #34.
It sets a core architecture that supports both declarative syntax [ FadeInEffect() ] and imperitive animate().fadeIn() when creating effect variations. The current solution for variations supports only the imperative syntax.
It also adds some plumbing to more easily make variations of other effects. Whether this is composing two or more existing effect together (FadeEffect + SlideEffect), or simply creating a variation of an existing one (FadeIn or BlurX).
NOTE: While this touches many files, that is mostly due to a rename. The bulk of the interesting changes are in the effect.dart class and the new variations themselves.
Architecture
Effects do not always want to expose a public begin/end value.
- Some may have multiple begin/ends, of different types, for example a
FadeInAndUpmay haveOffset beginOffsetanddouble beginOpacity - Some may not semantically make sense at all like
ToggleorSwap.
Being forced to inherit begin/end fields in these cases is confusing/undesired, so (imo) we need to make this optional.
To allow this, a new BeginEndEffect<T> class was created, which extends Effect, and the begin/end fields were moved there.
class BeginEndEffect<T> extends Effect {
const BeginEndEffect({super.delay, super.duration, super.curve, this.begin, this.end});
/// The begin value for the effect. If null, effects should use a reasonable
/// default value when appropriate.
final T? begin;
/// The end value for the effect. If null, effects should use a reasonable
/// default value when appropriate.
final T? end;
/// Helper method for concrete effects to easily create an Animation<T> from the current begin/end values.
Animation<T> buildBeginEndAnimation(AnimationController controller, EffectEntry entry) =>
entry.buildTweenedAnimation(controller, Tween(begin: begin, end: end));
}
Most of the existing 'core' effects were modified to extend BeginEndEffect, the exceptions were Then, Toggle, Callback and Swap where we were able to extend Effect directly and remove the weird Effect<double> and Effect<void> stuff that was being forced. There are also no longer phantom begin/end properties on these effects which is nice.
Finally a CompositeEffectMixin was created, to make it easier to create a new effect from existing ones. The mixin is just a small bit of syntactic sugar, that requires a list of effects, overrides build, and auto-builds all the effects. This allows variations to be created with virtually no boilerplate.
mixin CompositeEffectMixin on Effect {
// A list of Effects must be provided by any concrete instances of this mixin
List<Effect> get effects;
@override
/// override build, and call composeEffects(...) so the concrete instances don't have to
Widget build(BuildContext context, Widget child, AnimationController controller, EffectEntry entry) =>
composeEffects(effects, context, child, controller, entry);
}
Importantly, a new effect can both extend BeginEndEffect<T> and use the CompositeEffectMixin if needed so it's quite flexible and easy to use in different configurations.
Example 1,
FadeInEffect, does not want to expose public begin/end values, so it only extends Effect. It uses CompositeEffectMixin to keep boilerplate to a minimum:
class FadeInEffect extends Effect with CompositeEffectMixin {
const FadeInEffect({super.delay, super.duration, super.curve});
@override
List<Effect> get effects => const [FadeEffect(begin: 0, end: 1)];
}
Example 2,
BlurX does want a begin and end, so it extends BeginEndEffect and uses CompositeEffectMixin. This shows how a variation can change the type of its core effect, BlurEffect takes an Offset, but this takes a double, straight inheritence would struggle here but the compositional approach has no problems:
class BlurXEffect extends BeginEndEffect<double> with CompositeEffectMixin {
const BlurXEffect({super.begin, super.end, super.delay, super.duration, super.curve});
@override
List<Effect> get effects => [
BlurEffect(
begin: Offset(begin ?? BlurEffect.neutralBlur, 0),
end: Offset(end ?? (begin == null ? BlurEffect.defaultBlur : BlurEffect.neutralBlur), 0),
)
];
}
Example 3,
FadeInUp composes FadeIn and SlideInUp effects, which themselves are composed of Fade and Slide, showing advanced multi-level composition. It does not use extends BeginEndEffect, because it would be unclear what properties they refer to. Instead it declares it's own beginY for clarity:
class FadeInUpEffect extends Effect with CompositeEffectMixin {
const FadeInUpEffect({this.beginY, super.delay, super.duration, super.curve});
final double? beginY;
@override
List<Effect> get effects => [
const FadeInEffect(),
SlideInUpEffect(beginY: beginY),
];
}
NOTE: In this example its been decided that beginY would make sense but the other values do not (beginOpacity, endOpacity and endY). This is debatable, but also not relevant to the example.
What is left?
- Agree this is a good approach to move forward with
- Implement remaining low level variations that already exist in the lib
- FlipH/V
- MoveX/Y
- Desaturate
- ScaleX/Y/XY
- ShakeX/Y
- SlideX/Y
- Untint
- Show/Hide
- Add some higher level variations, described in issue #34 (
FadeInDown,SlideRight, etc)