From 1b61523dfee1fc95cd8b0c40643d669fe6d49e93 Mon Sep 17 00:00:00 2001 From: Sreelal TS Date: Sat, 5 Oct 2024 15:45:01 +0530 Subject: [PATCH] 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 9eae6dd..094e246 100644 --- a/lib/src/widget/card_swiper.dart +++ b/lib/src/widget/card_swiper.dart @@ -3,12 +3,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';