diff --git a/example/lib/examples/everything_view.dart b/example/lib/examples/everything_view.dart index 72c9dac..6e2c071 100644 --- a/example/lib/examples/everything_view.dart +++ b/example/lib/examples/everything_view.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'dart:ui' as ui; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -20,8 +21,8 @@ class EverythingView extends StatelessWidget { childAspectRatio: 0.85, children: [ /*** - A few fun / interesting examples - ***/ + A few fun / interesting examples + ***/ tile( 'fade+tint+blur+scale', a @@ -103,9 +104,9 @@ class EverythingView extends StatelessWidget { ), /*** - Catalog of minimal examples for all visual effects. - In alphabetic order of the effect's class name. - ***/ + Catalog of minimal examples for all visual effects. + In alphabetic order of the effect's class name. + ***/ tile('align', a.align()), @@ -175,6 +176,10 @@ class EverythingView extends StatelessWidget { tile('scaleY', a.scaleY()), tile('scaleXY', a.scaleXY()), + tile('sizeX', a.sizeX()), + tile('sizeY', a.sizeY()), + tile('sizeXY', a.size()), + tile('shake', a.shake()), tile('shakeX', a.shakeX()), tile('shakeY', a.shakeY()), diff --git a/lib/src/effects/effects.dart b/lib/src/effects/effects.dart index 3975b88..6b1c102 100644 --- a/lib/src/effects/effects.dart +++ b/lib/src/effects/effects.dart @@ -26,3 +26,4 @@ export 'then_effect.dart'; export 'tint_effect.dart'; export 'toggle_effect.dart'; export 'visibility_effect.dart'; +export 'size_effect.dart'; \ No newline at end of file diff --git a/lib/src/effects/size_effect.dart b/lib/src/effects/size_effect.dart new file mode 100644 index 0000000..04c1695 --- /dev/null +++ b/lib/src/effects/size_effect.dart @@ -0,0 +1,130 @@ +import 'dart:math' as math; +import 'package:flutter/widgets.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +/// An effect that adjust the size of target between the specified [begin] and [end] +/// offset values. unlike [ScaleEffect], this effect does affect the real size +/// of the widget. +/// Defaults to `begin=0.0, end=1.0`. +@immutable +class SizeEffect extends Effect { + static const double neutralValue = 1.0; + static const double defaultValue = 0.0; + static const double defaultAxisAlignment = 0.0; + + const SizeEffect({ + super.delay, + super.duration, + super.curve, + double? begin, + double? end, + this.fixedWidthFactor, + this.fixedHeightFactor, + this.alignment, + }) : super( + begin: begin ?? (end == null ? defaultValue : neutralValue), + end: end ?? neutralValue, + ); + + final AlignmentGeometry? alignment; + final double? fixedWidthFactor; + final double? fixedHeightFactor; + + @override + Widget build( + BuildContext context, + Widget child, + AnimationController controller, + EffectEntry entry, + ) { + final animation = buildAnimation(controller, entry); + return getOptimizedBuilder( + animation: animation, + builder: (_, __) { + return ClipRect( + child: Align( + alignment: alignment ?? Alignment.center, + widthFactor: fixedWidthFactor ?? math.max(animation.value, 0.0), + heightFactor: fixedHeightFactor ?? math.max(animation.value, 0.0), + child: child, + ), + ); + }, + ); + } +} + +/// Adds [SizeEffect] related extensions to [AnimateManager]. +extension SizeEffectExtensions> on T { + /// Adds a [SizeEffect] that adjust the size of target between + /// the specified [begin] and [end] offset values. + /// + T size({ + Duration? delay, + Duration? duration, + Curve? curve, + double? begin, + double? end, + double? fixedWidthFactor, + double? fixedHeightFactor, + AlignmentGeometry? alignment, + }) => + addEffect(SizeEffect( + delay: delay, + duration: duration, + curve: curve, + begin: begin, + end: end, + alignment: alignment, + fixedWidthFactor: fixedWidthFactor, + fixedHeightFactor: fixedHeightFactor, + )); + + /// Adds a [SizeEffect] that adjust the size of target horizontally between + /// the specified [begin] and [end] values. + /// + /// [axisAlignment] describes how to align the child along the horizontal axis + /// that [sizeFactor] is modifying. + T sizeX({ + Duration? delay, + Duration? duration, + Curve? curve, + double? begin, + double? end, + double? axisAlignment, + }) => + addEffect(SizeEffect( + delay: delay, + duration: duration, + curve: curve, + begin: begin, + end: end, + fixedHeightFactor: 1.0, + alignment: AlignmentDirectional( + axisAlignment ?? SizeEffect.defaultAxisAlignment, -1.0))); + + /// Adds a [SizeEffect] that adjust the size of target vertically between + /// the specified [begin] and [end] values. + /// + /// [axisAlignment] describes how to align the child along the vertical axis + /// that [sizeFactor] is modifying. + T sizeY({ + Duration? delay, + Duration? duration, + Curve? curve, + double? begin, + double? end, + double? axisAlignment, + }) => + addEffect(SizeEffect( + delay: delay, + duration: duration, + curve: curve, + begin: begin, + end: end, + fixedWidthFactor: 1.0, + alignment: AlignmentDirectional( + -1.0, + axisAlignment ?? SizeEffect.defaultAxisAlignment, + ))); +} diff --git a/test/effects/size_test.dart b/test/effects/size_test.dart new file mode 100644 index 0000000..554572a --- /dev/null +++ b/test/effects/size_test.dart @@ -0,0 +1,61 @@ +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('SizeEffect: size', (tester) async { + final animation = const FlutterLogo().animate().size( + duration: 1000.ms, + begin: 1.0, + end: 0.0, + ); + + await tester.pumpAnimation(MaterialApp(home: animation), + initialDelay: 500.ms); + _verifySize(tester, 0.5, 0.5); + }); + + testWidgets('SizeEffect: size', (tester) async { + final animation = + const FlutterLogo().animate().size(duration: 1000.ms, end: 2); + + // Check halfway, + await tester.pumpAnimation(MaterialApp(home: animation), + initialDelay: 500.ms); + _verifySize(tester, 1.5, 1.5); + }); + + testWidgets('SizeEffect: sizeX', (tester) async { + final animation = + const FlutterLogo().animate().sizeX(duration: 1000.ms, end: 2); + + // Check halfway, + await tester.pumpAnimation(MaterialApp(home: animation), + initialDelay: 500.ms); + _verifySize(tester, 1.5, 1); + }); + + testWidgets('SizeEffect: sizeY', (tester) async { + final animation = + const FlutterLogo().animate().sizeY(duration: 1000.ms, end: 2); + + // Check halfway, + await tester.pumpAnimation(MaterialApp(home: animation), + initialDelay: 500.ms); + _verifySize(tester, 1, 1.5); + }); +} + +_verifySize( + WidgetTester tester, double widthFactor, double heightFactor) async { + tester.widget(find.byType(Align)); + expect( + tester.widget(find.byType(Align)), + isA() + .having( + (Align align) => align.widthFactor, 'widthFactor', widthFactor) + .having((Align align) => align.heightFactor, 'heightFactor', + heightFactor)); +}