chore(package): change the base to the `appinio_swiper` package
175
README.md
|
|
@ -1,66 +1,159 @@
|
||||||
# swipeable_cards_stack
|
```appinio_swiper``` is a Flutter package for a Tinder Card Swiper. ✨
|
||||||
|
|
||||||
This is Tinder like swipeable cards package. You can add your own widgets to the stack, receive all four events, left, right, up and down. You can define your own business logic for each direction.
|
It allows swiping in all directions with any Custom Widget (Stateless or Statefull).
|
||||||
|
|
||||||
<img alt="Demo" src="https://github.com/ricardodalarme/swipeable-cards-stack/raw/main/images/swipe-card.gif" height="500">
|
Very smooth animations supporting Android, iOS & WebApp.
|
||||||
|
|
||||||
## Documentation
|
## Why?
|
||||||
|
|
||||||
### Installation
|
We build this package because we wanted to:
|
||||||
|
|
||||||
Add `swipeable_cards_stack` to your `pubspec.yaml`:
|
- have a complete customizable slider
|
||||||
|
- be able to swipe in every direction
|
||||||
|
- trigger unswipe however we want
|
||||||
|
- choose our own settings for the swiper such as duration, angle, padding..
|
||||||
|
- NEW: trigger swipe, swipe left and swipe right however we want
|
||||||
|
- NEW: add functions while un-/swiping, on end or when the swiper is disabled
|
||||||
|
- NEW: detect the direction (left, right, top, bottom) in which the card was swiped away
|
||||||
|
- NEW: unswipe all cards
|
||||||
|
|
||||||
|
## ❗NEW Features ❗
|
||||||
|
|
||||||
|
### Trigger swipe left and swipe right through controller
|
||||||
|
You can now trigger swipe left and swipe right with our ```AppinioSwiperController``` regardless of the chosen ```AppinioSwipeDirection``` (which is still used when ```swipe``` is called through the controller). Just like the unswipe and swipe call, you can call ```swipeLeft``` or ```swipeRight``` through the controller anywhere you want.
|
||||||
|
|
||||||
|
### Unswipe all cards
|
||||||
|
You can now unswipe as many cards as possible. If you set the parameter ```unlimitedUnswipe```to ```true``` (default value: ```false```) the limit of 1 card is extended to the number of cards that have been swiped away. The way to call ```unswipe``` hasn't changed.
|
||||||
|
|
||||||
|
### Detect direction of swipe
|
||||||
|
We've added the direction in which the card was swiped away to the function ```onSwipe```. The ```AppinioSwipeDirection``` gets now returned when the function gets called.
|
||||||
|
|
||||||
|
### Sending Feedback when widget is unswiped
|
||||||
|
We've added the function ```unswipe``` that now gets returned with the boolean ```true``` when the last card is unswiped and with boolean ```false``` when there is no last card to unswipe.
|
||||||
|
|
||||||
|
### Trigger swipe through controller
|
||||||
|
You can now trigger the swipe with our ```AppinioSwiperController```. Just like the unswipe call, you can call the ```swipe``` trough the controller anywhere you want. Just make sure to pass the controller to the parameter ```controller``` from our ```AppinioSwiper```.
|
||||||
|
|
||||||
|
## Show Cases
|
||||||
|
|
||||||
|
<img src="https://github.com/appinioGmbH/flutter_packages/blob/main/assets/swiper/swiping.gif?raw=true" height="250" /> <img src="https://github.com/appinioGmbH/flutter_packages/blob/main/assets/swiper/swipe_button.gif?raw=true" height="250" />
|
||||||
|
|
||||||
|
Trigger swipe right and swipe left however you want...
|
||||||
|
|
||||||
|
<img src="https://github.com/appinioGmbH/flutter_packages/blob/main/assets/swiper/swipe_left_right.gif?raw=true" height="250" />
|
||||||
|
|
||||||
|
Unswipe the cards however you want...
|
||||||
|
|
||||||
|
<img src="https://github.com/appinioGmbH/flutter_packages/blob/main/assets/swiper/unswipe.gif?raw=true" height="250" />
|
||||||
|
|
||||||
|
Customize the angle...
|
||||||
|
|
||||||
|
<img src="https://github.com/appinioGmbH/flutter_packages/blob/main/assets/swiper/angle.gif?raw=true" height="250" />
|
||||||
|
|
||||||
|
Customize the threshold of the swiper, when the card should slide away...
|
||||||
|
|
||||||
|
<img src="https://github.com/appinioGmbH/flutter_packages/blob/main/assets/swiper/treshold.gif?raw=true" height="250" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Create a new project with the command
|
||||||
|
```yaml
|
||||||
|
flutter create MyApp
|
||||||
|
```
|
||||||
|
Add
|
||||||
|
```yaml
|
||||||
|
appinio_swiper: ...
|
||||||
|
```
|
||||||
|
to your `pubspec.yaml` of your flutter project.
|
||||||
|
**OR**
|
||||||
|
run
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
dependencies:
|
flutter pub add appinio_swiper
|
||||||
swipeable_cards_stack: <latest version>
|
|
||||||
```
|
```
|
||||||
|
in your project's root directory.
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
Use the `SwipeableCardsStack` widget provided by the package
|
In your library add the following import:
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
import 'package:swipeable_cards_stack/swipeable_cards_stack.dart';
|
import 'package:appinio_swiper/appinio_swiper.dart';
|
||||||
|
```
|
||||||
|
|
||||||
class MyWidget extends StatefulWidget {
|
For help getting started with Flutter, view the online [documentation](https://flutter.io/).
|
||||||
@override
|
|
||||||
State<MyWidget> createState() => _MyWidgetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyWidgetState extends State<MyWidget> {
|
## Usage
|
||||||
final _cardsController = SwipeableCardsStackController();
|
You can place your `AppinioSwiper` inside of a `Scaffold` or `CupertinoPageScaffold` like we did here. Optional parameters can be defined to enable different features. See the following example..
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:appinio_swiper/appinio_swiper.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class Example extends StatelessWidget {
|
||||||
|
List<Container> cards = [
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Text('1'),
|
||||||
|
color: CupertinoColors.activeBlue,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Text('2'),
|
||||||
|
color: CupertinoColors.activeBlue,
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Text('3'),
|
||||||
|
color: CupertinoColors.activeBlue,
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SwipeableCardsStack(
|
return CupertinoPageScaffold(
|
||||||
cardController: _cardsController,
|
child: SizedBox(
|
||||||
context: context,
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
// Add the first 3 cards (widgets)
|
child: AppinioSwiper(
|
||||||
items: [
|
cards: cards,
|
||||||
CardView(text: "First card"),
|
),
|
||||||
CardView(text: "Second card"),
|
),
|
||||||
CardView(text: "Third card"),
|
|
||||||
],
|
|
||||||
// Get card swipe event callbacks
|
|
||||||
onCardSwiped: (dir, index, widget) {
|
|
||||||
// Add the next card using _cardController
|
|
||||||
_cardsController.addItem(CardView(text: "Next card"));
|
|
||||||
|
|
||||||
// Take action on the swiped widget based on the direction of swipe
|
|
||||||
// Return false to not animate cards
|
|
||||||
},
|
|
||||||
enableSwipeUp: true,
|
|
||||||
enableSwipeDown: false,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Contributing
|
## Constructor
|
||||||
|
#### Basic
|
||||||
|
|
||||||
If you want to contribute to this project, you may easily create issues and send PRs. Please take note that your code contributions will be applicable under MIT license unless specified otherwise.
|
|
||||||
|
|
||||||
### Credits
|
| Parameter | Default | Description | Required |
|
||||||
|
| ------------- |:-------------|:-----|:-----:|
|
||||||
|
| cards | - | List of Widgets for the swiper | true
|
||||||
|
| controller | - | Trigger unswipe | false
|
||||||
|
| padding | EdgeInsets.symmetric(horizontal: 20, vertical: 25) | Control swiper padding | false
|
||||||
|
| duration | 200 milliseconds | The duration that every animation should last | false
|
||||||
|
| maxAngle | 30 | Maximum angle the card reaches while swiping | false
|
||||||
|
| threshold | 50 | Threshold from which the card is swiped away | false
|
||||||
|
| isDisabled | false | Set to ```true``` if swiping should be disabled, has no impact when triggered from the outside | false
|
||||||
|
| onTapDisabled | - | Function that get triggered when the swiper is disabled | false
|
||||||
|
| onSwipe | - | Called with the new index and detected swipe direction when the user swiped | false
|
||||||
|
| onEnd | - | Called when there is no Widget left to be swiped away | false
|
||||||
|
| direction | right | Direction in which the card is swiped away when triggered from the outside | false
|
||||||
|
| allowUnswipe | true | Set to ```false``` if unswipe should be disabled away | false
|
||||||
|
| unlimitedUnswipe | false | Set to ```true``` if the user can unswipe as many cards as possible | false
|
||||||
|
| unswipe | - | Called with the boolean ```true``` when the last card gets unswiped and with the boolean ```false``` if there is no card to unswipe | false
|
||||||
|
|
||||||
- **Ricardo Dalarme** (Package maintainer)
|
#### Controller
|
||||||
- [**CodeToArt Technology**](https://github.com/codetoart) (Original project creator)
|
|
||||||
|
The ```Controller``` is used to control the ```swipe```, ```swipeLeft```, ```swipeRight``` or ```unswipe``` function of the swiper from outside of the widget. You can create a controller called ```AppinioSwiperController``` and save the instance for further usage. Please have a closer look to our Example for the usage.
|
||||||
|
|
||||||
|
| Method | Description
|
||||||
|
| ------------- |:-------------
|
||||||
|
| swipe | Changes the state of the controller to swipe and swipes the card in your selected direction.
|
||||||
|
| swipeLeft | Changes the state of the controller to swipe left and swipes the card to the left side.
|
||||||
|
| swipeRight | Changes the state of the controller to swipe right and swipes the card to the right side.
|
||||||
|
| unswipe | Changes the state of the controller to unswipe and brings back the last card that was swiped away.
|
||||||
|
|
||||||
|
|
||||||
|
<hr/>
|
||||||
|
Made with ❤ by Flutter team at <a href="https://appinio.com">Appinio GmbH</a>
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
class CardView extends StatelessWidget {
|
|
||||||
const CardView({
|
|
||||||
Key? key,
|
|
||||||
this.text = "Card View",
|
|
||||||
}) : super(key: key);
|
|
||||||
final String text;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
stops: [
|
|
||||||
0.5,
|
|
||||||
1,
|
|
||||||
],
|
|
||||||
colors: [
|
|
||||||
Colors.blueAccent,
|
|
||||||
Color.fromARGB(255, 6, 28, 61),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
Text(
|
|
||||||
text,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 20.0,
|
|
||||||
fontWeight: FontWeight.w700),
|
|
||||||
),
|
|
||||||
const Padding(padding: EdgeInsets.only(bottom: 8.0)),
|
|
||||||
Text("$text details",
|
|
||||||
textAlign: TextAlign.start,
|
|
||||||
style: const TextStyle(color: Colors.white)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:appinio_swiper/appinio_swiper.dart';
|
||||||
|
|
||||||
|
class ExampleButton extends StatelessWidget {
|
||||||
|
final Function onTap;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const ExampleButton({
|
||||||
|
required this.onTap,
|
||||||
|
required this.child,
|
||||||
|
Key? key,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => onTap(),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//swipe card to the right side
|
||||||
|
Widget swipeRightButton(AppinioSwiperController controller) {
|
||||||
|
return ExampleButton(
|
||||||
|
onTap: () => controller.swipeRight(),
|
||||||
|
child: Container(
|
||||||
|
height: 60,
|
||||||
|
width: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: CupertinoColors.activeGreen,
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: CupertinoColors.activeGreen.withOpacity(0.9),
|
||||||
|
spreadRadius: -10,
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 20), // changes position of shadow
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.check,
|
||||||
|
color: CupertinoColors.white,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//swipe card to the left side
|
||||||
|
Widget swipeLeftButton(AppinioSwiperController controller) {
|
||||||
|
return ExampleButton(
|
||||||
|
onTap: () => controller.swipeLeft(),
|
||||||
|
child: Container(
|
||||||
|
height: 60,
|
||||||
|
width: 60,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFF3868),
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: const Color(0xFFFF3868).withOpacity(0.9),
|
||||||
|
spreadRadius: -10,
|
||||||
|
blurRadius: 20,
|
||||||
|
offset: const Offset(0, 20), // changes position of shadow
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: CupertinoColors.white,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
//unswipe card
|
||||||
|
Widget unswipeButton(AppinioSwiperController controller) {
|
||||||
|
return ExampleButton(
|
||||||
|
onTap: () => controller.unswipe(),
|
||||||
|
child: Container(
|
||||||
|
height: 60,
|
||||||
|
width: 60,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.rotate_left_rounded,
|
||||||
|
color: CupertinoColors.systemGrey2,
|
||||||
|
size: 40,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
|
||||||
|
class ExampleCandidateModel {
|
||||||
|
String? name;
|
||||||
|
String? job;
|
||||||
|
String? city;
|
||||||
|
LinearGradient? color;
|
||||||
|
|
||||||
|
ExampleCandidateModel({
|
||||||
|
this.name,
|
||||||
|
this.job,
|
||||||
|
this.city,
|
||||||
|
this.color,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ExampleCandidateModel> candidates = [
|
||||||
|
ExampleCandidateModel(
|
||||||
|
name: 'Two, 2',
|
||||||
|
job: 'Manager',
|
||||||
|
city: 'Town',
|
||||||
|
color: gradientPurple,
|
||||||
|
),
|
||||||
|
ExampleCandidateModel(
|
||||||
|
name: 'One, 1',
|
||||||
|
job: 'Manager',
|
||||||
|
city: 'Town',
|
||||||
|
color: gradientRed,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const LinearGradient gradientRed = LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFF3868),
|
||||||
|
Color(0xFFFFB49A),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const LinearGradient gradientPurple = LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF736EFE),
|
||||||
|
Color(0xFF62E4EC),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const LinearGradient gradientBlue = LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF0BA4E0),
|
||||||
|
Color(0xFFA9E4BD),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const LinearGradient gradientPink = LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFFFF6864),
|
||||||
|
Color(0xFFFFB92F),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const LinearGradient kNewFeedCardColorsIdentityGradient = LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF7960F1),
|
||||||
|
Color(0xFFE1A5C9),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'example_candidate_model.dart';
|
||||||
|
|
||||||
|
class ExampleCard extends StatelessWidget {
|
||||||
|
final ExampleCandidateModel candidate;
|
||||||
|
|
||||||
|
const ExampleCard({
|
||||||
|
Key? key,
|
||||||
|
required this.candidate,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
color: CupertinoColors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: CupertinoColors.systemGrey.withOpacity(0.2),
|
||||||
|
spreadRadius: 3,
|
||||||
|
blurRadius: 7,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: candidate.color,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(10),
|
||||||
|
topRight: Radius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(15),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(10),
|
||||||
|
bottomRight: Radius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
candidate.name!,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 5,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
candidate.job!,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 5,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
candidate.city!,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.grey,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,86 +1,114 @@
|
||||||
import 'package:example/card_view.dart';
|
import 'dart:developer';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:swipeable_cards_stack/swipeable_cards_stack.dart';
|
|
||||||
|
import 'package:appinio_swiper/appinio_swiper.dart';
|
||||||
|
import 'package:example/example_candidate_model.dart';
|
||||||
|
import 'package:example/example_card.dart';
|
||||||
|
import 'example_buttons.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MaterialApp(
|
runApp(const MyApp());
|
||||||
title: 'Flutter Demo',
|
|
||||||
home: const MyHomePage(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyHomePage extends StatefulWidget {
|
class MyApp extends StatelessWidget {
|
||||||
const MyHomePage({Key? key}) : super(key: key);
|
const MyApp({
|
||||||
|
Key? key,
|
||||||
@override
|
}) : super(key: key);
|
||||||
State<MyHomePage> createState() => _MyHomePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _MyHomePageState extends State<MyHomePage> {
|
|
||||||
int counter = 4;
|
|
||||||
final cardController = SwipeableCardsStackController();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return const CupertinoApp(
|
||||||
body: Column(
|
debugShowCheckedModeBanner: false,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
home: Example(),
|
||||||
children: [
|
);
|
||||||
SwipeableCardsStack(
|
}
|
||||||
cardController: cardController,
|
|
||||||
context: context,
|
|
||||||
//add the first 3 cards
|
|
||||||
items: const [
|
|
||||||
CardView(text: "First card"),
|
|
||||||
CardView(text: "Second card"),
|
|
||||||
CardView(text: "Third card"),
|
|
||||||
],
|
|
||||||
onCardSwiped: (dir, index, widget) {
|
|
||||||
//Add the next card
|
|
||||||
if (counter <= 20) {
|
|
||||||
cardController.addItem(CardView(text: "Card $counter"));
|
|
||||||
counter++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dir == AxisDirection.left) {
|
class Example extends StatefulWidget {
|
||||||
debugPrint('onDisliked ${(widget as CardView).text} $index');
|
const Example({
|
||||||
} else if (dir == AxisDirection.right) {
|
Key? key,
|
||||||
debugPrint('onLiked ${(widget as CardView).text} $index');
|
}) : super(key: key);
|
||||||
} else if (dir == AxisDirection.up) {
|
|
||||||
debugPrint('onUp ${(widget as CardView).text} $index');
|
@override
|
||||||
} else if (dir == AxisDirection.down) {
|
State<Example> createState() => _ExamplePageState();
|
||||||
debugPrint('onDown ${(widget as CardView).text} $index');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
class _ExamplePageState extends State<Example> {
|
||||||
},
|
final AppinioSwiperController controller = AppinioSwiperController();
|
||||||
|
|
||||||
|
List<ExampleCard> cards = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
_loadCards();
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadCards() {
|
||||||
|
for (ExampleCandidateModel candidate in candidates) {
|
||||||
|
cards.add(
|
||||||
|
ExampleCard(
|
||||||
|
candidate: candidate,
|
||||||
),
|
),
|
||||||
Container(
|
);
|
||||||
margin: const EdgeInsets.symmetric(vertical: 20.0),
|
}
|
||||||
child: Row(
|
}
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return CupertinoPageScaffold(
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
FloatingActionButton(
|
const SizedBox(
|
||||||
child: const Icon(Icons.chevron_left),
|
height: 50,
|
||||||
onPressed: () => cardController.triggerSwipeLeft(),
|
|
||||||
),
|
),
|
||||||
FloatingActionButton(
|
SizedBox(
|
||||||
child: const Icon(Icons.chevron_right),
|
height: MediaQuery.of(context).size.height * 0.75,
|
||||||
onPressed: () => cardController.triggerSwipeRight(),
|
child: AppinioSwiper(
|
||||||
|
unlimitedUnswipe: true,
|
||||||
|
controller: controller,
|
||||||
|
unswipe: _unswipe,
|
||||||
|
cards: cards,
|
||||||
|
onSwipe: _swipe,
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 25,
|
||||||
|
right: 25,
|
||||||
|
top: 50,
|
||||||
|
bottom: 40,
|
||||||
),
|
),
|
||||||
FloatingActionButton(
|
|
||||||
child: const Icon(Icons.arrow_upward),
|
|
||||||
onPressed: () => cardController.triggerSwipeUp(),
|
|
||||||
),
|
),
|
||||||
FloatingActionButton(
|
|
||||||
child: const Icon(Icons.arrow_downward),
|
|
||||||
onPressed: () => cardController.triggerSwipeDown(),
|
|
||||||
),
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const SizedBox(
|
||||||
|
width: 80,
|
||||||
|
),
|
||||||
|
swipeLeftButton(controller),
|
||||||
|
const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
),
|
||||||
|
swipeRightButton(controller),
|
||||||
|
const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
),
|
||||||
|
unswipeButton(controller),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _swipe(int index, AppinioSwiperDirection direction) {
|
||||||
|
log("the card was swiped to the: " + direction.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unswipe(bool unswiped) {
|
||||||
|
if (unswiped) {
|
||||||
|
log("SUCCESS: card was unswiped");
|
||||||
|
} else {
|
||||||
|
log("FAIL: no card left to unswipe");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,13 @@
|
||||||
# Generated by pub
|
# Generated by pub
|
||||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
packages:
|
packages:
|
||||||
|
appinio_swiper:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
path: ".."
|
||||||
|
relative: true
|
||||||
|
source: path
|
||||||
|
version: "1.1.1"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -39,13 +46,6 @@ packages:
|
||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.99"
|
version: "0.0.99"
|
||||||
swipeable_cards_stack:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
path: ".."
|
|
||||||
relative: true
|
|
||||||
source: path
|
|
||||||
version: "1.0.2+1"
|
|
||||||
vector_math:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -54,5 +54,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.18.6 <3.0.0"
|
dart: ">=2.17.0-0 <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,16 @@
|
||||||
name: example
|
name: example
|
||||||
description: A new Flutter project.
|
description: Example for the Appinio Swiper
|
||||||
|
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: 'none'
|
||||||
version: 1.0.0+1
|
version: 0.0.1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.18.6 <3.0.0'
|
sdk: ">=2.12.0 <3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
appinio_swiper:
|
||||||
swipeable_cards_stack:
|
|
||||||
path: ../
|
path: ../
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
|
|
|
||||||
|
After Width: | Height: | Size: 693 KiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
After Width: | Height: | Size: 788 KiB |
|
Before Width: | Height: | Size: 508 KiB |
|
After Width: | Height: | Size: 852 KiB |
|
After Width: | Height: | Size: 542 KiB |
|
After Width: | Height: | Size: 552 KiB |
|
After Width: | Height: | Size: 576 KiB |
|
|
@ -0,0 +1,600 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AppinioSwiper extends StatefulWidget {
|
||||||
|
/// list of widgets for the swiper
|
||||||
|
final List<Widget?>? cards;
|
||||||
|
|
||||||
|
/// controller to trigger unswipe action
|
||||||
|
final AppinioSwiperController? controller;
|
||||||
|
|
||||||
|
/// duration of every animation
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
/// padding of the swiper
|
||||||
|
final EdgeInsetsGeometry padding;
|
||||||
|
|
||||||
|
/// maximum angle the card reaches while swiping
|
||||||
|
final double maxAngle;
|
||||||
|
|
||||||
|
/// threshold from which the card is swiped away
|
||||||
|
final int threshold;
|
||||||
|
|
||||||
|
/// set to true if swiping should be disabled, exception: triggered from the outside
|
||||||
|
final bool isDisabled;
|
||||||
|
|
||||||
|
/// set to false if unswipe should be disabled
|
||||||
|
final bool allowUnswipe;
|
||||||
|
|
||||||
|
/// set to true if the user can unswipe as many cards as possible
|
||||||
|
final bool unlimitedUnswipe;
|
||||||
|
|
||||||
|
/// function that gets called with the new index and detected swipe direction when the user swiped or swipe is triggered by controller
|
||||||
|
final Function onSwipe;
|
||||||
|
|
||||||
|
/// function that gets called when there is no widget left to be swiped away
|
||||||
|
final Function onEnd;
|
||||||
|
|
||||||
|
/// function that gets triggered when the swiper is disabled
|
||||||
|
final Function onTapDisabled;
|
||||||
|
|
||||||
|
/// function that gets called with the boolean true when the last card gets unswiped and with the boolean false when there is no card to unswipe
|
||||||
|
final Function unswipe;
|
||||||
|
|
||||||
|
/// direction in which the card gets swiped when triggered by controller, default set to right
|
||||||
|
final AppinioSwiperDirection direction;
|
||||||
|
|
||||||
|
const AppinioSwiper({
|
||||||
|
Key? key,
|
||||||
|
required this.cards,
|
||||||
|
this.controller,
|
||||||
|
this.padding = const EdgeInsets.symmetric(horizontal: 20, vertical: 25),
|
||||||
|
this.duration = const Duration(milliseconds: 200),
|
||||||
|
this.maxAngle = 30,
|
||||||
|
this.threshold = 50,
|
||||||
|
this.isDisabled = false,
|
||||||
|
this.allowUnswipe = true,
|
||||||
|
this.unlimitedUnswipe = false,
|
||||||
|
this.onTapDisabled = emptyFunction,
|
||||||
|
this.onSwipe = emptyFunctionIndex,
|
||||||
|
this.onEnd = emptyFunction,
|
||||||
|
this.unswipe = emptyFunctionBool,
|
||||||
|
this.direction = AppinioSwiperDirection.right,
|
||||||
|
}) : assert(maxAngle >= 0 && maxAngle <= 360),
|
||||||
|
assert(threshold >= 1 && threshold <= 100),
|
||||||
|
assert(direction != AppinioSwiperDirection.none),
|
||||||
|
super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => _AppinioSwiperState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppinioSwiperState extends State<AppinioSwiper>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
double _left = 0;
|
||||||
|
double _top = 0;
|
||||||
|
double _total = 0;
|
||||||
|
double _angle = 0;
|
||||||
|
double _maxAngle = 0;
|
||||||
|
double _scale = 0.9;
|
||||||
|
double _difference = 40;
|
||||||
|
|
||||||
|
int _swipeTyp = 0; // 1 = swipe, 2 = unswipe, 3 = goBack
|
||||||
|
bool _tapOnTop = false; //position of starting drag point on card
|
||||||
|
|
||||||
|
late AnimationController _animationController;
|
||||||
|
late Animation<double> _leftAnimation;
|
||||||
|
late Animation<double> _topAnimation;
|
||||||
|
late Animation<double> _scaleAnimation;
|
||||||
|
late Animation<double> _differenceAnimation;
|
||||||
|
late Animation<double> _unSwipeLeftAnimation;
|
||||||
|
late Animation<double> _unSwipeTopAnimation;
|
||||||
|
|
||||||
|
bool _vertical = false;
|
||||||
|
bool _horizontal = false;
|
||||||
|
bool _isUnswiping = false;
|
||||||
|
int _swipedDirectionVertical = 0; //-1 left, 1 right
|
||||||
|
int _swipedDirectionHorizontal = 0; //-1 bottom, 1 top
|
||||||
|
|
||||||
|
AppinioUnswipeCard? _lastCard;
|
||||||
|
// ignore: prefer_final_fields
|
||||||
|
List<AppinioUnswipeCard?> _lastCards = [];
|
||||||
|
AppinioSwiperDirection detectedDirection = AppinioSwiperDirection.none;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
|
||||||
|
if (widget.controller != null) {
|
||||||
|
widget.controller!
|
||||||
|
//swipe widget from the outside
|
||||||
|
..addListener(() {
|
||||||
|
if (widget.controller!.state == AppinioSwiperState.swipe) {
|
||||||
|
if (widget.cards!.isNotEmpty) {
|
||||||
|
switch (widget.direction) {
|
||||||
|
case AppinioSwiperDirection.right:
|
||||||
|
_swipeHorizontal(context);
|
||||||
|
break;
|
||||||
|
case AppinioSwiperDirection.left:
|
||||||
|
_swipeHorizontal(context);
|
||||||
|
break;
|
||||||
|
case AppinioSwiperDirection.top:
|
||||||
|
_swipeVertical(context);
|
||||||
|
break;
|
||||||
|
case AppinioSwiperDirection.bottom:
|
||||||
|
_swipeVertical(context);
|
||||||
|
break;
|
||||||
|
case AppinioSwiperDirection.none:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_animationController.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//swipe widget left from the outside
|
||||||
|
..addListener(() {
|
||||||
|
if (widget.controller!.state == AppinioSwiperState.swipeLeft) {
|
||||||
|
if (widget.cards!.isNotEmpty) {
|
||||||
|
_left = -1;
|
||||||
|
_swipeHorizontal(context);
|
||||||
|
_animationController.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//swipe widget right from the outside
|
||||||
|
..addListener(() {
|
||||||
|
if (widget.controller!.state == AppinioSwiperState.swipeRight) {
|
||||||
|
if (widget.cards!.isNotEmpty) {
|
||||||
|
_left = widget.threshold + 1;
|
||||||
|
_swipeHorizontal(context);
|
||||||
|
_animationController.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
//unswipe widget from the outside
|
||||||
|
..addListener(() {
|
||||||
|
if (widget.controller!.state == AppinioSwiperState.unswipe) {
|
||||||
|
if (widget.allowUnswipe) {
|
||||||
|
if (!_isUnswiping) {
|
||||||
|
if (_lastCard != null || _lastCards.isNotEmpty) {
|
||||||
|
if (widget.unlimitedUnswipe) {
|
||||||
|
_unswipe(_lastCards.last!);
|
||||||
|
} else {
|
||||||
|
_unswipe(_lastCard!);
|
||||||
|
}
|
||||||
|
widget.unswipe(true);
|
||||||
|
_animationController.forward();
|
||||||
|
} else {
|
||||||
|
widget.unswipe(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (widget.maxAngle > 0) {
|
||||||
|
_maxAngle = widget.maxAngle * (pi / 180);
|
||||||
|
}
|
||||||
|
|
||||||
|
_animationController =
|
||||||
|
AnimationController(duration: widget.duration, vsync: this);
|
||||||
|
_animationController.addListener(() {
|
||||||
|
//when value of controller changes
|
||||||
|
if (_animationController.status == AnimationStatus.forward) {
|
||||||
|
setState(() {
|
||||||
|
if (_swipeTyp != 2) {
|
||||||
|
_left = _leftAnimation.value;
|
||||||
|
_top = _topAnimation.value;
|
||||||
|
}
|
||||||
|
if (_swipeTyp == 2) {
|
||||||
|
_left = _unSwipeLeftAnimation.value;
|
||||||
|
_top = _unSwipeTopAnimation.value;
|
||||||
|
}
|
||||||
|
_scale = _scaleAnimation.value;
|
||||||
|
_difference = _differenceAnimation.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_animationController.addStatusListener((status) {
|
||||||
|
//when status of controller changes
|
||||||
|
if (status == AnimationStatus.completed) {
|
||||||
|
setState(() {
|
||||||
|
if (_swipeTyp == 1) {
|
||||||
|
if (widget.unlimitedUnswipe) {
|
||||||
|
_lastCards.add(
|
||||||
|
AppinioUnswipeCard(
|
||||||
|
widget: widget.cards!.last!,
|
||||||
|
horizontal: _horizontal,
|
||||||
|
vertical: _vertical,
|
||||||
|
swipedDirectionHorizontal: _swipedDirectionHorizontal,
|
||||||
|
swipedDirectionVertical: _swipedDirectionVertical,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_lastCard = AppinioUnswipeCard(
|
||||||
|
widget: widget.cards!.last!,
|
||||||
|
horizontal: _horizontal,
|
||||||
|
vertical: _vertical,
|
||||||
|
swipedDirectionHorizontal: _swipedDirectionHorizontal,
|
||||||
|
swipedDirectionVertical: _swipedDirectionVertical,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_swipedDirectionHorizontal = 0;
|
||||||
|
_swipedDirectionVertical = 0;
|
||||||
|
_vertical = false;
|
||||||
|
_horizontal = false;
|
||||||
|
widget.cards!.removeLast();
|
||||||
|
|
||||||
|
widget.onSwipe(widget.cards!.length, detectedDirection);
|
||||||
|
if (widget.cards!.isEmpty) widget.onEnd();
|
||||||
|
} else if (_swipeTyp == 2) {
|
||||||
|
if (widget.unlimitedUnswipe) {
|
||||||
|
_lastCards.removeLast();
|
||||||
|
} else {
|
||||||
|
_lastCard = null;
|
||||||
|
}
|
||||||
|
_isUnswiping = false;
|
||||||
|
}
|
||||||
|
_animationController.reset();
|
||||||
|
_left = 0;
|
||||||
|
_top = 0;
|
||||||
|
_total = 0;
|
||||||
|
_angle = 0;
|
||||||
|
_scale = 0.9;
|
||||||
|
_difference = 40;
|
||||||
|
_swipeTyp = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
super.dispose();
|
||||||
|
_animationController.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
return Container(
|
||||||
|
padding: widget.padding,
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (BuildContext context, BoxConstraints constraints) {
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
...widget.cards!
|
||||||
|
.asMap()
|
||||||
|
.map((index, _) {
|
||||||
|
return MapEntry(
|
||||||
|
index,
|
||||||
|
_item(constraints, index),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.values
|
||||||
|
.toList(),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _item(BoxConstraints constraints, int index) {
|
||||||
|
if (index != widget.cards!.length - 1) {
|
||||||
|
return Visibility(
|
||||||
|
visible: widget.cards!.length - index <= 2,
|
||||||
|
child: Positioned(
|
||||||
|
top: _difference,
|
||||||
|
left: 0,
|
||||||
|
child: Container(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: _scale,
|
||||||
|
child: Container(
|
||||||
|
constraints: constraints,
|
||||||
|
child: widget.cards![index],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: _left,
|
||||||
|
top: _top,
|
||||||
|
child: GestureDetector(
|
||||||
|
child: Transform.rotate(
|
||||||
|
angle: _angle,
|
||||||
|
child: Container(
|
||||||
|
constraints: constraints,
|
||||||
|
child: widget.cards![index],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
if (widget.isDisabled) {
|
||||||
|
widget.onTapDisabled();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPanStart: (tapInfo) {
|
||||||
|
if (!widget.isDisabled) {
|
||||||
|
RenderBox renderBox = context.findRenderObject() as RenderBox;
|
||||||
|
Offset position = renderBox.globalToLocal(tapInfo.globalPosition);
|
||||||
|
|
||||||
|
if (position.dy < renderBox.size.height / 2) _tapOnTop = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPanUpdate: (tapInfo) {
|
||||||
|
if (!widget.isDisabled) {
|
||||||
|
setState(() {
|
||||||
|
_left += tapInfo.delta.dx;
|
||||||
|
_top += tapInfo.delta.dy;
|
||||||
|
_total = _left + _top;
|
||||||
|
_calculateAngle();
|
||||||
|
_calculateScale();
|
||||||
|
_calculateDifference();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onPanEnd: (tapInfo) {
|
||||||
|
if (!widget.isDisabled) {
|
||||||
|
_tapOnTop = false;
|
||||||
|
_onEndAnimation();
|
||||||
|
_animationController.forward();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculateAngle() {
|
||||||
|
if (_angle <= _maxAngle && _angle >= -_maxAngle) {
|
||||||
|
(_tapOnTop == true)
|
||||||
|
? _angle = (_maxAngle / 100) * (_left / 10)
|
||||||
|
: _angle = (_maxAngle / 100) * (_left / 10) * -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculateScale() {
|
||||||
|
if (_scale <= 1.0 && _scale >= 0.9) {
|
||||||
|
_scale =
|
||||||
|
(_total > 0) ? 0.9 + (_total / 5000) : 0.9 + -1 * (_total / 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _calculateDifference() {
|
||||||
|
if (_difference >= 0 && _difference <= _difference) {
|
||||||
|
_difference = (_total > 0) ? 40 - (_total / 10) : 40 + (_total / 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEndAnimation() {
|
||||||
|
if (_left < -widget.threshold || _left > widget.threshold) {
|
||||||
|
_swipeHorizontal(context);
|
||||||
|
} else if (_top < -widget.threshold || _top > widget.threshold) {
|
||||||
|
_swipeVertical(context);
|
||||||
|
} else {
|
||||||
|
_goBack(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//moves the card away to the left or right
|
||||||
|
void _swipeHorizontal(BuildContext context) {
|
||||||
|
setState(() {
|
||||||
|
_swipeTyp = 1;
|
||||||
|
_leftAnimation = Tween<double>(
|
||||||
|
begin: _left,
|
||||||
|
end: (_left == 0)
|
||||||
|
? (widget.direction == AppinioSwiperDirection.right)
|
||||||
|
? MediaQuery.of(context).size.width
|
||||||
|
: -MediaQuery.of(context).size.width
|
||||||
|
: (_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);
|
||||||
|
});
|
||||||
|
if (_left > widget.threshold ||
|
||||||
|
_left == 0 && widget.direction == AppinioSwiperDirection.right) {
|
||||||
|
_swipedDirectionHorizontal = 1;
|
||||||
|
detectedDirection = AppinioSwiperDirection.right;
|
||||||
|
} else {
|
||||||
|
_swipedDirectionHorizontal = -1;
|
||||||
|
detectedDirection = AppinioSwiperDirection.left;
|
||||||
|
}
|
||||||
|
(_top <= 0) ? _swipedDirectionVertical = 1 : _swipedDirectionVertical = -1;
|
||||||
|
_horizontal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//moves the card away to the top or bottom
|
||||||
|
void _swipeVertical(BuildContext context) {
|
||||||
|
setState(() {
|
||||||
|
_swipeTyp = 1;
|
||||||
|
_leftAnimation = Tween<double>(
|
||||||
|
begin: _left,
|
||||||
|
end: _left + _left,
|
||||||
|
).animate(_animationController);
|
||||||
|
_topAnimation = Tween<double>(
|
||||||
|
begin: _top,
|
||||||
|
end: (_top == 0)
|
||||||
|
? (widget.direction == AppinioSwiperDirection.bottom)
|
||||||
|
? MediaQuery.of(context).size.height
|
||||||
|
: -MediaQuery.of(context).size.height
|
||||||
|
: (_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);
|
||||||
|
});
|
||||||
|
if (_top > widget.threshold ||
|
||||||
|
_top == 0 && widget.direction == AppinioSwiperDirection.bottom) {
|
||||||
|
_swipedDirectionVertical = -1;
|
||||||
|
detectedDirection = AppinioSwiperDirection.bottom;
|
||||||
|
} else {
|
||||||
|
_swipedDirectionVertical = 1;
|
||||||
|
detectedDirection = AppinioSwiperDirection.top;
|
||||||
|
}
|
||||||
|
(_left >= 0)
|
||||||
|
? _swipedDirectionHorizontal = 1
|
||||||
|
: _swipedDirectionHorizontal = -1;
|
||||||
|
_vertical = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
//moves the card back to starting position
|
||||||
|
void _goBack(BuildContext context) {
|
||||||
|
setState(() {
|
||||||
|
_swipeTyp = 3;
|
||||||
|
_leftAnimation = Tween<double>(
|
||||||
|
begin: _left,
|
||||||
|
end: 0,
|
||||||
|
).animate(_animationController);
|
||||||
|
_topAnimation = Tween<double>(
|
||||||
|
begin: _top,
|
||||||
|
end: 0,
|
||||||
|
).animate(_animationController);
|
||||||
|
_scaleAnimation = Tween<double>(
|
||||||
|
begin: _scale,
|
||||||
|
end: 0.9,
|
||||||
|
).animate(_animationController);
|
||||||
|
_differenceAnimation = Tween<double>(
|
||||||
|
begin: _difference,
|
||||||
|
end: 40,
|
||||||
|
).animate(_animationController);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//unswipe the card: brings back the last card that was swiped away
|
||||||
|
void _unswipe(AppinioUnswipeCard card) {
|
||||||
|
_isUnswiping = true;
|
||||||
|
widget.cards!.add(card.widget);
|
||||||
|
_swipeTyp = 2;
|
||||||
|
//unSwipe horizontal
|
||||||
|
if (card.horizontal == true) {
|
||||||
|
_unSwipeLeftAnimation = Tween<double>(
|
||||||
|
begin: (card.swipedDirectionHorizontal == 1)
|
||||||
|
? MediaQuery.of(context).size.width
|
||||||
|
: -MediaQuery.of(context).size.width,
|
||||||
|
end: 0,
|
||||||
|
).animate(_animationController);
|
||||||
|
_unSwipeTopAnimation = Tween<double>(
|
||||||
|
begin: (card.swipedDirectionVertical == 1)
|
||||||
|
? -MediaQuery.of(context).size.height / 4
|
||||||
|
: MediaQuery.of(context).size.height / 4,
|
||||||
|
end: 0,
|
||||||
|
).animate(_animationController);
|
||||||
|
_scaleAnimation = Tween<double>(
|
||||||
|
begin: 1.0,
|
||||||
|
end: _scale,
|
||||||
|
).animate(_animationController);
|
||||||
|
_differenceAnimation = Tween<double>(
|
||||||
|
begin: 0,
|
||||||
|
end: _difference,
|
||||||
|
).animate(_animationController);
|
||||||
|
}
|
||||||
|
//unSwipe vertical
|
||||||
|
if (card.vertical == true) {
|
||||||
|
_unSwipeLeftAnimation = Tween<double>(
|
||||||
|
begin: (card.swipedDirectionHorizontal == 1)
|
||||||
|
? MediaQuery.of(context).size.width / 4
|
||||||
|
: -MediaQuery.of(context).size.width / 4,
|
||||||
|
end: 0,
|
||||||
|
).animate(_animationController);
|
||||||
|
_unSwipeTopAnimation = Tween<double>(
|
||||||
|
begin: (card.swipedDirectionVertical == 1)
|
||||||
|
? -MediaQuery.of(context).size.height
|
||||||
|
: MediaQuery.of(context).size.height,
|
||||||
|
end: 0,
|
||||||
|
).animate(_animationController);
|
||||||
|
_scaleAnimation = Tween<double>(
|
||||||
|
begin: 1.0,
|
||||||
|
end: _scale,
|
||||||
|
).animate(_animationController);
|
||||||
|
_differenceAnimation = Tween<double>(
|
||||||
|
begin: 0,
|
||||||
|
end: _difference,
|
||||||
|
).animate(_animationController);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//for null safety
|
||||||
|
void emptyFunction() {}
|
||||||
|
void emptyFunctionIndex(int index, AppinioSwiperDirection direction) {}
|
||||||
|
void emptyFunctionBool(bool unswiped) {}
|
||||||
|
|
||||||
|
//to call the swipe or unswipe function from outside of the appinio swiper
|
||||||
|
class AppinioSwiperController extends ChangeNotifier {
|
||||||
|
AppinioSwiperState? state;
|
||||||
|
|
||||||
|
//swipe the card by changing the status of the controller
|
||||||
|
void swipe() {
|
||||||
|
state = AppinioSwiperState.swipe;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
//swipe the card to the left side by changing the status of the controller
|
||||||
|
void swipeLeft() {
|
||||||
|
state = AppinioSwiperState.swipeLeft;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
//swipe the card to the right side by changing the status of the controller
|
||||||
|
void swipeRight() {
|
||||||
|
state = AppinioSwiperState.swipeRight;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
//calls unswipe the card by changing the status of the controller
|
||||||
|
void unswipe() {
|
||||||
|
state = AppinioSwiperState.unswipe;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AppinioUnswipeCard {
|
||||||
|
Widget widget;
|
||||||
|
bool horizontal;
|
||||||
|
bool vertical;
|
||||||
|
int swipedDirectionHorizontal;
|
||||||
|
int swipedDirectionVertical;
|
||||||
|
|
||||||
|
AppinioUnswipeCard({
|
||||||
|
required this.widget,
|
||||||
|
required this.horizontal,
|
||||||
|
required this.vertical,
|
||||||
|
required this.swipedDirectionHorizontal,
|
||||||
|
required this.swipedDirectionVertical,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppinioSwiperState { swipe, swipeLeft, swipeRight, unswipe }
|
||||||
|
|
||||||
|
enum AppinioSwiperDirection { none, left, right, top, bottom }
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import 'swipeable_cards_stack.dart';
|
|
||||||
|
|
||||||
class CardsAnimation {
|
|
||||||
static Animation<Alignment> 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<Size?> 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<Alignment> 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<Size?> 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<Alignment> 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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,294 +0,0 @@
|
||||||
library swipeable_cards_stack;
|
|
||||||
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:swipeable_cards_stack/src/cards_animation.dart';
|
|
||||||
import 'package:swipeable_cards_stack/src/swipeable_cards_stack_controller.dart';
|
|
||||||
|
|
||||||
const List<Alignment> cardsAlign = [
|
|
||||||
Alignment(0.0, 1.0),
|
|
||||||
Alignment(0.0, 0.8),
|
|
||||||
Alignment(0.0, 0.0)
|
|
||||||
];
|
|
||||||
final List<Size> cardsSize = List.filled(3, const Size(1, 1));
|
|
||||||
|
|
||||||
typedef OnCardSwiped = bool Function(
|
|
||||||
AxisDirection dir, int index, Widget? widget);
|
|
||||||
|
|
||||||
class SwipeableCardsStack extends StatefulWidget {
|
|
||||||
final SwipeableCardsStackController? cardController;
|
|
||||||
|
|
||||||
//First 3 widgets
|
|
||||||
final List<Widget> items;
|
|
||||||
final OnCardSwiped? onCardSwiped;
|
|
||||||
final double cardWidthTopMul;
|
|
||||||
final double cardWidthMiddleMul;
|
|
||||||
final double cardWidthBottomMul;
|
|
||||||
final double cardHeightTopMul;
|
|
||||||
final double cardHeightMiddleMul;
|
|
||||||
final double cardHeightBottomMul;
|
|
||||||
final bool enableSwipeUp;
|
|
||||||
final bool enableSwipeDown;
|
|
||||||
final bool enableSwipeLeft;
|
|
||||||
final bool enableSwipeRight;
|
|
||||||
|
|
||||||
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.enableSwipeUp = true,
|
|
||||||
this.enableSwipeDown = true,
|
|
||||||
this.enableSwipeLeft = true,
|
|
||||||
this.enableSwipeRight = 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<SwipeableCardsStack> createState() => _SwipeableCardsStackState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SwipeableCardsStackState extends State<SwipeableCardsStack>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
int cardsCounter = 0;
|
|
||||||
int index = 0;
|
|
||||||
Widget? appendCard;
|
|
||||||
|
|
||||||
List<Widget?> 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(AxisDirection dir) {
|
|
||||||
final swipedCallback = widget.onCardSwiped ?? (_, __, ___) => true;
|
|
||||||
bool? shouldAnimate = false;
|
|
||||||
if (dir == AxisDirection.left) {
|
|
||||||
shouldAnimate = swipedCallback(AxisDirection.left, index, cards[0]);
|
|
||||||
frontCardAlign = const Alignment(-0.001, 0.0);
|
|
||||||
} else if (dir == AxisDirection.right) {
|
|
||||||
shouldAnimate = swipedCallback(AxisDirection.right, index, cards[0]);
|
|
||||||
frontCardAlign = const Alignment(0.001, 0.0);
|
|
||||||
} else if (dir == AxisDirection.up) {
|
|
||||||
shouldAnimate = swipedCallback(AxisDirection.up, index, cards[0]);
|
|
||||||
frontCardAlign = const Alignment(0.0, -0.001);
|
|
||||||
} else if (dir == AxisDirection.down) {
|
|
||||||
shouldAnimate = swipedCallback(AxisDirection.down, index, cards[0]);
|
|
||||||
frontCardAlign = const Alignment(0.0, 0.001);
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Expanded(
|
|
||||||
child: IgnorePointer(
|
|
||||||
ignoring: !enableSwipe,
|
|
||||||
child: Stack(
|
|
||||||
children: <Widget>[
|
|
||||||
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 && widget.enableSwipeRight) {
|
|
||||||
shouldAnimate =
|
|
||||||
onCardSwiped(AxisDirection.right, index, cards[0]);
|
|
||||||
} else if (frontCardAlign.x < -3.0 &&
|
|
||||||
widget.enableSwipeLeft) {
|
|
||||||
shouldAnimate =
|
|
||||||
onCardSwiped(AxisDirection.left, index, cards[0]);
|
|
||||||
} else if (frontCardAlign.y < -3.0 &&
|
|
||||||
widget.enableSwipeUp) {
|
|
||||||
shouldAnimate =
|
|
||||||
onCardSwiped(AxisDirection.up, index, cards[0]);
|
|
||||||
} else if (frontCardAlign.y > 3.0 &&
|
|
||||||
widget.enableSwipeDown) {
|
|
||||||
shouldAnimate =
|
|
||||||
onCardSwiped(AxisDirection.down, index, cards[0]);
|
|
||||||
} else {
|
|
||||||
// Return to the initial rotation and alignment
|
|
||||||
setState(() {
|
|
||||||
frontCardAlign = defaultFrontCardAlign;
|
|
||||||
frontCardRot = 0.0;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldAnimate) {
|
|
||||||
animateCards();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
typedef TriggerListener = void Function(AxisDirection dir);
|
|
||||||
typedef AppendItem = void Function(Widget item);
|
|
||||||
typedef EnableSwipe = void Function(bool dir);
|
|
||||||
|
|
||||||
class SwipeableCardsStackController {
|
|
||||||
late TriggerListener listener;
|
|
||||||
late AppendItem addItem;
|
|
||||||
late EnableSwipe enableSwipeListener;
|
|
||||||
|
|
||||||
void triggerSwipeLeft() {
|
|
||||||
return listener.call(AxisDirection.left);
|
|
||||||
}
|
|
||||||
|
|
||||||
void triggerSwipeRight() {
|
|
||||||
return listener.call(AxisDirection.right);
|
|
||||||
}
|
|
||||||
|
|
||||||
void triggerSwipeUp() {
|
|
||||||
return listener.call(AxisDirection.up);
|
|
||||||
}
|
|
||||||
|
|
||||||
void triggerSwipeDown() {
|
|
||||||
return listener.call(AxisDirection.down);
|
|
||||||
}
|
|
||||||
|
|
||||||
void appendItem(Widget item) {
|
|
||||||
return addItem.call(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
void enableSwipe(bool isSwipeEnabled) {
|
|
||||||
return enableSwipeListener.call(isSwipeEnabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
library swipeable_cards_stack;
|
|
||||||
|
|
||||||
export 'src/swipeable_cards_stack.dart';
|
|
||||||
export 'src/swipeable_cards_stack_controller.dart';
|
|
||||||
|
|
@ -66,7 +66,7 @@ packages:
|
||||||
name: lints
|
name: lints
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.1"
|
version: "2.0.0"
|
||||||
matcher:
|
matcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -150,5 +150,5 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.0 <3.0.0"
|
dart: ">=2.17.0-206.0.dev <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@ name: swipeable_cards_stack
|
||||||
description: This is Tinder like swipeable cards package. You can add your own widgets to the stack, receive all four events, left, right, up and down. You can define your own business logic for each direction.
|
description: This is Tinder like swipeable cards package. You can add your own widgets to the stack, receive all four events, left, right, up and down. You can define your own business logic for each direction.
|
||||||
homepage: https://github.com/ricardodalarme/swipeable-cards-stack
|
homepage: https://github.com/ricardodalarme/swipeable-cards-stack
|
||||||
issue_tracker: https://github.com/ricardodalarme/swipeable-cards-stack/issues
|
issue_tracker: https://github.com/ricardodalarme/swipeable-cards-stack/issues
|
||||||
version: 1.0.2+1
|
version: 2.0.0
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.12.0 <3.0.0"
|
sdk: ">=2.12.0 <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=1.17.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
|
|
@ -19,4 +19,4 @@ dev_dependencies:
|
||||||
|
|
||||||
screenshots:
|
screenshots:
|
||||||
- description: 'Example of the widget.'
|
- description: 'Example of the widget.'
|
||||||
path: images/swipe-card.gif
|
path: images/swiping.gif
|
||||||
|
|
|
||||||