diff --git a/lib/src/swipeable_cards_stack.dart b/lib/src/swipeable_cards_stack.dart new file mode 100644 index 0000000..ee38cb1 --- /dev/null +++ b/lib/src/swipeable_cards_stack.dart @@ -0,0 +1,365 @@ +library swipeable_cards_stack; + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:swipeable_cards_stack/src/swipeable_cards_stack_controller.dart'; + +const List cardsAlign = [ + Alignment(0.0, 1.0), + Alignment(0.0, 0.8), + Alignment(0.0, 0.0) +]; +final List cardsSize = List.filled(3, const Size(1, 1)); + +class SwipeableCardsStack extends StatefulWidget { + final SwipeableCardsStackController? cardController; + + //First 3 widgets + final List items; + final Function? onCardSwiped; + final double cardWidthTopMul; + final double cardWidthMiddleMul; + final double cardWidthBottomMul; + final double cardHeightTopMul; + final double cardHeightMiddleMul; + final double cardHeightBottomMul; + final Function? appendItemCallback; + final bool enableSwipeUp; + final bool enableSwipeDown; + + SwipeableCardsStack({ + Key? key, + this.cardController, + required BuildContext context, + required this.items, + this.onCardSwiped, + this.cardWidthTopMul = 0.9, + this.cardWidthMiddleMul = 0.85, + this.cardWidthBottomMul = 0.8, + this.cardHeightTopMul = 0.6, + this.cardHeightMiddleMul = 0.55, + this.cardHeightBottomMul = 0.5, + this.appendItemCallback, + this.enableSwipeUp = true, + this.enableSwipeDown = true, + }) : super(key: key) { + cardsSize[0] = Size( + MediaQuery.of(context).size.width * cardWidthTopMul, + MediaQuery.of(context).size.height * cardHeightTopMul, + ); + cardsSize[1] = Size( + MediaQuery.of(context).size.width * cardWidthMiddleMul, + MediaQuery.of(context).size.height * cardHeightMiddleMul, + ); + cardsSize[2] = Size( + MediaQuery.of(context).size.width * cardWidthBottomMul, + MediaQuery.of(context).size.height * cardHeightBottomMul, + ); + } + + @override + State createState() => _SwipeableCardsStackState(); +} + +class _SwipeableCardsStackState extends State + with SingleTickerProviderStateMixin { + int cardsCounter = 0; + int index = 0; + Widget? appendCard; + + List cards = []; + late AnimationController _controller; + bool enableSwipe = true; + + final Alignment defaultFrontCardAlign = const Alignment(0.0, 0.0); + Alignment frontCardAlign = cardsAlign[2]; + double frontCardRot = 0.0; + + void _triggerSwipe(Direction dir) { + final swipedCallback = widget.onCardSwiped ?? (_, __, ___) => true; + bool? shouldAnimate = false; + if (dir == Direction.left) { + shouldAnimate = swipedCallback(Direction.left, index, cards[0]); + frontCardAlign = const Alignment(-0.001, 0.0); + } else if (dir == Direction.right) { + shouldAnimate = swipedCallback(Direction.right, index, cards[0]); + frontCardAlign = const Alignment(0.001, 0.0); + } else if (dir == Direction.up) { + shouldAnimate = swipedCallback(Direction.up, index, cards[0]); + frontCardAlign = const Alignment(0.0, -0.001); + } else if (dir == Direction.down) { + shouldAnimate = swipedCallback(Direction.down, index, cards[0]); + frontCardAlign = const Alignment(0.0, 0.001); + } + + shouldAnimate ??= true; + + if (shouldAnimate) { + animateCards(); + } + } + + void _appendItem(Widget newCard) { + appendCard = newCard; + } + + void _enableSwipe(bool isSwipeEnabled) { + setState(() { + enableSwipe = isSwipeEnabled; + }); + } + + @override + void initState() { + super.initState(); + + final cardController = widget.cardController; + if (cardController != null) { + cardController.listener = _triggerSwipe; + cardController.addItem = _appendItem; + cardController.enableSwipeListener = _enableSwipe; + } + + // Init cards + for (cardsCounter = 0; cardsCounter < 3; cardsCounter++) { + if (widget.items.isNotEmpty && cardsCounter < widget.items.length) { + cards.add(widget.items[cardsCounter]); + } else { + cards.add(null); + } + } + + frontCardAlign = cardsAlign[2]; + + // Init the animation controller + _controller = AnimationController( + duration: const Duration(milliseconds: 700), + vsync: this, + ); + _controller.addListener(() => setState(() {})); + _controller.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) changeCardsOrder(); + }); + } + + @override + Widget build(BuildContext context) { + return Expanded( + child: IgnorePointer( + ignoring: !enableSwipe, + child: Stack( + children: [ + if (cards[2] != null) backCard(), + if (cards[1] != null) middleCard(), + if (cards[0] != null) frontCard(), + // Prevent swiping if the cards are animating + if (_controller.status != AnimationStatus.forward) + SizedBox.expand( + child: GestureDetector( + // While dragging the first card + onPanUpdate: (DragUpdateDetails details) { + // Add what the user swiped in the last frame to the alignment of the card + setState(() { + frontCardAlign = Alignment( + frontCardAlign.x + + 20 * + details.delta.dx / + MediaQuery.of(context).size.width, + frontCardAlign.y + + 20 * + details.delta.dy / + MediaQuery.of(context).size.height, + ); + + frontCardRot = frontCardAlign.x; // * rotation speed; + }); + }, + // When releasing the first card + onPanEnd: (_) { + // If the front card was swiped far enough to count as swiped + final onCardSwiped = + widget.onCardSwiped ?? (_, __, ___) => true; + bool? shouldAnimate = false; + if (frontCardAlign.x > 3.0) { + shouldAnimate = + onCardSwiped(Direction.right, index, cards[0]); + } else if (frontCardAlign.x < -3.0) { + shouldAnimate = + onCardSwiped(Direction.left, index, cards[0]); + } else if (frontCardAlign.y < -3.0 && + widget.enableSwipeUp) { + shouldAnimate = + onCardSwiped(Direction.up, index, cards[0]); + } else if (frontCardAlign.y > 3.0 && + widget.enableSwipeDown) { + shouldAnimate = + onCardSwiped(Direction.down, index, cards[0]); + } else { + // Return to the initial rotation and alignment + setState(() { + frontCardAlign = defaultFrontCardAlign; + frontCardRot = 0.0; + }); + } + + shouldAnimate ??= true; + + if (shouldAnimate) { + animateCards(); + } + }, + ), + ) + else + const SizedBox(), + ], + ), + ), + ); + } + + Widget backCard() { + return Align( + alignment: _controller.status == AnimationStatus.forward + ? CardsAnimation.backCardAlignmentAnim(_controller).value + : cardsAlign[0], + child: SizedBox.fromSize( + size: _controller.status == AnimationStatus.forward + ? CardsAnimation.backCardSizeAnim(_controller).value + : cardsSize[2], + child: cards[2], + ), + ); + } + + Widget middleCard() { + return Align( + alignment: _controller.status == AnimationStatus.forward + ? CardsAnimation.middleCardAlignmentAnim(_controller).value + : cardsAlign[1], + child: SizedBox.fromSize( + size: _controller.status == AnimationStatus.forward + ? CardsAnimation.middleCardSizeAnim(_controller).value + : cardsSize[1], + child: cards[1], + ), + ); + } + + Widget frontCard() { + return Align( + alignment: _controller.status == AnimationStatus.forward + ? CardsAnimation.frontCardDisappearAlignmentAnim( + _controller, + frontCardAlign, + ).value + : frontCardAlign, + child: Transform.rotate( + angle: (pi / 180.0) * frontCardRot, + child: SizedBox.fromSize(size: cardsSize[0], child: cards[0]), + ), + ); + } + + void changeCardsOrder() { + setState(() { + // Swap cards (back card becomes the middle card; middle card becomes the front card) + cards[0] = cards[1]; + cards[1] = cards[2]; + cards[2] = appendCard; + appendCard = null; + + cardsCounter++; + index++; + + frontCardAlign = defaultFrontCardAlign; + frontCardRot = 0.0; + }); + } + + void animateCards() { + _controller + ..stop() + ..value = 0.0 + ..forward(); + } +} + +class CardsAnimation { + static Animation backCardAlignmentAnim( + AnimationController parent, + ) { + return AlignmentTween(begin: cardsAlign[0], end: cardsAlign[1]).animate( + CurvedAnimation( + parent: parent, + curve: const Interval(0.4, 0.7, curve: Curves.easeIn), + ), + ); + } + + static Animation backCardSizeAnim(AnimationController parent) { + return SizeTween(begin: cardsSize[2], end: cardsSize[1]).animate( + CurvedAnimation( + parent: parent, + curve: const Interval(0.4, 0.7, curve: Curves.easeIn), + ), + ); + } + + static Animation middleCardAlignmentAnim( + AnimationController parent, + ) { + return AlignmentTween(begin: cardsAlign[1], end: cardsAlign[2]).animate( + CurvedAnimation( + parent: parent, + curve: const Interval(0.2, 0.5, curve: Curves.easeIn), + ), + ); + } + + static Animation middleCardSizeAnim(AnimationController parent) { + return SizeTween(begin: cardsSize[1], end: cardsSize[0]).animate( + CurvedAnimation( + parent: parent, + curve: const Interval(0.2, 0.5, curve: Curves.easeIn), + ), + ); + } + + static Animation frontCardDisappearAlignmentAnim( + AnimationController parent, + Alignment beginAlign, + ) { + if (beginAlign.x == -0.001 || + beginAlign.x == 0.001 || + beginAlign.x > 3.0 || + beginAlign.x < -3.0) { + return AlignmentTween( + begin: beginAlign, + end: Alignment( + beginAlign.x > 0 ? beginAlign.x + 30.0 : beginAlign.x - 30.0, + 0.0, + ), // Has swiped to the left or right? + ).animate( + CurvedAnimation( + parent: parent, + curve: const Interval(0.0, 0.5, curve: Curves.easeIn), + ), + ); + } else { + return AlignmentTween( + begin: beginAlign, + end: Alignment( + 0.0, + beginAlign.y > 0 ? beginAlign.y + 30.0 : beginAlign.y - 30.0, + ), // Has swiped to the top or bottom? + ).animate( + CurvedAnimation( + parent: parent, + curve: const Interval(0.0, 0.5, curve: Curves.easeIn), + ), + ); + } + } +} diff --git a/lib/swipeable_cards_stack_controller.dart b/lib/src/swipeable_cards_stack_controller.dart similarity index 100% rename from lib/swipeable_cards_stack_controller.dart rename to lib/src/swipeable_cards_stack_controller.dart diff --git a/lib/swipeable_cards_stack.dart b/lib/swipeable_cards_stack.dart index 82cc1e5..e3ebebb 100644 --- a/lib/swipeable_cards_stack.dart +++ b/lib/swipeable_cards_stack.dart @@ -1,367 +1,4 @@ library swipeable_cards_stack; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:swipeable_cards_stack/swipeable_cards_stack_controller.dart'; - -export 'swipeable_cards_stack_controller.dart'; - -const List cardsAlign = [ - Alignment(0.0, 1.0), - Alignment(0.0, 0.8), - Alignment(0.0, 0.0) -]; -final List cardsSize = List.filled(3, const Size(1, 1)); - -class SwipeableCardsStack extends StatefulWidget { - final SwipeableCardsStackController? cardController; - - //First 3 widgets - final List items; - final Function? onCardSwiped; - final double cardWidthTopMul; - final double cardWidthMiddleMul; - final double cardWidthBottomMul; - final double cardHeightTopMul; - final double cardHeightMiddleMul; - final double cardHeightBottomMul; - final Function? appendItemCallback; - final bool enableSwipeUp; - final bool enableSwipeDown; - - SwipeableCardsStack({ - Key? key, - this.cardController, - required BuildContext context, - required this.items, - this.onCardSwiped, - this.cardWidthTopMul = 0.9, - this.cardWidthMiddleMul = 0.85, - this.cardWidthBottomMul = 0.8, - this.cardHeightTopMul = 0.6, - this.cardHeightMiddleMul = 0.55, - this.cardHeightBottomMul = 0.5, - this.appendItemCallback, - this.enableSwipeUp = true, - this.enableSwipeDown = true, - }) : super(key: key) { - cardsSize[0] = Size( - MediaQuery.of(context).size.width * cardWidthTopMul, - MediaQuery.of(context).size.height * cardHeightTopMul, - ); - cardsSize[1] = Size( - MediaQuery.of(context).size.width * cardWidthMiddleMul, - MediaQuery.of(context).size.height * cardHeightMiddleMul, - ); - cardsSize[2] = Size( - MediaQuery.of(context).size.width * cardWidthBottomMul, - MediaQuery.of(context).size.height * cardHeightBottomMul, - ); - } - - @override - State createState() => _SwipeableCardsStackState(); -} - -class _SwipeableCardsStackState extends State - with SingleTickerProviderStateMixin { - int cardsCounter = 0; - int index = 0; - Widget? appendCard; - - List cards = []; - late AnimationController _controller; - bool enableSwipe = true; - - final Alignment defaultFrontCardAlign = const Alignment(0.0, 0.0); - Alignment frontCardAlign = cardsAlign[2]; - double frontCardRot = 0.0; - - void _triggerSwipe(Direction dir) { - final swipedCallback = widget.onCardSwiped ?? (_, __, ___) => true; - bool? shouldAnimate = false; - if (dir == Direction.left) { - shouldAnimate = swipedCallback(Direction.left, index, cards[0]); - frontCardAlign = const Alignment(-0.001, 0.0); - } else if (dir == Direction.right) { - shouldAnimate = swipedCallback(Direction.right, index, cards[0]); - frontCardAlign = const Alignment(0.001, 0.0); - } else if (dir == Direction.up) { - shouldAnimate = swipedCallback(Direction.up, index, cards[0]); - frontCardAlign = const Alignment(0.0, -0.001); - } else if (dir == Direction.down) { - shouldAnimate = swipedCallback(Direction.down, index, cards[0]); - frontCardAlign = const Alignment(0.0, 0.001); - } - - shouldAnimate ??= true; - - if (shouldAnimate) { - animateCards(); - } - } - - void _appendItem(Widget newCard) { - appendCard = newCard; - } - - void _enableSwipe(bool isSwipeEnabled) { - setState(() { - enableSwipe = isSwipeEnabled; - }); - } - - @override - void initState() { - super.initState(); - - final cardController = widget.cardController; - if (cardController != null) { - cardController.listener = _triggerSwipe; - cardController.addItem = _appendItem; - cardController.enableSwipeListener = _enableSwipe; - } - - // Init cards - for (cardsCounter = 0; cardsCounter < 3; cardsCounter++) { - if (widget.items.isNotEmpty && cardsCounter < widget.items.length) { - cards.add(widget.items[cardsCounter]); - } else { - cards.add(null); - } - } - - frontCardAlign = cardsAlign[2]; - - // Init the animation controller - _controller = AnimationController( - duration: const Duration(milliseconds: 700), - vsync: this, - ); - _controller.addListener(() => setState(() {})); - _controller.addStatusListener((AnimationStatus status) { - if (status == AnimationStatus.completed) changeCardsOrder(); - }); - } - - @override - Widget build(BuildContext context) { - return Expanded( - child: IgnorePointer( - ignoring: !enableSwipe, - child: Stack( - children: [ - if (cards[2] != null) backCard(), - if (cards[1] != null) middleCard(), - if (cards[0] != null) frontCard(), - // Prevent swiping if the cards are animating - if (_controller.status != AnimationStatus.forward) - SizedBox.expand( - child: GestureDetector( - // While dragging the first card - onPanUpdate: (DragUpdateDetails details) { - // Add what the user swiped in the last frame to the alignment of the card - setState(() { - frontCardAlign = Alignment( - frontCardAlign.x + - 20 * - details.delta.dx / - MediaQuery.of(context).size.width, - frontCardAlign.y + - 20 * - details.delta.dy / - MediaQuery.of(context).size.height, - ); - - frontCardRot = frontCardAlign.x; // * rotation speed; - }); - }, - // When releasing the first card - onPanEnd: (_) { - // If the front card was swiped far enough to count as swiped - final onCardSwiped = - widget.onCardSwiped ?? (_, __, ___) => true; - bool? shouldAnimate = false; - if (frontCardAlign.x > 3.0) { - shouldAnimate = - onCardSwiped(Direction.right, index, cards[0]); - } else if (frontCardAlign.x < -3.0) { - shouldAnimate = - onCardSwiped(Direction.left, index, cards[0]); - } else if (frontCardAlign.y < -3.0 && - widget.enableSwipeUp) { - shouldAnimate = - onCardSwiped(Direction.up, index, cards[0]); - } else if (frontCardAlign.y > 3.0 && - widget.enableSwipeDown) { - shouldAnimate = - onCardSwiped(Direction.down, index, cards[0]); - } else { - // Return to the initial rotation and alignment - setState(() { - frontCardAlign = defaultFrontCardAlign; - frontCardRot = 0.0; - }); - } - - shouldAnimate ??= true; - - if (shouldAnimate) { - animateCards(); - } - }, - ), - ) - else - const SizedBox(), - ], - ), - ), - ); - } - - Widget backCard() { - return Align( - alignment: _controller.status == AnimationStatus.forward - ? CardsAnimation.backCardAlignmentAnim(_controller).value - : cardsAlign[0], - child: SizedBox.fromSize( - size: _controller.status == AnimationStatus.forward - ? CardsAnimation.backCardSizeAnim(_controller).value - : cardsSize[2], - child: cards[2], - ), - ); - } - - Widget middleCard() { - return Align( - alignment: _controller.status == AnimationStatus.forward - ? CardsAnimation.middleCardAlignmentAnim(_controller).value - : cardsAlign[1], - child: SizedBox.fromSize( - size: _controller.status == AnimationStatus.forward - ? CardsAnimation.middleCardSizeAnim(_controller).value - : cardsSize[1], - child: cards[1], - ), - ); - } - - Widget frontCard() { - return Align( - alignment: _controller.status == AnimationStatus.forward - ? CardsAnimation.frontCardDisappearAlignmentAnim( - _controller, - frontCardAlign, - ).value - : frontCardAlign, - child: Transform.rotate( - angle: (pi / 180.0) * frontCardRot, - child: SizedBox.fromSize(size: cardsSize[0], child: cards[0]), - ), - ); - } - - void changeCardsOrder() { - setState(() { - // Swap cards (back card becomes the middle card; middle card becomes the front card) - cards[0] = cards[1]; - cards[1] = cards[2]; - cards[2] = appendCard; - appendCard = null; - - cardsCounter++; - index++; - - frontCardAlign = defaultFrontCardAlign; - frontCardRot = 0.0; - }); - } - - void animateCards() { - _controller - ..stop() - ..value = 0.0 - ..forward(); - } -} - -class CardsAnimation { - static Animation backCardAlignmentAnim( - AnimationController parent, - ) { - return AlignmentTween(begin: cardsAlign[0], end: cardsAlign[1]).animate( - CurvedAnimation( - parent: parent, - curve: const Interval(0.4, 0.7, curve: Curves.easeIn), - ), - ); - } - - static Animation backCardSizeAnim(AnimationController parent) { - return SizeTween(begin: cardsSize[2], end: cardsSize[1]).animate( - CurvedAnimation( - parent: parent, - curve: const Interval(0.4, 0.7, curve: Curves.easeIn), - ), - ); - } - - static Animation middleCardAlignmentAnim( - AnimationController parent, - ) { - return AlignmentTween(begin: cardsAlign[1], end: cardsAlign[2]).animate( - CurvedAnimation( - parent: parent, - curve: const Interval(0.2, 0.5, curve: Curves.easeIn), - ), - ); - } - - static Animation middleCardSizeAnim(AnimationController parent) { - return SizeTween(begin: cardsSize[1], end: cardsSize[0]).animate( - CurvedAnimation( - parent: parent, - curve: const Interval(0.2, 0.5, curve: Curves.easeIn), - ), - ); - } - - static Animation frontCardDisappearAlignmentAnim( - AnimationController parent, - Alignment beginAlign, - ) { - if (beginAlign.x == -0.001 || - beginAlign.x == 0.001 || - beginAlign.x > 3.0 || - beginAlign.x < -3.0) { - return AlignmentTween( - begin: beginAlign, - end: Alignment( - beginAlign.x > 0 ? beginAlign.x + 30.0 : beginAlign.x - 30.0, - 0.0, - ), // Has swiped to the left or right? - ).animate( - CurvedAnimation( - parent: parent, - curve: const Interval(0.0, 0.5, curve: Curves.easeIn), - ), - ); - } else { - return AlignmentTween( - begin: beginAlign, - end: Alignment( - 0.0, - beginAlign.y > 0 ? beginAlign.y + 30.0 : beginAlign.y - 30.0, - ), // Has swiped to the top or bottom? - ).animate( - CurvedAnimation( - parent: parent, - curve: const Interval(0.0, 0.5, curve: Curves.easeIn), - ), - ); - } - } -} +export 'src/swipeable_cards_stack.dart'; +export 'src/swipeable_cards_stack_controller.dart';