Swipe to an angle
Allow swiping to custom direction specified by an angle
This commit is contained in:
parent
aa378eaecd
commit
1b61523dfe
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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°)';
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1 @@
|
||||||
enum CardSwiperDirection { none, left, right, top, bottom }
|
|
||||||
|
|
||||||
enum SwipeType { none, swipe, back, undo }
|
enum SwipeType { none, swipe, back, undo }
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue