Swipe to an angle

Allow swiping to custom direction specified by an angle
This commit is contained in:
Sreelal TS 2024-10-05 15:45:01 +05:30 committed by ricardodalarme
parent aa378eaecd
commit 1b61523dfe
9 changed files with 234 additions and 31 deletions

View File

@ -4,6 +4,7 @@
library flutter_card_swiper; library flutter_card_swiper;
export 'package:flutter_card_swiper/src/controller/card_swiper_controller.dart'; 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/enums.dart';
export 'package:flutter_card_swiper/src/properties/allowed_swipe_direction.dart'; export 'package:flutter_card_swiper/src/properties/allowed_swipe_direction.dart';
export 'package:flutter_card_swiper/src/typedefs.dart'; export 'package:flutter_card_swiper/src/typedefs.dart';

View File

@ -136,13 +136,55 @@ class CardAnimation {
} }
void animate(BuildContext context, CardSwiperDirection direction) { void animate(BuildContext context, CardSwiperDirection direction) {
return switch (direction) { if (direction == CardSwiperDirection.none) {
CardSwiperDirection.left => animateHorizontally(context, false), return;
CardSwiperDirection.right => animateHorizontally(context, true), }
CardSwiperDirection.top => animateVertically(context, false), if (direction.isCloseTo(CardSwiperDirection.left)) {
CardSwiperDirection.bottom => animateVertically(context, true), animateHorizontally(context, false);
CardSwiperDirection.none => null, } 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<double>(
begin: left,
end: targetX,
).animate(animationController);
_topAnimation = Tween<double>(
begin: top,
end: targetY,
).animate(animationController);
_scaleAnimation = Tween<double>(
begin: scale,
end: 1.0,
).animate(animationController);
_differenceAnimation = Tween<Offset>(
begin: difference,
end: initialOffset,
).animate(animationController);
animationController.forward();
} }
void animateHorizontally(BuildContext context, bool isToRight) { void animateHorizontally(BuildContext context, bool isToRight) {
@ -210,13 +252,20 @@ class CardAnimation {
} }
void animateUndo(BuildContext context, CardSwiperDirection direction) { void animateUndo(BuildContext context, CardSwiperDirection direction) {
return switch (direction) { if (direction == CardSwiperDirection.none) {
CardSwiperDirection.left => animateUndoHorizontally(context, false), return;
CardSwiperDirection.right => animateUndoHorizontally(context, true), }
CardSwiperDirection.top => animateUndoVertically(context, false), if (direction.isCloseTo(CardSwiperDirection.left)) {
CardSwiperDirection.bottom => animateUndoVertically(context, true), animateUndoHorizontally(context, false);
_ => null } 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) { void animateUndoHorizontally(BuildContext context, bool isToRight) {
@ -262,4 +311,36 @@ class CardAnimation {
).animate(animationController); ).animate(animationController);
animationController.forward(); 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<double>(
begin: startX,
end: 0,
).animate(animationController);
_topAnimation = Tween<double>(
begin: startY,
end: 0,
).animate(animationController);
_scaleAnimation = Tween<double>(
begin: 1.0,
end: scale,
).animate(animationController);
_differenceAnimation = Tween<Offset>(
begin: initialOffset,
end: difference,
).animate(animationController);
animationController.forward();
}
} }

View File

@ -1,7 +1,7 @@
import 'dart:async'; 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/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. /// A controller that can be used to trigger swipes on a CardSwiper widget.
class CardSwiperController { class CardSwiperController {

View File

@ -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°)';
}

View File

@ -1,3 +1 @@
enum CardSwiperDirection { none, left, right, top, bottom }
enum SwipeType { none, swipe, back, undo } enum SwipeType { none, swipe, back, undo }

View File

@ -1,7 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/widgets.dart'; 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<bool> Function( typedef CardSwiperOnSwipe = FutureOr<bool> Function(
int previousIndex, int previousIndex,

View File

@ -1,12 +1,22 @@
import 'package:flutter/widgets.dart'; 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 { extension DirectionExtension on CardSwiperDirection {
Axis get axis => switch (this) { Axis get axis {
CardSwiperDirection.left || if (this == CardSwiperDirection.left || this == CardSwiperDirection.right) {
CardSwiperDirection.right => return Axis.horizontal;
Axis.horizontal, } else if (this == CardSwiperDirection.top ||
CardSwiperDirection.top || CardSwiperDirection.bottom => Axis.vertical, this == CardSwiperDirection.bottom) {
CardSwiperDirection.none => throw Exception('Direction is none'), 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
}
}
}
} }

View File

@ -3,12 +3,9 @@ import 'dart:collection';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/widgets.dart'; 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/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/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/number_extension.dart';
import 'package:flutter_card_swiper/src/utils/undoable.dart'; import 'package:flutter_card_swiper/src/utils/undoable.dart';

View File

@ -1,5 +1,5 @@
import 'package:flutter/widgets.dart'; 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_card_swiper/src/utils/direction_extension.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';