refactor: add an animation layer to split responsibilities

This commit is contained in:
ricardodalarme 2023-03-25 16:07:29 -03:00
parent 2d378c7d65
commit 8330f0bc80
3 changed files with 252 additions and 235 deletions

165
lib/src/card_animation.dart Normal file
View File

@ -0,0 +1,165 @@
import 'dart:math';
import 'package:flutter/widgets.dart';
import 'package:flutter_card_swiper/flutter_card_swiper.dart';
import 'package:flutter_card_swiper/src/extensions.dart';
class CardAnimation {
CardAnimation({
required this.animationController,
required this.maxAngle,
required this.initialScale,
this.isHorizontalSwipingEnabled = true,
this.isVerticalSwipingEnabled = true,
}) : scale = initialScale;
final double maxAngle;
final double initialScale;
final AnimationController animationController;
final bool isHorizontalSwipingEnabled;
final bool isVerticalSwipingEnabled;
double left = 0;
double top = 0;
double total = 0;
double angle = 0;
double scale;
double difference = 40;
late Animation<double> _leftAnimation;
late Animation<double> _topAnimation;
late Animation<double> _scaleAnimation;
late Animation<double> _differenceAnimation;
double get _maxAngleInRadian => maxAngle * (pi / 180);
void sync() {
left = _leftAnimation.value;
top = _topAnimation.value;
scale = _scaleAnimation.value;
difference = _differenceAnimation.value;
}
void reset() {
animationController.reset();
left = 0;
top = 0;
total = 0;
angle = 0;
scale = initialScale;
difference = 40;
}
void update(double dx, double dy, bool inverseAngle) {
if (isHorizontalSwipingEnabled) {
left += dx;
}
if (isVerticalSwipingEnabled) {
top += dy;
}
total = left + top;
updateAngle(inverseAngle);
updateScale();
updateDifference();
}
void updateAngle(bool inverse) {
if (angle.isBetween(-_maxAngleInRadian, _maxAngleInRadian)) {
angle = _maxAngleInRadian * left / 1000;
if (inverse) angle *= -1;
}
}
void updateScale() {
if (scale.isBetween(initialScale, 1.0)) {
scale = (total > 0)
? initialScale + (total / 5000)
: initialScale + -(total / 5000);
}
}
void updateDifference() {
if (difference.isBetween(0, difference)) {
difference = (total > 0) ? 40 - (total / 10) : 40 + (total / 10);
}
}
void animate(BuildContext context, CardSwiperDirection direction) {
switch (direction) {
case CardSwiperDirection.left:
return animateHorizontally(context, false);
case CardSwiperDirection.right:
return animateHorizontally(context, true);
case CardSwiperDirection.top:
return animateVertically(context, false);
case CardSwiperDirection.bottom:
return animateVertically(context, true);
default:
return;
}
}
void animateHorizontally(BuildContext context, bool isToRight) {
final screenWidth = MediaQuery.of(context).size.width;
_leftAnimation = Tween<double>(
begin: left,
end: isToRight ? screenWidth : -screenWidth,
).animate(animationController);
_topAnimation = Tween<double>(
begin: top,
end: top + top,
).animate(animationController);
_scaleAnimation = Tween<double>(
begin: scale,
end: 1.0,
).animate(animationController);
_differenceAnimation = Tween<double>(
begin: difference,
end: 0,
).animate(animationController);
animationController.forward();
}
void animateVertically(BuildContext context, bool isToBottom) {
final screenHeight = MediaQuery.of(context).size.height;
_leftAnimation = Tween<double>(
begin: left,
end: left + left,
).animate(animationController);
_topAnimation = Tween<double>(
begin: top,
end: isToBottom ? screenHeight : -screenHeight,
).animate(animationController);
_scaleAnimation = Tween<double>(
begin: scale,
end: 1.0,
).animate(animationController);
_differenceAnimation = Tween<double>(
begin: difference,
end: 0,
).animate(animationController);
animationController.forward();
}
void animateBack(BuildContext context) {
_leftAnimation = Tween<double>(
begin: left,
end: 0,
).animate(animationController);
_topAnimation = Tween<double>(
begin: top,
end: 0,
).animate(animationController);
_scaleAnimation = Tween<double>(
begin: scale,
end: initialScale,
).animate(animationController);
_differenceAnimation = Tween<double>(
begin: difference,
end: 40,
).animate(animationController);
animationController.forward();
}
}

View File

@ -1,6 +1,7 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_card_swiper/src/card_animation.dart';
import 'package:flutter_card_swiper/src/card_swiper_controller.dart'; import 'package:flutter_card_swiper/src/card_swiper_controller.dart';
import 'package:flutter_card_swiper/src/enums.dart'; import 'package:flutter_card_swiper/src/enums.dart';
import 'package:flutter_card_swiper/src/extensions.dart'; import 'package:flutter_card_swiper/src/extensions.dart';
@ -148,32 +149,18 @@ class CardSwiper extends StatefulWidget {
class _CardSwiperState<T extends Widget> extends State<CardSwiper> class _CardSwiperState<T extends Widget> extends State<CardSwiper>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
double _left = 0; late CardAnimation _cardAnimation;
double _top = 0; late AnimationController _animationController;
double _total = 0;
double _angle = 0;
late double _scale = widget.scale;
double _difference = 40;
SwipeType _swipeType = SwipeType.none; SwipeType _swipeType = SwipeType.none;
bool _tapOnTop = false; //position of starting drag point on card CardSwiperDirection _detectedDirection = CardSwiperDirection.none;
bool _tappedOnTop = false;
late AnimationController _animationController;
late Animation<double> _leftAnimation;
late Animation<double> _topAnimation;
late Animation<double> _scaleAnimation;
late Animation<double> _differenceAnimation;
CardSwiperDirection detectedDirection = CardSwiperDirection.none;
double get _maxAngle => widget.maxAngle * (pi / 180);
int? _currentIndex;
int? get _nextIndex => getValidIndexOffset(1);
bool get _canSwipe => _currentIndex != null && !widget.isDisabled; bool get _canSwipe => _currentIndex != null && !widget.isDisabled;
int? _currentIndex;
int? get _nextIndex => getValidIndexOffset(1);
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -188,6 +175,12 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
) )
..addListener(_animationListener) ..addListener(_animationListener)
..addStatusListener(_animationStatusListener); ..addStatusListener(_animationStatusListener);
_cardAnimation = CardAnimation(
animationController: _animationController,
maxAngle: widget.maxAngle,
initialScale: widget.scale,
);
} }
@override @override
@ -208,7 +201,7 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
fit: StackFit.expand, fit: StackFit.expand,
children: List.generate(nbOfCardsOnScreen(), (index) { children: List.generate(numberOfCardsOnScreen(), (index) {
if (index == 0) { if (index == 0) {
return _frontItem(constraints); return _frontItem(constraints);
} }
@ -225,14 +218,13 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
); );
} }
/// The card shown at the front of the stack, that can be dragged and swipped
Widget _frontItem(BoxConstraints constraints) { Widget _frontItem(BoxConstraints constraints) {
return Positioned( return Positioned(
left: _left, left: _cardAnimation.left,
top: _top, top: _cardAnimation.top,
child: GestureDetector( child: GestureDetector(
child: Transform.rotate( child: Transform.rotate(
angle: _angle, angle: _cardAnimation.angle,
child: ConstrainedBox( child: ConstrainedBox(
constraints: constraints, constraints: constraints,
child: widget.cardBuilder(context, _currentIndex!), child: widget.cardBuilder(context, _currentIndex!),
@ -248,44 +240,36 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
final renderBox = context.findRenderObject()! as RenderBox; final renderBox = context.findRenderObject()! as RenderBox;
final position = renderBox.globalToLocal(tapInfo.globalPosition); final position = renderBox.globalToLocal(tapInfo.globalPosition);
if (position.dy < renderBox.size.height / 2) _tapOnTop = true; if (position.dy < renderBox.size.height / 2) _tappedOnTop = true;
} }
}, },
onPanUpdate: (tapInfo) { onPanUpdate: (tapInfo) {
if (!widget.isDisabled) { if (!widget.isDisabled) {
setState(() { setState(
if (widget.isHorizontalSwipingEnabled) { () => _cardAnimation.update(
_left += tapInfo.delta.dx; tapInfo.delta.dx,
} tapInfo.delta.dy,
if (widget.isVerticalSwipingEnabled) { _tappedOnTop,
_top += tapInfo.delta.dy; ),
} );
_total = _left + _top;
_calculateAngle();
_calculateScale();
_calculateDifference();
});
} }
}, },
onPanEnd: (tapInfo) { onPanEnd: (tapInfo) {
if (_canSwipe) { if (_canSwipe) {
_tapOnTop = false; _tappedOnTop = false;
_onEndAnimation(); _onEndAnimation();
_animationController.forward();
} }
}, },
), ),
); );
} }
/// the card that is just behind the _frontItem, only moves to take its place
/// during a movement of _frontItem
Widget _secondItem(BoxConstraints constraints) { Widget _secondItem(BoxConstraints constraints) {
return Positioned( return Positioned(
top: _difference, top: _cardAnimation.difference,
left: 0, left: 0,
child: Transform.scale( child: Transform.scale(
scale: _scale, scale: _cardAnimation.scale,
child: ConstrainedBox( child: ConstrainedBox(
constraints: constraints, constraints: constraints,
child: widget.cardBuilder(context, _nextIndex!), child: widget.cardBuilder(context, _nextIndex!),
@ -294,8 +278,6 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
); );
} }
/// if widget.numberOfCardsDisplayed > 2, those cards are built behind the
/// _secondItem and can't move at all
Widget _backItem(BoxConstraints constraints, int offset) { Widget _backItem(BoxConstraints constraints, int offset) {
return Positioned( return Positioned(
top: 40, top: 40,
@ -313,48 +295,46 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
); );
} }
//swipe widget from the outside
void _controllerListener() { void _controllerListener() {
switch (widget.controller!.state) { switch (widget.controller?.state) {
case CardSwiperState.swipe: case CardSwiperState.swipe:
_swipe(context, widget.direction); return _swipe(widget.direction);
break;
case CardSwiperState.swipeLeft: case CardSwiperState.swipeLeft:
_swipe(context, CardSwiperDirection.left); return _swipe(CardSwiperDirection.left);
break;
case CardSwiperState.swipeRight: case CardSwiperState.swipeRight:
_swipe(context, CardSwiperDirection.right); return _swipe(CardSwiperDirection.right);
break;
case CardSwiperState.swipeTop: case CardSwiperState.swipeTop:
_swipe(context, CardSwiperDirection.top); return _swipe(CardSwiperDirection.top);
break;
case CardSwiperState.swipeBottom: case CardSwiperState.swipeBottom:
_swipe(context, CardSwiperDirection.bottom); return _swipe(CardSwiperDirection.bottom);
default:
return;
}
}
void _animationListener() {
if (_animationController.status == AnimationStatus.forward) {
setState(_cardAnimation.sync);
}
}
void _animationStatusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
switch (_swipeType) {
case SwipeType.swipe:
_handleCompleteSwipe();
break; break;
default: default:
break; break;
} }
}
//when value of controller changes _reset();
void _animationListener() {
if (_animationController.status == AnimationStatus.forward) {
setState(() {
_left = _leftAnimation.value;
_top = _topAnimation.value;
_scale = _scaleAnimation.value;
_difference = _differenceAnimation.value;
});
} }
} }
// handle the onSwipe methode as well as removing the current card from the void _handleCompleteSwipe() {
// stack if onSwipe does not return false final shouldCancelSwipe =
void _handleOnSwipe() { widget.onSwipe?.call(_currentIndex, _nextIndex, _detectedDirection) ==
setState(() {
if (_swipeType == SwipeType.swipe) {
final shouldCancelSwipe = widget.onSwipe
?.call(_currentIndex, _nextIndex, detectedDirection) ==
false; false;
if (shouldCancelSwipe) { if (shouldCancelSwipe) {
@ -368,174 +348,46 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
widget.onEnd?.call(); widget.onEnd?.call();
} }
} }
});
}
// reset the card animation void _reset() {
void _resetCardAnimation() {
setState(() { setState(() {
_animationController.reset(); _animationController.reset();
_left = 0; _cardAnimation.reset();
_top = 0;
_total = 0;
_angle = 0;
_scale = widget.scale;
_difference = 40;
_swipeType = SwipeType.none; _swipeType = SwipeType.none;
}); });
} }
//when the status of animation changes
void _animationStatusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
_handleOnSwipe();
_resetCardAnimation();
}
}
void _calculateAngle() {
if (_angle <= _maxAngle && _angle >= -_maxAngle) {
_angle = (_maxAngle / 100) * (_left / 10);
if (_tapOnTop) _angle *= -1;
}
}
void _calculateScale() {
if (_scale <= 1.0 && _scale >= widget.scale) {
_scale = (_total > 0)
? widget.scale + (_total / 5000)
: widget.scale + -1 * (_total / 5000);
}
}
void _calculateDifference() {
if (_difference >= 0 && _difference <= _difference) {
_difference = (_total > 0) ? 40 - (_total / 10) : 40 + (_total / 10);
}
}
void _onEndAnimation() { void _onEndAnimation() {
if (_left < -widget.threshold || _left > widget.threshold) { if (_cardAnimation.left.abs() > widget.threshold) {
_swipeHorizontal(context); final direction = _cardAnimation.left.isNegative
} else if (_top < -widget.threshold || _top > widget.threshold) { ? CardSwiperDirection.left
_swipeVertical(context); : CardSwiperDirection.right;
_swipe(direction);
} else if (_cardAnimation.top.abs() > widget.threshold) {
final direction = _cardAnimation.top.isNegative
? CardSwiperDirection.top
: CardSwiperDirection.bottom;
_swipe(direction);
} else { } else {
_goBack(context); _goBack();
} }
} }
void _swipe(BuildContext context, CardSwiperDirection direction) { void _swipe(CardSwiperDirection direction) {
if (!_canSwipe) return; if (!_canSwipe) return;
switch (direction) {
case CardSwiperDirection.left:
_left = -1;
_swipeHorizontal(context);
break;
case CardSwiperDirection.right:
_left = widget.threshold + 1;
_swipeHorizontal(context);
break;
case CardSwiperDirection.top:
_top = -1;
_swipeVertical(context);
break;
case CardSwiperDirection.bottom:
_top = widget.threshold + 1;
_swipeVertical(context);
break;
default:
break;
}
_animationController.forward();
}
//moves the card away to the left or right
void _swipeHorizontal(BuildContext context) {
_leftAnimation = Tween<double>(
begin: _left,
end: (_left == 0 && widget.direction == CardSwiperDirection.right) ||
_left > widget.threshold
? MediaQuery.of(context).size.width
: -MediaQuery.of(context).size.width,
).animate(_animationController);
_topAnimation = Tween<double>(
begin: _top,
end: _top + _top,
).animate(_animationController);
_scaleAnimation = Tween<double>(
begin: _scale,
end: 1.0,
).animate(_animationController);
_differenceAnimation = Tween<double>(
begin: _difference,
end: 0,
).animate(_animationController);
_swipeType = SwipeType.swipe; _swipeType = SwipeType.swipe;
if (_left > widget.threshold || _detectedDirection = direction;
_left == 0 && widget.direction == CardSwiperDirection.right) { _cardAnimation.animate(context, direction);
detectedDirection = CardSwiperDirection.right;
} else {
detectedDirection = CardSwiperDirection.left;
}
} }
//moves the card away to the top or bottom void _goBack() {
void _swipeVertical(BuildContext context) {
_leftAnimation = Tween<double>(
begin: _left,
end: _left + _left,
).animate(_animationController);
_topAnimation = Tween<double>(
begin: _top,
end: (_top == 0 && widget.direction == CardSwiperDirection.bottom) ||
_top > widget.threshold
? MediaQuery.of(context).size.height
: -MediaQuery.of(context).size.height,
).animate(_animationController);
_scaleAnimation = Tween<double>(
begin: _scale,
end: 1.0,
).animate(_animationController);
_differenceAnimation = Tween<double>(
begin: _difference,
end: 0,
).animate(_animationController);
_swipeType = SwipeType.swipe;
if (_top > widget.threshold ||
_top == 0 && widget.direction == CardSwiperDirection.bottom) {
detectedDirection = CardSwiperDirection.bottom;
} else {
detectedDirection = CardSwiperDirection.top;
}
}
//moves the card back to starting position
void _goBack(BuildContext context) {
_leftAnimation = Tween<double>(
begin: _left,
end: 0,
).animate(_animationController);
_topAnimation = Tween<double>(
begin: _top,
end: 0,
).animate(_animationController);
_scaleAnimation = Tween<double>(
begin: _scale,
end: widget.scale,
).animate(_animationController);
_differenceAnimation = Tween<double>(
begin: _difference,
end: 40,
).animate(_animationController);
_swipeType = SwipeType.back; _swipeType = SwipeType.back;
_detectedDirection = CardSwiperDirection.none;
_cardAnimation.animateBack(context);
} }
///the number of cards that are built on the screen int numberOfCardsOnScreen() {
int nbOfCardsOnScreen() {
if (widget.isLoop) { if (widget.isLoop) {
return widget.numberOfCardsDisplayed; return widget.numberOfCardsDisplayed;
} }
@ -555,7 +407,7 @@ class _CardSwiperState<T extends Widget> extends State<CardSwiper>
} }
final index = _currentIndex! + offset; final index = _currentIndex! + offset;
if (!widget.isLoop && !index.isBetween(0, widget.cardsCount)) { if (!widget.isLoop && !index.isBetween(0, widget.cardsCount - 1)) {
return null; return null;
} }
return index % widget.cardsCount; return index % widget.cardsCount;

View File

@ -1,5 +1,5 @@
extension Range on num { extension Range on num {
bool isBetween(num from, num to) { bool isBetween(num from, num to) {
return from < this && this < to; return from <= this && this <= to;
} }
} }