diff --git a/example/pubspec.lock b/example/pubspec.lock index bf9c481..8a340e1 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -74,13 +74,6 @@ packages: description: flutter source: sdk version: "0.0.0" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.4" lints: dependency: transitive description: @@ -162,14 +155,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.14" + version: "0.4.12" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.2" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.17.1 <3.0.0" flutter: ">=1.17.0" diff --git a/lib/animate.dart b/lib/animate.dart index d68f6f1..78247db 100644 --- a/lib/animate.dart +++ b/lib/animate.dart @@ -170,9 +170,7 @@ class Animate extends StatefulWidget with AnimateManager { EffectEntry? prior = _lastEntry; Duration delay = (effect is ThenEffect) - ? (effect.delay ?? Duration.zero) + - (prior?.delay ?? Duration.zero) + - (prior?.duration ?? Duration.zero) + ? (effect.delay ?? Duration.zero) + (prior?.delay ?? Duration.zero) + (prior?.duration ?? Duration.zero) : effect.delay ?? prior?.delay ?? Duration.zero; EffectEntry entry = EffectEntry( @@ -205,8 +203,7 @@ class _AnimateState extends State with SingleTickerProviderStateMixin { @override void didUpdateWidget(Animate oldWidget) { - if (oldWidget.controller != widget.controller || - oldWidget._duration != widget._duration) { + if (oldWidget.controller != widget.controller || oldWidget._duration != widget._duration) { _initController(); _play(); } else if (oldWidget.adapter != widget.adapter) { diff --git a/lib/animate_list.dart b/lib/animate_list.dart index 5d81933..c450dbc 100644 --- a/lib/animate_list.dart +++ b/lib/animate_list.dart @@ -29,8 +29,7 @@ import 'flutter_animate.dart'; /// ) /// ) /// ``` -class AnimateList extends ListBase - with AnimateManager { +class AnimateList extends ListBase with AnimateManager { /// Specifies a default interval to use for new `AnimateList` instances. static Duration defaultInterval = Duration.zero; diff --git a/lib/effects/effect.dart b/lib/effect.dart similarity index 71% rename from lib/effects/effect.dart rename to lib/effect.dart index 79576ba..23e7fce 100644 --- a/lib/effects/effect.dart +++ b/lib/effect.dart @@ -1,6 +1,34 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import '../flutter_animate.dart'; +import 'flutter_animate.dart'; + +/// Used to easily create effects that are composed of one or more existing Effects. Provides +/// syntactic sugar for overrideing build and calling the `composeEffects` method +/// with a list of effects. +mixin CompositeEffectMixin on Effect { + List get effects; + + @override + Widget build(BuildContext context, Widget child, AnimationController controller, EffectEntry entry) => + composeEffects(effects, context, child, controller, entry); +} + +/// Adds a begin/end fields and a convenience method for passing them to entry.buildAnimation() +class BeginEndEffect 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 from the current begin/end values. + Animation buildBeginEndAnimation(AnimationController controller, EffectEntry entry) => + entry.buildTweenedAnimation(controller, Tween(begin: begin, end: end)); +} /// Class that defines the required interface and helper methods for /// all effect classes. Look at the various effects for examples of how @@ -9,7 +37,9 @@ import '../flutter_animate.dart'; /// /// It can be instantiated and added to Animate, but has no visual effect. @immutable -class Effect { +class Effect { + const Effect({this.delay, this.duration, this.curve}); + /// The specified delay for the effect. If null, will use the delay from the /// previous effect, or [Duration.zero] if this is the first effect. final Duration? delay; @@ -22,16 +52,6 @@ class Effect { /// previous effect, or [Animate.defaultCurve] if this is the first effect. final Curve? curve; - /// 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; - - const Effect({this.delay, this.duration, this.curve, this.begin, this.end}); - /// Builds the widgets necessary to implement the effect, based on the /// provided [AnimationController] and [EffectEntry]. Widget build( @@ -43,14 +63,19 @@ class Effect { return child; } - /// Returns an animation based on the controller, entry, and begin/end values. - Animation buildAnimation( + /// Calls build on one or more effects, composing them together and returning the resulting widget tree. + @protected + Widget composeEffects( + List effects, + BuildContext context, + Widget child, AnimationController controller, EffectEntry entry, ) { - return entry - .buildAnimation(controller) - .drive(Tween(begin: begin, end: end)); + for (var f in effects) { + child = f.build(context, child, controller, entry); + } + return child; } /// Returns a ratio corresponding to the beginning of the specified entry. @@ -68,8 +93,7 @@ class Effect { /// Check if the animation is currently running / active. bool isAnimationActive(Animation animation) { AnimationStatus status = animation.status; - return status == AnimationStatus.forward || - status == AnimationStatus.reverse; + return status == AnimationStatus.forward || status == AnimationStatus.reverse; } /// Returns an optimized [AnimatedBuilder] that doesn't @@ -117,7 +141,7 @@ extension EffectExtensions on AnimateManager { double? begin, double? end, }) => - addEffect(Effect( + addEffect(BeginEndEffect( delay: delay, duration: duration, curve: curve, diff --git a/lib/effect_entry.dart b/lib/effect_entry.dart new file mode 100644 index 0000000..f4f321c --- /dev/null +++ b/lib/effect_entry.dart @@ -0,0 +1,59 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +export 'animate.dart'; +export 'animate_list.dart'; +export 'adapters/adapters.dart'; +export 'effects/effects.dart'; + +export 'extensions/extensions.dart'; + +/// Because [Effect] classes are immutable and may be reused between multiple +/// [Animate] (or [AnimateList]) instances, an `EffectEntry` is created to store +/// values that may be different between instances. For example, due to +/// `AnimateList interval`, or from inheriting values from prior effects in the chain. +@immutable +class EffectEntry { + const EffectEntry({ + required this.effect, + required this.delay, + required this.duration, + required this.curve, + required this.owner, + }); + + /// The delay for this entry. + final Duration delay; + + /// The duration for this entry. + final Duration duration; + + /// The curve used by this entry. + final Curve curve; + + /// The effect associated with this entry. + final Effect effect; + + /// The [Animate] instance that created this entry. This can be used by effects + /// to read information about the animation. Effects _should not_ modify + /// the animation (ex. by calling [Animate.addEffect]). + final Animate owner; + + /// The begin time for this entry. + Duration get begin => delay; + + /// The end time for this entry. + Duration get end => delay + duration; + + /// Builds a sub-animation that runs from 0-1, based on the properties of this entry. + Animation buildAnimation( + AnimationController controller, { + Curve? curve, + }) { + return buildSubAnimation(controller, begin, end, curve ?? this.curve); + } + + /// Builds an sub-animation that runs from the begin and end values of a given [Tween] + Animation buildTweenedAnimation(AnimationController controller, Tween tween) => + buildAnimation(controller).drive(tween); +} diff --git a/lib/effects/blur_effect.dart b/lib/effects/blur_effect.dart index 1e8c61d..29d08ba 100644 --- a/lib/effects/blur_effect.dart +++ b/lib/effects/blur_effect.dart @@ -6,7 +6,7 @@ import '../flutter_animate.dart'; /// Effect that animates a blur on the target (via [ImageFiltered]) /// between the specified begin and end blur radius values. Defaults to a blur radius of `begin=0, end=4`. @immutable -class BlurEffect extends Effect { +class BlurEffect extends BeginEndEffect { static const Offset neutralValue = Offset(neutralBlur, neutralBlur); static const Offset defaultValue = Offset(defaultBlur, defaultBlur); @@ -35,7 +35,7 @@ class BlurEffect extends Effect { AnimationController controller, EffectEntry entry, ) { - Animation animation = buildAnimation(controller, entry); + Animation animation = buildBeginEndAnimation(controller, entry); return getOptimizedBuilder( animation: animation, builder: (_, __) { diff --git a/lib/effects/callback_effect.dart b/lib/effects/callback_effect.dart index 472c576..bde4b58 100644 --- a/lib/effects/callback_effect.dart +++ b/lib/effects/callback_effect.dart @@ -27,7 +27,7 @@ import '../flutter_animate.dart'; /// an animation that is driven by an [Adapter] (or manipulated via its controller) /// may behave unexpectedly in certain circumstances. @immutable -class CallbackEffect extends Effect { +class CallbackEffect extends Effect { const CallbackEffect({ Duration? delay, Duration? duration, diff --git a/lib/effects/custom_effect.dart b/lib/effects/custom_effect.dart index 68d5544..afe4db4 100644 --- a/lib/effects/custom_effect.dart +++ b/lib/effects/custom_effect.dart @@ -11,9 +11,9 @@ import '../flutter_animate.dart'; /// ``` /// /// Note that the above could also be accomplished by creating a custom effect class -/// that extends [Effect] and utilizes [AnimatedPadding]. +/// that extends [BeginEndEffect] and utilizes [AnimatedPadding]. @immutable -class CustomEffect extends Effect { +class CustomEffect extends BeginEndEffect { const CustomEffect({ required this.builder, Duration? delay, @@ -38,7 +38,7 @@ class CustomEffect extends Effect { AnimationController controller, EffectEntry entry, ) { - Animation animation = buildAnimation(controller, entry); + Animation animation = buildBeginEndAnimation(controller, entry); return getOptimizedBuilder( animation: animation, builder: (ctx, __) => builder(ctx, animation.value, child), diff --git a/lib/effects/effects.dart b/lib/effects/effects.dart index 591148e..11b9464 100644 --- a/lib/effects/effects.dart +++ b/lib/effects/effects.dart @@ -1,6 +1,4 @@ // collects effect classes for easy import. -export 'effect.dart'; - export 'blur_effect.dart'; export 'callback_effect.dart'; export 'custom_effect.dart'; @@ -19,3 +17,7 @@ export 'then_effect.dart'; export 'tint_effect.dart'; export 'toggle_effect.dart'; export 'visibility_effect.dart'; +export 'variations/fade_in.dart'; +export 'variations/fade_out.dart'; +export 'variations/slide_in.dart'; +export 'variations/slide_out.dart'; diff --git a/lib/effects/fade_effect.dart b/lib/effects/fade_effect.dart index 3363236..e8b8853 100644 --- a/lib/effects/fade_effect.dart +++ b/lib/effects/fade_effect.dart @@ -5,7 +5,7 @@ import '../flutter_animate.dart'; /// Effect that animates the opacity of the target (via [FadeTransition]) between the specified begin and end values. /// It defaults to `begin=0, end=1`. @immutable -class FadeEffect extends Effect { +class FadeEffect extends BeginEndEffect { static const double neutralValue = 1.0; static const double defaultValue = 0.0; @@ -31,7 +31,7 @@ class FadeEffect extends Effect { EffectEntry entry, ) { return FadeTransition( - opacity: buildAnimation(controller, entry), + opacity: buildBeginEndAnimation(controller, entry), child: child, ); } @@ -53,36 +53,4 @@ extension FadeEffectExtensions on AnimateManager { begin: begin, end: end, )); - - /// Adds a [fadeIn] extension to [AnimateManager] ([Animate] and [AnimateList]). - /// This is identical to the [fade] extension, except it always uses `end=1.0`. - T fadeIn({ - Duration? delay, - Duration? duration, - Curve? curve, - double? begin, - }) => - addEffect(FadeEffect( - delay: delay, - duration: duration, - curve: curve, - begin: begin ?? FadeEffect.defaultValue, - end: 1.0, - )); - - /// Adds a [fadeOut] extension to [AnimateManager] ([Animate] and [AnimateList]). - /// This is identical to the [fade] extension, except it always uses `end=0.0`. - T fadeOut({ - Duration? delay, - Duration? duration, - Curve? curve, - double? begin, - }) => - addEffect(FadeEffect( - delay: delay, - duration: duration, - curve: curve, - begin: begin ?? FadeEffect.neutralValue, - end: 0.0, - )); } diff --git a/lib/effects/flip_effect.dart b/lib/effects/flip_effect.dart index 78dbe0e..1dd6058 100644 --- a/lib/effects/flip_effect.dart +++ b/lib/effects/flip_effect.dart @@ -22,7 +22,7 @@ import '../flutter_animate.dart'; /// would cause it to rotate around the Y axis — flipping horizontally. /// Default is [Axis.vertical]. @immutable -class FlipEffect extends Effect { +class FlipEffect extends BeginEndEffect { static const double neutralValue = 0.0; static const double defaultValue = -0.5; @@ -60,7 +60,7 @@ class FlipEffect extends Effect { AnimationController controller, EffectEntry entry, ) { - Animation animation = buildAnimation(controller, entry); + Animation animation = buildBeginEndAnimation(controller, entry); return getOptimizedBuilder( animation: animation, builder: (_, __) { diff --git a/lib/effects/listen_effect.dart b/lib/effects/listen_effect.dart index f551796..72a073a 100644 --- a/lib/effects/listen_effect.dart +++ b/lib/effects/listen_effect.dart @@ -27,7 +27,7 @@ import '../flutter_animate.dart'; /// /// See also: [CustomEffect] and [CallbackEffect]. @immutable -class ListenEffect extends Effect { +class ListenEffect extends BeginEndEffect { const ListenEffect({ Duration? delay, Duration? duration, diff --git a/lib/effects/move_effect.dart b/lib/effects/move_effect.dart index fe9bbf2..2c6a3fd 100644 --- a/lib/effects/move_effect.dart +++ b/lib/effects/move_effect.dart @@ -8,7 +8,7 @@ import '../flutter_animate.dart'; /// /// To specify offsets relative to the target's size, use [SlideEffect]. @immutable -class MoveEffect extends Effect { +class MoveEffect extends BeginEndEffect { static const Offset neutralValue = Offset(neutralMove, neutralMove); static const Offset defaultValue = Offset(neutralMove, defaultMove); @@ -41,7 +41,7 @@ class MoveEffect extends Effect { AnimationController controller, EffectEntry entry, ) { - Animation animation = buildAnimation(controller, entry); + Animation animation = buildBeginEndAnimation(controller, entry); return getOptimizedBuilder( animation: animation, builder: (_, __) { diff --git a/lib/effects/rotate_effect.dart b/lib/effects/rotate_effect.dart index 49f371b..b79ec04 100644 --- a/lib/effects/rotate_effect.dart +++ b/lib/effects/rotate_effect.dart @@ -10,7 +10,7 @@ import '../flutter_animate.dart'; /// will occur). For example an alignment of [Alignment.topLeft] would rotate around the top left /// corner of the child. @immutable -class RotateEffect extends Effect { +class RotateEffect extends BeginEndEffect { static const double neutralValue = 0.0; static const double defaultValue = -1.0; @@ -38,7 +38,7 @@ class RotateEffect extends Effect { AnimationController controller, EffectEntry entry, ) { - Animation animation = buildAnimation(controller, entry); + Animation animation = buildBeginEndAnimation(controller, entry); return RotationTransition( turns: animation, alignment: alignment ?? Alignment.center, diff --git a/lib/effects/saturate_effect.dart b/lib/effects/saturate_effect.dart index 3551070..a7fdc11 100644 --- a/lib/effects/saturate_effect.dart +++ b/lib/effects/saturate_effect.dart @@ -13,7 +13,7 @@ import '../flutter_animate.dart'; /// .saturate(duration: 2.seconds) /// ``` @immutable -class SaturateEffect extends Effect { +class SaturateEffect extends BeginEndEffect { static const double neutralValue = 1.0; static const double defaultValue = 0.0; @@ -38,7 +38,7 @@ class SaturateEffect extends Effect { AnimationController controller, EffectEntry entry, ) { - Animation animation = buildAnimation(controller, entry); + Animation animation = buildBeginEndAnimation(controller, entry); return getOptimizedBuilder( animation: animation, builder: (_, __) { diff --git a/lib/effects/scale_effect.dart b/lib/effects/scale_effect.dart index c318516..59d9381 100644 --- a/lib/effects/scale_effect.dart +++ b/lib/effects/scale_effect.dart @@ -5,7 +5,7 @@ import '../flutter_animate.dart'; /// Effect that scales the target (via [ScaleTransition]) between the specified begin and end values. /// Defaults to `begin=0, end=1`. @immutable -class ScaleEffect extends Effect { +class ScaleEffect extends BeginEndEffect { static const Offset neutralValue = Offset(neutralScale, neutralScale); static const Offset defaultValue = Offset(defaultScale, defaultScale); @@ -36,7 +36,7 @@ class ScaleEffect extends Effect { AnimationController controller, EffectEntry entry, ) { - Animation animation = buildAnimation(controller, entry); + Animation animation = buildBeginEndAnimation(controller, entry); return getOptimizedBuilder( animation: animation, builder: (_, __) { @@ -79,8 +79,7 @@ extension ScaleEffectExtensions on AnimateManager { double? end, Alignment? alignment, }) { - begin ??= - (end == null ? ScaleEffect.defaultScale : ScaleEffect.neutralScale); + begin ??= (end == null ? ScaleEffect.defaultScale : ScaleEffect.neutralScale); end ??= ScaleEffect.neutralScale; return addEffect(ScaleEffect( delay: delay, @@ -102,8 +101,7 @@ extension ScaleEffectExtensions on AnimateManager { double? end, Alignment? alignment, }) { - begin ??= - (end == null ? ScaleEffect.defaultScale : ScaleEffect.neutralScale); + begin ??= (end == null ? ScaleEffect.defaultScale : ScaleEffect.neutralScale); end ??= ScaleEffect.neutralScale; return addEffect(ScaleEffect( delay: delay, @@ -125,8 +123,7 @@ extension ScaleEffectExtensions on AnimateManager { double? end, Alignment? alignment, }) { - begin ??= - (end == null ? ScaleEffect.defaultScale : ScaleEffect.neutralScale); + begin ??= (end == null ? ScaleEffect.defaultScale : ScaleEffect.neutralScale); end ??= ScaleEffect.neutralScale; return addEffect(ScaleEffect( delay: delay, diff --git a/lib/effects/shake_effect.dart b/lib/effects/shake_effect.dart index ce8e4ff..e96670d 100644 --- a/lib/effects/shake_effect.dart +++ b/lib/effects/shake_effect.dart @@ -22,7 +22,7 @@ import '../flutter_animate.dart'; /// Text("Hello").animate().shakeX(amount: 10) /// ``` @immutable -class ShakeEffect extends Effect { +class ShakeEffect extends BeginEndEffect { static const int defaultHz = 8; static const double defaultRotation = pi / 36; static const double defaultMove = 5; @@ -60,7 +60,7 @@ class ShakeEffect extends Effect { final bool shouldTranslate = offset != Offset.zero; if (!shouldRotate && !shouldTranslate) return child; - final Animation animation = buildAnimation(controller, entry); + final Animation animation = buildBeginEndAnimation(controller, entry); final int count = (entry.duration.inMilliseconds / 1000 * hz).round(); return getOptimizedBuilder( diff --git a/lib/effects/shimmer_effect.dart b/lib/effects/shimmer_effect.dart index 0d25552..26bc043 100644 --- a/lib/effects/shimmer_effect.dart +++ b/lib/effects/shimmer_effect.dart @@ -22,7 +22,7 @@ import '../flutter_animate.dart'; /// * [BlendMode.srcOver] layers the gradient fill over the child (no masking) /// * [BlendMode.dstOver] layers the gradient fill under the child (no masking) @immutable -class ShimmerEffect extends Effect { +class ShimmerEffect extends BeginEndEffect { static const Color defaultColor = Color(0x80FFFFFF); static const double defaultSize = 1; static const double defaultAngle = pi / 12; @@ -60,7 +60,7 @@ class ShimmerEffect extends Effect { AnimationController controller, EffectEntry entry, ) { - Animation animation = buildAnimation(controller, entry); + Animation animation = buildBeginEndAnimation(controller, entry); return getOptimizedBuilder( animation: animation, builder: (_, __) { diff --git a/lib/effects/slide_effect.dart b/lib/effects/slide_effect.dart index d3c171a..72155b4 100644 --- a/lib/effects/slide_effect.dart +++ b/lib/effects/slide_effect.dart @@ -8,7 +8,7 @@ import '../flutter_animate.dart'; /// /// To use pixel offsets instead, use [MoveEffect]. @immutable -class SlideEffect extends Effect { +class SlideEffect extends BeginEndEffect { static const Offset neutralValue = Offset(neutralSlide, neutralSlide); static const Offset defaultValue = Offset(neutralSlide, defaultSlide); @@ -37,7 +37,7 @@ class SlideEffect extends Effect { EffectEntry entry, ) { return SlideTransition( - position: buildAnimation(controller, entry), + position: buildBeginEndAnimation(controller, entry), child: child, ); } diff --git a/lib/effects/swap_effect.dart b/lib/effects/swap_effect.dart index 8a86afc..4736878 100644 --- a/lib/effects/swap_effect.dart +++ b/lib/effects/swap_effect.dart @@ -36,7 +36,7 @@ import '../flutter_animate.dart'; /// [AnimationController]. So, for example, repeating the first animation (the /// fade out) via its controller will not affect the second animation (fade in). @immutable -class SwapEffect extends Effect { +class SwapEffect extends Effect { const SwapEffect({ Duration? delay, Duration? duration, diff --git a/lib/effects/then_effect.dart b/lib/effects/then_effect.dart index e6ab40f..d9c1433 100644 --- a/lib/effects/then_effect.dart +++ b/lib/effects/then_effect.dart @@ -28,16 +28,14 @@ import '../flutter_animate.dart'; /// subsequent effect. In the example above, it is functionally equivalent to /// setting `delay: 1400.ms` on the blur effect. @immutable -class ThenEffect extends Effect { +class ThenEffect extends Effect { // NOTE: this is just an empty effect, the logic happens in Animate // when it recognizes the type. const ThenEffect({Duration? delay, Duration? duration, Curve? curve}) : super(delay: delay, duration: duration, curve: curve); @override - Widget build(BuildContext context, Widget child, - AnimationController controller, EffectEntry entry) => - child; + Widget build(BuildContext context, Widget child, AnimationController controller, EffectEntry entry) => child; } extension ThenEffectExtensions on AnimateManager { diff --git a/lib/effects/tint_effect.dart b/lib/effects/tint_effect.dart index 200e592..3d03818 100644 --- a/lib/effects/tint_effect.dart +++ b/lib/effects/tint_effect.dart @@ -15,7 +15,7 @@ import '../flutter_animate.dart'; /// .tint(color: Colors.blue, end: 0.5, duration: 2.seconds) /// ``` @immutable -class TintEffect extends Effect { +class TintEffect extends BeginEndEffect { static const double neutralValue = 0.0; static const double defaultValue = 1.0; @@ -44,7 +44,7 @@ class TintEffect extends Effect { AnimationController controller, EffectEntry entry, ) { - Animation animation = buildAnimation(controller, entry); + Animation animation = buildBeginEndAnimation(controller, entry); return getOptimizedBuilder( animation: animation, builder: (_, __) { diff --git a/lib/effects/toggle_effect.dart b/lib/effects/toggle_effect.dart index 079ce49..c305537 100644 --- a/lib/effects/toggle_effect.dart +++ b/lib/effects/toggle_effect.dart @@ -20,7 +20,7 @@ import '../flutter_animate.dart'; /// The child of `Animate` is passed through to the builder in the `child` param /// (possibly already wrapped by prior effects). @immutable -class ToggleEffect extends Effect { +class ToggleEffect extends Effect { const ToggleEffect({ Duration? delay, Duration? duration, diff --git a/lib/effects/variations/blur.dart b/lib/effects/variations/blur.dart new file mode 100644 index 0000000..9df0a50 --- /dev/null +++ b/lib/effects/variations/blur.dart @@ -0,0 +1,34 @@ +// ignore_for_file: overridden_fields + +import 'package:flutter/widgets.dart'; + +import '../../flutter_animate.dart'; + +/* +TODO: + BlurY + BlurXY +*/ +@immutable +class BlurXEffect extends BeginEndEffect with CompositeEffectMixin { + const BlurXEffect({super.begin, super.end, super.delay, super.duration, super.curve}); + + @override + List get effects => [ + BlurEffect( + begin: Offset(begin ?? BlurEffect.neutralBlur, 0), + end: Offset(end ?? (begin == null ? BlurEffect.defaultBlur : BlurEffect.neutralBlur), 0), + ) + ]; +} + +extension BlurEffectExtensions on AnimateManager { + T blurX({ + Duration? delay, + Duration? duration, + Curve? curve, + double? begin, + double? end, + }) => + addEffect(BlurXEffect(delay: delay, duration: duration, curve: curve)); +} diff --git a/lib/effects/variations/fade_in.dart b/lib/effects/variations/fade_in.dart new file mode 100644 index 0000000..bcc031e --- /dev/null +++ b/lib/effects/variations/fade_in.dart @@ -0,0 +1,39 @@ +import 'package:flutter/widgets.dart'; + +import '../../flutter_animate.dart'; + +/* +TODO: + FadeInDown + FadeInLeft + FadeInRight +*/ + +@immutable +class FadeInEffect extends Effect with CompositeEffectMixin { + const FadeInEffect({super.delay, super.duration, super.curve}); + + @override + List get effects => const [FadeEffect(begin: 0, end: 1)]; +} + +@immutable +class FadeInUpEffect extends Effect with CompositeEffectMixin { + const FadeInUpEffect({this.beginY, super.delay, super.duration, super.curve}); + + final double? beginY; + + @override + List get effects => [ + const FadeInEffect(), + SlideInUpEffect(beginY: beginY), + ]; +} + +extension FadeInEffectExtensions on AnimateManager { + T fadeIn({Duration? delay, Duration? duration, Curve? curve}) => + addEffect(FadeInEffect(delay: delay, duration: duration, curve: curve)); + + T fadeInUp({double? beginY, Duration? delay, Duration? duration, Curve? curve}) => + addEffect(FadeInUpEffect(beginY: beginY, delay: delay, duration: duration, curve: curve)); +} diff --git a/lib/effects/variations/fade_out.dart b/lib/effects/variations/fade_out.dart new file mode 100644 index 0000000..2ddd4ba --- /dev/null +++ b/lib/effects/variations/fade_out.dart @@ -0,0 +1,36 @@ +import 'package:flutter/widgets.dart'; + +import '../../flutter_animate.dart'; + +/* +TODO: +FadeOutDown +FadeOutLeft +FadeOutRight +*/ + +@immutable +class FadeOutEffect extends Effect with CompositeEffectMixin { + const FadeOutEffect({super.delay, super.duration, super.curve}); + + @override + List get effects => const [FadeEffect(begin: 1, end: 0)]; +} + +@immutable +class FadeOutUpEffect extends Effect with CompositeEffectMixin { + const FadeOutUpEffect({this.endY, super.delay, super.duration, super.curve}); + + final double? endY; + + @override + List get effects => [const FadeOutEffect(), SlideOutUpEffect(endY: endY)]; +} + +extension FadeOutEffectExtensions on AnimateManager { + T fadeOut({Duration? delay, Duration? duration, Curve? curve}) => + addEffect(FadeOutEffect(delay: delay, duration: duration, curve: curve)); + + T fadeOutUp({double? endY, Duration? delay, Duration? duration, Curve? curve}) => + addEffect(FadeOutUpEffect(endY: endY, delay: delay, duration: duration, curve: curve)); +} diff --git a/lib/effects/variations/slide_in.dart b/lib/effects/variations/slide_in.dart new file mode 100644 index 0000000..a92ad47 --- /dev/null +++ b/lib/effects/variations/slide_in.dart @@ -0,0 +1,27 @@ +import 'package:flutter/widgets.dart'; + +import '../../flutter_animate.dart'; + +/* +TODO: +SlideInDown +SlideInLeft +SlideInRight +*/ + +@immutable +class SlideInUpEffect extends Effect with CompositeEffectMixin { + const SlideInUpEffect({this.beginY, super.delay, super.duration, super.curve}); + static const defaultBeginY = -.2; + final double? beginY; + + @override + List get effects => [ + SlideEffect(begin: Offset(0, beginY ?? defaultBeginY), end: Offset.zero), + ]; +} + +extension SlideInExtensions on AnimateManager { + T slideInUp({double? beginY, Duration? delay, Duration? duration, Curve? curve}) => + addEffect(SlideInUpEffect(beginY: beginY, delay: delay, duration: duration, curve: curve)); +} diff --git a/lib/effects/variations/slide_out.dart b/lib/effects/variations/slide_out.dart new file mode 100644 index 0000000..8ba5a08 --- /dev/null +++ b/lib/effects/variations/slide_out.dart @@ -0,0 +1,26 @@ +import 'package:flutter/widgets.dart'; + +import '../../flutter_animate.dart'; + +/* +TODO: +SlideOutDown +SlideOutLeft +SlideOutRight +*/ +@immutable +class SlideOutUpEffect extends Effect with CompositeEffectMixin { + const SlideOutUpEffect({this.endY, super.delay, super.duration, super.curve}); + static const defaultEndY = .2; + final double? endY; + + @override + List get effects => [ + SlideEffect(begin: Offset.zero, end: Offset(0, endY ?? defaultEndY)), + ]; +} + +extension SlideOutExtensions on AnimateManager { + T slideOutUp({double? endY, Duration? delay, Duration? duration, Curve? curve}) => + addEffect(SlideOutUpEffect(endY: endY, delay: delay, duration: duration, curve: curve)); +} diff --git a/lib/effects/visibility_effect.dart b/lib/effects/visibility_effect.dart index 4070604..023af1f 100644 --- a/lib/effects/visibility_effect.dart +++ b/lib/effects/visibility_effect.dart @@ -8,7 +8,7 @@ import '../flutter_animate.dart'; /// The `maintain` parameter is assigned to the [Visibility] properties `maintainSize`, /// `maintainAnimation`, `maintainState`, `maintainInteractivity` and `maintainSemantics`. @immutable -class VisibilityEffect extends Effect { +class VisibilityEffect extends BeginEndEffect { static const bool neutralValue = true; static const bool defaultMaintain = true; @@ -29,8 +29,7 @@ class VisibilityEffect extends Effect { final bool maintain; @override - Widget build(BuildContext context, Widget child, - AnimationController controller, EffectEntry entry) { + Widget build(BuildContext context, Widget child, AnimationController controller, EffectEntry entry) { double ratio = getEndRatio(controller, entry); return getToggleBuilder( animation: controller, diff --git a/lib/flutter_animate.dart b/lib/flutter_animate.dart index 97ee505..de6ee44 100644 --- a/lib/flutter_animate.dart +++ b/lib/flutter_animate.dart @@ -1,62 +1,16 @@ import 'package:flutter/widgets.dart'; -import 'effects/effects.dart'; -import 'animate.dart'; -import 'animate_list.dart'; +import 'effect.dart'; export 'animate.dart'; export 'animate_list.dart'; export 'adapters/adapters.dart'; export 'effects/effects.dart'; - +export 'effect_entry.dart'; +export 'effect.dart'; +export 'effects/effects.dart'; export 'extensions/extensions.dart'; -/// Because [Effect] classes are immutable and may be reused between multiple -/// [Animate] (or [AnimateList]) instances, an `EffectEntry` is created to store -/// values that may be different between instances. For example, due to -/// `AnimateList interval`, or from inheriting values from prior effects in the chain. -@immutable -class EffectEntry { - const EffectEntry({ - required this.effect, - required this.delay, - required this.duration, - required this.curve, - required this.owner, - }); - - /// The delay for this entry. - final Duration delay; - - /// The duration for this entry. - final Duration duration; - - /// The curve used by this entry. - final Curve curve; - - /// The effect associated with this entry. - final Effect effect; - - /// The [Animate] instance that created this entry. This can be used by effects - /// to read information about the animation. Effects _should not_ modify - /// the animation (ex. by calling [Animate.addEffect]). - final Animate owner; - - /// The begin time for this entry. - Duration get begin => delay; - - /// The end time for this entry. - Duration get end => delay + duration; - - /// Builds a sub-animation based on the properties of this entry. - Animation buildAnimation( - AnimationController controller, { - Curve? curve, - }) { - return buildSubAnimation(controller, begin, end, curve ?? this.curve); - } -} - /// Builds a sub-animation to the provided controller that runs from start to /// end, with the provided curve. For example, it could create an animation that /// runs from 300ms to 800ms with an easeOut curve, within a controller that has a @@ -77,7 +31,7 @@ Animation buildSubAnimation( ); } -/// Provides a common interface for [Animate] and [AnimateList] to attach [Effect] extensions. +/// Provides a common interface for [Animate] and [AnimateList] to attach [BeginEndEffect] extensions. mixin AnimateManager { T addEffect(Effect effect) => throw (UnimplementedError()); T addEffects(List effects) { diff --git a/pubspec.lock b/pubspec.lock index 7708595..0555ac7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -60,13 +60,6 @@ packages: description: flutter source: sdk version: "0.0.0" - js: - dependency: transitive - description: - name: js - url: "https://pub.dartlang.org" - source: hosted - version: "0.6.4" lints: dependency: transitive description: @@ -148,14 +141,14 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.14" + version: "0.4.12" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.2" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.17.0 <3.0.0" flutter: ">=1.17.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9e2a7d4..b61f317 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,7 +5,7 @@ repository: https://github.com/gskinner/flutter_animate issue_tracker: https://github.com/gskinner/flutter_animate/issues environment: - sdk: ">=2.15.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: ">=1.17.0" dependencies: diff --git a/test/effects/variations/fade_test.dart b/test/effects/variations/fade_test.dart new file mode 100644 index 0000000..3b7eca0 --- /dev/null +++ b/test/effects/variations/fade_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +import '../../tester_extensions.dart'; + +void main() { + testWidgets('fade variations', (tester) async { + final fadeIn = const FlutterLogo().animate().fadeIn(duration: 1.seconds); + await tester.pumpAnimation(fadeIn, initialDelay: 750.ms); + tester.expectWidgetWithDouble((ft) => ft.opacity.value, .75, 'opacity'); + + final fadeInUp = const FlutterLogo().animate(key: const ValueKey(1)).fadeInUp(duration: 1.seconds, beginY: -1); + await tester.pumpAnimation(fadeInUp, initialDelay: 750.ms); + tester.expectWidgetWithDouble((ft) => ft.opacity.value, .75, 'opacity'); + tester.expectWidgetWithDouble((ft) => ft.position.value.dy, -.25, 'slide'); + + final fadeOut = const FlutterLogo().animate(key: const ValueKey(2)).fadeOut(duration: 1.seconds); + await tester.pumpAnimation(fadeOut, initialDelay: 750.ms); + tester.expectWidgetWithDouble((ft) => ft.opacity.value, .25, 'opacity'); + + final fadeOutUp = const FlutterLogo().animate(key: const ValueKey(3)).fadeOutUp(duration: 1.seconds, endY: 1); + await tester.pumpAnimation(fadeOutUp, initialDelay: 750.ms); + tester.expectWidgetWithDouble((ft) => ft.opacity.value, .25, 'opacity'); + tester.expectWidgetWithDouble((ft) => ft.position.value.dy, .75, 'slide'); + }); +} diff --git a/test/effects/variations/slide_test.dart b/test/effects/variations/slide_test.dart new file mode 100644 index 0000000..4747084 --- /dev/null +++ b/test/effects/variations/slide_test.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +import '../../tester_extensions.dart'; + +void main() { + testWidgets('slide variations', (tester) async { + final slideInUp = const FlutterLogo().animate().slideInUp(duration: 1.seconds, beginY: -1); + await tester.pumpAnimation(slideInUp, initialDelay: 750.ms); + tester.expectWidgetWithDouble((ft) => ft.position.value.dy, -.25, 'slide'); + + final slideOutUp = const FlutterLogo().animate(key: const ValueKey(1)).slideOutUp(duration: 1.seconds, endY: 1); + await tester.pumpAnimation(slideOutUp, initialDelay: 750.ms); + tester.expectWidgetWithDouble((ft) => ft.position.value.dy, .75, 'slide'); + }); +} diff --git a/test/tester_extensions.dart b/test/tester_extensions.dart index 498e92a..16f6778 100644 --- a/test/tester_extensions.dart +++ b/test/tester_extensions.dart @@ -22,7 +22,7 @@ extension TesterExtensions on WidgetTester { }) { expect( widget(findFirst ? find.byType(T).first : find.byType(T).last), - isA().having((t) => getValue(t), debugTitle, expectedValue), + isA().having((t) => (getValue(t) - expectedValue).abs() < .00000001, debugTitle, true), ); }