From 1f1d7edaeac85ba8074fbffe051526031ee030bd Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sat, 5 Oct 2024 15:45:01 +0530 Subject: [PATCH 1/3] Swipe to an angle Allow swiping to custom direction specified by an angle --- lib/flutter_card_swiper.dart | 1 + lib/src/card_animation.dart | 109 +++++++++++++--- .../controller/card_swiper_controller.dart | 2 +- lib/src/direction/card_swiper_direction.dart | 116 ++++++++++++++++++ lib/src/enums.dart | 2 - lib/src/typedefs.dart | 2 +- lib/src/utils/direction_extension.dart | 26 ++-- lib/src/widget/card_swiper.dart | 5 +- test/utils/direction_extension_test.dart | 2 +- 9 files changed, 234 insertions(+), 31 deletions(-) create mode 100644 lib/src/direction/card_swiper_direction.dart diff --git a/lib/flutter_card_swiper.dart b/lib/flutter_card_swiper.dart index 57c3f02..bf94004 100644 --- a/lib/flutter_card_swiper.dart +++ b/lib/flutter_card_swiper.dart @@ -4,6 +4,7 @@ library flutter_card_swiper; export 'package:flutter_card_swiper/src/controller/card_swiper_controller.dart'; +export 'package:flutter_card_swiper/src/direction/card_swiper_direction.dart'; export 'package:flutter_card_swiper/src/enums.dart'; export 'package:flutter_card_swiper/src/properties/allowed_swipe_direction.dart'; export 'package:flutter_card_swiper/src/typedefs.dart'; diff --git a/lib/src/card_animation.dart b/lib/src/card_animation.dart index eba4cf4..bb225cf 100644 --- a/lib/src/card_animation.dart +++ b/lib/src/card_animation.dart @@ -136,13 +136,55 @@ class CardAnimation { } void animate(BuildContext context, CardSwiperDirection direction) { - return switch (direction) { - CardSwiperDirection.left => animateHorizontally(context, false), - CardSwiperDirection.right => animateHorizontally(context, true), - CardSwiperDirection.top => animateVertically(context, false), - CardSwiperDirection.bottom => animateVertically(context, true), - CardSwiperDirection.none => null, - }; + if (direction == CardSwiperDirection.none) { + return; + } + if (direction.isCloseTo(CardSwiperDirection.left)) { + animateHorizontally(context, false); + } else if (direction.isCloseTo(CardSwiperDirection.right)) { + animateHorizontally(context, true); + } else if (direction.isCloseTo(CardSwiperDirection.top)) { + animateVertically(context, false); + } else if (direction.isCloseTo(CardSwiperDirection.bottom)) { + animateVertically(context, true); + } else { + // Custom angle animation + animateToAngle(context, direction.angle); + } + } + + void animateToAngle(BuildContext context, double targetAngle) { + final size = MediaQuery.of(context).size; + + // Convert the angle to radians + final adjustedAngle = (targetAngle - 90) * (math.pi / 180); + + // Calculate the target position based on the angle + final magnitude = size.width; // Use screen width as base magnitude + final targetX = magnitude * math.cos(adjustedAngle); + final targetY = magnitude * math.sin(adjustedAngle); + + _leftAnimation = Tween( + begin: left, + end: targetX, + ).animate(animationController); + + _topAnimation = Tween( + begin: top, + end: targetY, + ).animate(animationController); + + _scaleAnimation = Tween( + begin: scale, + end: 1.0, + ).animate(animationController); + + _differenceAnimation = Tween( + begin: difference, + end: initialOffset, + ).animate(animationController); + + animationController.forward(); } void animateHorizontally(BuildContext context, bool isToRight) { @@ -210,13 +252,20 @@ class CardAnimation { } void animateUndo(BuildContext context, CardSwiperDirection direction) { - return switch (direction) { - CardSwiperDirection.left => animateUndoHorizontally(context, false), - CardSwiperDirection.right => animateUndoHorizontally(context, true), - CardSwiperDirection.top => animateUndoVertically(context, false), - CardSwiperDirection.bottom => animateUndoVertically(context, true), - _ => null - }; + if (direction == CardSwiperDirection.none) { + return; + } + if (direction.isCloseTo(CardSwiperDirection.left)) { + animateUndoHorizontally(context, false); + } else if (direction.isCloseTo(CardSwiperDirection.right)) { + animateUndoHorizontally(context, true); + } else if (direction.isCloseTo(CardSwiperDirection.top)) { + animateUndoVertically(context, true); + } else if (direction.isCloseTo(CardSwiperDirection.bottom)) { + animateUndoVertically(context, false); + } else { + animateUndoFromAngle(context, direction.angle); + } } void animateUndoHorizontally(BuildContext context, bool isToRight) { @@ -262,4 +311,36 @@ class CardAnimation { ).animate(animationController); animationController.forward(); } + + void animateUndoFromAngle(BuildContext context, double angle) { + final size = MediaQuery.of(context).size; + + final adjustedAngle = (angle - 90) * (math.pi / 180); + + final magnitude = size.width; + final startX = magnitude * math.cos(adjustedAngle); + final startY = magnitude * math.sin(adjustedAngle); + + _leftAnimation = Tween( + begin: startX, + end: 0, + ).animate(animationController); + + _topAnimation = Tween( + begin: startY, + end: 0, + ).animate(animationController); + + _scaleAnimation = Tween( + begin: 1.0, + end: scale, + ).animate(animationController); + + _differenceAnimation = Tween( + begin: initialOffset, + end: difference, + ).animate(animationController); + + animationController.forward(); + } } diff --git a/lib/src/controller/card_swiper_controller.dart b/lib/src/controller/card_swiper_controller.dart index 4aaac7f..04f5619 100644 --- a/lib/src/controller/card_swiper_controller.dart +++ b/lib/src/controller/card_swiper_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; +import 'package:flutter_card_swiper/flutter_card_swiper.dart'; import 'package:flutter_card_swiper/src/controller/controller_event.dart'; -import 'package:flutter_card_swiper/src/enums.dart'; /// A controller that can be used to trigger swipes on a CardSwiper widget. class CardSwiperController { diff --git a/lib/src/direction/card_swiper_direction.dart b/lib/src/direction/card_swiper_direction.dart new file mode 100644 index 0000000..ddf7444 --- /dev/null +++ b/lib/src/direction/card_swiper_direction.dart @@ -0,0 +1,116 @@ +/// Represents the direction of a card swipe using an angle. +/// +/// The direction is represented by an angle in degrees, following a clockwise rotation: +/// * 0° points to the top +/// * 90° points to the right +/// * 180° points to the bottom +/// * 270° points to the left +/// +/// The class provides standard cardinal directions as static constants: +/// ```dart +/// CardSwiperDirection.top // 0° +/// CardSwiperDirection.right // 90° +/// CardSwiperDirection.bottom // 180° +/// CardSwiperDirection.left // 270° +/// ``` +/// +/// Custom angles can be created using [CardSwiperDirection.custom]: +/// ```dart +/// final diagonal = CardSwiperDirection.custom(45); // Creates a top-right direction +/// ``` +/// +/// All angles are normalized to be within the range [0, 360) degrees. When comparing +/// directions, a tolerance of 5 degrees is used by default to account for small variations +/// in swipe gestures. +/// +/// The direction also maintains a human-readable name, which is automatically generated +/// based on the angle's quadrant (e.g., 'top-right', 'right-bottom') or can be +/// manually specified when creating a custom direction. +class CardSwiperDirection { + /// The angle in degrees representing the direction of the swipe + final double angle; + + /// The name of the direction. + /// + /// This is not used in any operations - can be considered as a debug info if you may. + final String name; + + /// Creates a new [CardSwiperDirection] with the specified angle in degrees + const CardSwiperDirection._({ + required this.angle, + required this.name, + }); + + /// No movement direction (Infinity) + static const none = CardSwiperDirection._( + angle: double.infinity, + name: 'none', + ); + + /// Swipe to the top (0 degrees) + static const top = CardSwiperDirection._(angle: 0, name: 'top'); + + /// Swipe to the right (90 degrees) + static const right = CardSwiperDirection._(angle: 90, name: 'right'); + + /// Swipe to the bottom (180 degrees) + static const bottom = CardSwiperDirection._(angle: 180, name: 'bottom'); + + /// Swipe to the left (270 degrees) + static const left = CardSwiperDirection._(angle: 270, name: 'left'); + + /// Creates a custom swipe direction with the specified angle in degrees + factory CardSwiperDirection.custom(double angle, {String? name}) { + // Normalize angle to be between 0 and 360 degrees + final normalizedAngle = (angle % 360 + 360) % 360; + // Generate a name if not provided + final directionName = name ?? _getDirectionName(normalizedAngle); + return CardSwiperDirection._( + angle: normalizedAngle, + name: directionName, + ); + } + + /// Generate a direction name based on the angle + static String _getDirectionName(double angle) { + if (angle == 0) return 'top'; + if (angle == 90) return 'right'; + if (angle == 180) return 'bottom'; + if (angle == 270) return 'left'; + + // For custom angles, generate a name based on the quadrant + if (angle > 0 && angle < 90) return 'top-right'; + if (angle > 90 && angle < 180) return 'right-bottom'; + if (angle > 180 && angle < 270) return 'bottom-left'; + return 'left-top'; + } + + /// Checks if this direction is approximately equal to another direction + /// within a certain tolerance (default is 5 degrees) + bool isCloseTo(CardSwiperDirection other, {double tolerance = 5}) { + final diff = (angle - other.angle).abs(); + return diff <= tolerance || (360 - diff) <= tolerance; + } + + /// Returns true if the direction is horizontal (left or right) + bool get isHorizontal => isCloseTo(right) || isCloseTo(left); + + /// Returns true if the direction is vertical (top or bottom) + bool get isVertical => isCloseTo(top) || isCloseTo(bottom); + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + bool operator ==(Object other) { + if (identical(this, other)) return true; + return other is CardSwiperDirection && + other.angle == angle && + other.name == name; + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes + int get hashCode => Object.hash(angle, name); + + @override + String toString() => 'CardSwiperDirection($name: $angle°)'; +} diff --git a/lib/src/enums.dart b/lib/src/enums.dart index 3430fec..69c8adf 100644 --- a/lib/src/enums.dart +++ b/lib/src/enums.dart @@ -1,3 +1 @@ -enum CardSwiperDirection { none, left, right, top, bottom } - enum SwipeType { none, swipe, back, undo } diff --git a/lib/src/typedefs.dart b/lib/src/typedefs.dart index d19549a..d7c19d2 100644 --- a/lib/src/typedefs.dart +++ b/lib/src/typedefs.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; -import 'package:flutter_card_swiper/src/enums.dart'; +import 'package:flutter_card_swiper/src/direction/card_swiper_direction.dart'; typedef CardSwiperOnSwipe = FutureOr Function( int previousIndex, diff --git a/lib/src/utils/direction_extension.dart b/lib/src/utils/direction_extension.dart index daa61e0..50b926e 100644 --- a/lib/src/utils/direction_extension.dart +++ b/lib/src/utils/direction_extension.dart @@ -1,12 +1,22 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_card_swiper/src/enums.dart'; +import 'package:flutter_card_swiper/flutter_card_swiper.dart'; extension DirectionExtension on CardSwiperDirection { - Axis get axis => switch (this) { - CardSwiperDirection.left || - CardSwiperDirection.right => - Axis.horizontal, - CardSwiperDirection.top || CardSwiperDirection.bottom => Axis.vertical, - CardSwiperDirection.none => throw Exception('Direction is none'), - }; + Axis get axis { + if (this == CardSwiperDirection.left || this == CardSwiperDirection.right) { + return Axis.horizontal; + } else if (this == CardSwiperDirection.top || + this == CardSwiperDirection.bottom) { + return Axis.vertical; + } else if (this == CardSwiperDirection.none) { + throw Exception('Direction is none'); + } else { + // Handle custom angles: if the angle is closer to horizontal or vertical + if ((angle >= 45 && angle <= 135) || (angle >= 225 && angle <= 315)) { + return Axis.vertical; // Top/Bottom-ish + } else { + return Axis.horizontal; // Left/Right-ish + } + } + } } diff --git a/lib/src/widget/card_swiper.dart b/lib/src/widget/card_swiper.dart index 5d129ff..41b498e 100644 --- a/lib/src/widget/card_swiper.dart +++ b/lib/src/widget/card_swiper.dart @@ -2,12 +2,9 @@ import 'dart:collection'; import 'dart:math' as math; import 'package:flutter/widgets.dart'; +import 'package:flutter_card_swiper/flutter_card_swiper.dart'; import 'package:flutter_card_swiper/src/card_animation.dart'; -import 'package:flutter_card_swiper/src/controller/card_swiper_controller.dart'; import 'package:flutter_card_swiper/src/controller/controller_event.dart'; -import 'package:flutter_card_swiper/src/enums.dart'; -import 'package:flutter_card_swiper/src/properties/allowed_swipe_direction.dart'; -import 'package:flutter_card_swiper/src/typedefs.dart'; import 'package:flutter_card_swiper/src/utils/number_extension.dart'; import 'package:flutter_card_swiper/src/utils/undoable.dart'; diff --git a/test/utils/direction_extension_test.dart b/test/utils/direction_extension_test.dart index 1a0746a..b1f17d8 100644 --- a/test/utils/direction_extension_test.dart +++ b/test/utils/direction_extension_test.dart @@ -1,5 +1,5 @@ import 'package:flutter/widgets.dart'; -import 'package:flutter_card_swiper/src/enums.dart'; +import 'package:flutter_card_swiper/flutter_card_swiper.dart'; import 'package:flutter_card_swiper/src/utils/direction_extension.dart'; import 'package:flutter_test/flutter_test.dart'; From e14aff68805de46555f2289592c0d00d4285485c Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sat, 5 Oct 2024 15:52:40 +0530 Subject: [PATCH 2/3] refactor: remove unnecessary methods in CardAnimation --- lib/src/card_animation.dart | 121 ++---------------------------------- 1 file changed, 4 insertions(+), 117 deletions(-) diff --git a/lib/src/card_animation.dart b/lib/src/card_animation.dart index bb225cf..0cb7336 100644 --- a/lib/src/card_animation.dart +++ b/lib/src/card_animation.dart @@ -136,21 +136,8 @@ class CardAnimation { } void animate(BuildContext context, CardSwiperDirection direction) { - if (direction == CardSwiperDirection.none) { - return; - } - if (direction.isCloseTo(CardSwiperDirection.left)) { - animateHorizontally(context, false); - } else if (direction.isCloseTo(CardSwiperDirection.right)) { - animateHorizontally(context, true); - } else if (direction.isCloseTo(CardSwiperDirection.top)) { - animateVertically(context, false); - } else if (direction.isCloseTo(CardSwiperDirection.bottom)) { - animateVertically(context, true); - } else { - // Custom angle animation - animateToAngle(context, direction.angle); - } + if (direction == CardSwiperDirection.none) return; + animateToAngle(context, direction.angle); } void animateToAngle(BuildContext context, double targetAngle) { @@ -187,50 +174,6 @@ class CardAnimation { animationController.forward(); } - void animateHorizontally(BuildContext context, bool isToRight) { - final screenWidth = MediaQuery.of(context).size.width; - - _leftAnimation = Tween( - begin: left, - end: isToRight ? screenWidth : -screenWidth, - ).animate(animationController); - _topAnimation = Tween( - begin: top, - end: top + top, - ).animate(animationController); - _scaleAnimation = Tween( - begin: scale, - end: 1.0, - ).animate(animationController); - _differenceAnimation = Tween( - begin: difference, - end: initialOffset, - ).animate(animationController); - animationController.forward(); - } - - void animateVertically(BuildContext context, bool isToBottom) { - final screenHeight = MediaQuery.of(context).size.height; - - _leftAnimation = Tween( - begin: left, - end: left + left, - ).animate(animationController); - _topAnimation = Tween( - begin: top, - end: isToBottom ? screenHeight : -screenHeight, - ).animate(animationController); - _scaleAnimation = Tween( - begin: scale, - end: 1.0, - ).animate(animationController); - _differenceAnimation = Tween( - begin: difference, - end: initialOffset, - ).animate(animationController); - animationController.forward(); - } - void animateBack(BuildContext context) { _leftAnimation = Tween( begin: left, @@ -252,64 +195,8 @@ class CardAnimation { } void animateUndo(BuildContext context, CardSwiperDirection direction) { - if (direction == CardSwiperDirection.none) { - return; - } - if (direction.isCloseTo(CardSwiperDirection.left)) { - animateUndoHorizontally(context, false); - } else if (direction.isCloseTo(CardSwiperDirection.right)) { - animateUndoHorizontally(context, true); - } else if (direction.isCloseTo(CardSwiperDirection.top)) { - animateUndoVertically(context, true); - } else if (direction.isCloseTo(CardSwiperDirection.bottom)) { - animateUndoVertically(context, false); - } else { - animateUndoFromAngle(context, direction.angle); - } - } - - void animateUndoHorizontally(BuildContext context, bool isToRight) { - final size = MediaQuery.of(context).size; - - _leftAnimation = Tween( - begin: isToRight ? size.width : -size.width, - end: 0, - ).animate(animationController); - _topAnimation = Tween( - begin: top, - end: top + top, - ).animate(animationController); - _scaleAnimation = Tween( - begin: 1.0, - end: scale, - ).animate(animationController); - _differenceAnimation = Tween( - begin: initialOffset, - end: difference, - ).animate(animationController); - animationController.forward(); - } - - void animateUndoVertically(BuildContext context, bool isToBottom) { - final size = MediaQuery.of(context).size; - - _leftAnimation = Tween( - begin: left, - end: left + left, - ).animate(animationController); - _topAnimation = Tween( - begin: isToBottom ? -size.height : size.height, - end: 0, - ).animate(animationController); - _scaleAnimation = Tween( - begin: 1.0, - end: scale, - ).animate(animationController); - _differenceAnimation = Tween( - begin: initialOffset, - end: difference, - ).animate(animationController); - animationController.forward(); + if (direction == CardSwiperDirection.none) return; + animateUndoFromAngle(context, direction.angle); } void animateUndoFromAngle(BuildContext context, double angle) { From a1c9f198b1761faa62b6947968c30e24d27e7a2b Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sat, 5 Oct 2024 16:18:11 +0530 Subject: [PATCH 3/3] version: v7.0.2 --- CHANGELOG.md | 7 +++++++ pubspec.yaml | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93803fd..686b1f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [7.0.2] + +- Added `CardAnimation.animateToAngle` helper to animate swipe the card to any given angle between 0-360°. +- Added `CardAnimation.animateUndoFromAngle` helper method to undo animation from any angle. +- Remoed previous implementations for the above in the `CardAnimation` class, namely - `animateHorizontally`, `animateVertically`, `animateUndoHorizontally`, and `animateUndoVertically` +- Replaced `enum CardSwiperDirection` with `class CardSwiperDirection` to support custom angle swiping. + ## [7.0.1] - Prevents `CardSwiperController` to be disposed by `CardSwiper`. diff --git a/pubspec.yaml b/pubspec.yaml index 4fa096e..5044c54 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: flutter_card_swiper description: This is a Tinder-like card swiper package. It allows you to swipe left, right, up, and down and define your own business logic for each direction. homepage: https://github.com/ricardodalarme/flutter_card_swiper issue_tracker: https://github.com/ricardodalarme/flutter_card_swiper/issues -version: 7.0.1 +version: 7.0.2 environment: sdk: ">=3.0.0 <4.0.0"