card_deck_view
English | 简体中文 | API reference (中文) | 0.0.2 release notes (中文)
A Flutter card deck with continuous stack animation, horizontal swiping and spring return. Build cards from any widget, control navigation programmatically, and display large lists through a small mounted window.

Desktop preview · Mobile preview · Example guide
Features
- Rear cards move forward as you drag; subtle rotation gives the stack depth.
- Distance and release velocity determine whether a card advances or springs back.
- Both swipe directions advance by default; optionally use left-next/right-previous navigation.
- Choose off-screen exits or reversible cycling behind the stack.
- Configure visible layers, spacing, rotation, shadows and physics.
- Touch, mouse, trackpad, horizontal wheel, keyboard and accessibility actions.
- Pure Flutter, with no swiper or native plugin dependency.
Getting started
Requires Flutter 3.44.0+ and Dart 3.12.2+ (<4.0.0).
After version 0.0.2 is published, install from pub.dev with flutter pub add card_deck_view, or add:
dependencies:
flutter:
sdk: flutter
card_deck_view: ^0.0.2
Run flutter pub get. To develop from this checkout before publication, use a local path dependency; the bundled example already uses path: ../. Only import the public library:
import 'package:card_deck_view/card_deck_view.dart';
Minimal example
import 'package:card_deck_view/card_deck_view.dart';
import 'package:flutter/material.dart';
void main() => runApp(const MaterialApp(home: DeckExample()));
class DeckExample extends StatelessWidget {
const DeckExample({super.key});
@override
Widget build(BuildContext context) => Scaffold(
body: Center(
child: SizedBox(
width: 300,
height: 400,
child: CardDeckView<String>(
items: const ['One', 'Two', 'Three', 'Four'],
itemBuilder: (context, item, index) => ColoredBox(
color: Colors.white,
child: Center(child: Text(item)),
),
),
),
),
);
}
Give the deck finite width and height, using a SizedBox, AspectRatio within bounded constraints, or another constrained parent. Leave space around it for rear cards, shadows and swipes. Card content and aspect ratio belong to your application.
Widget API
| Property | Default | Purpose |
|---|---|---|
items |
Required | List<T> containing the card data |
itemBuilder |
Required | (BuildContext, T, int) → Widget; index refers to the source list |
visibleCount |
3 |
Positive number of visible layers; mounts at most this number plus one buffer |
loop |
true |
Wrap to the beginning after the last card |
initialIndex |
0 |
Non-negative start index, clamped to available items |
controller |
null |
Optional CardDeckController |
itemKeyBuilder |
null |
(T) → Object returning a unique stable identity; index identity otherwise |
style |
CardDeckStyle() |
Stack appearance |
physics |
CardDeckPhysics() |
Swipe and return behavior |
swipeBehavior |
CardDeckSwipeBehavior.advanceBothDirections |
Both directions advance, or left next / right previous |
transition |
CardDeckTransition.slideOut |
Off-screen exit or cycleToBack |
onSwipe |
null |
(int, DeckSwipeDirection) → void; original index and physical swipe direction |
onChanged |
null |
(int) → void; current index after navigation or data reconciliation |
onCompleted |
null |
Called once each time a non-looping deck becomes exhausted |
emptyBuilder |
null |
Empty/exhausted state; a blank widget by default |
DeckSwipeDirection contains left and right. onSwipe reports the original index and physical swipe direction after the target index and controller have been updated, before onChanged. Each successful swipe emits each callback once; initialization, boundary returns and a single looping item emit neither. Controller previous, jump and reset do not emit onSwipe.
Reversible stack navigation
CardDeckView<TraceModel>(
items: traces,
itemKeyBuilder: (trace) => trace.id,
visibleCount: 3,
loop: true,
swipeBehavior: CardDeckSwipeBehavior.leftNextRightPrevious,
transition: CardDeckTransition.cycleToBack,
style: const CardDeckStyle(shadows: []),
onChanged: (index) {
if (index < traces.length) updateCoverAndColors(traces[index]);
},
itemBuilder: (context, trace, index) => GestureDetector(
onTap: () => openTrace(trace),
onLongPress: () => previewTrace(trace),
child: buildTraceCard(trace),
),
)
This is an application integration sketch: supply your model and handlers, and place the deck inside a finite SizedBox or constrained AspectRatio. Use shadows: [] when the card already draws its own shadow. Drive cover/color updates from onChanged, not a compensating previous() call in onSwipe.
cycleToBack sends the held top card around the swipe side into the rear. With left-next/right-previous navigation, a right swipe promotes the previous item inside the stack; it does not pull that item in from the side. The explicit controller previous() command retains the reverse entry animation. The source list is never reordered. Both options are independent, so cycling can also use both-directions-advance navigation. With leftNextRightPrevious in non-looping mode, a right swipe at the first item springs back; after exhaustion use previous() to restore the last item. Reduced motion completes immediately. Changing data, direction behavior or transition cancels pending motion.
Controller
Create the controller in your widget's State, pass it to CardDeckView(controller: controller, ...), and dispose it with that State:
final controller = CardDeckController();
@override
void dispose() {
controller.dispose();
super.dispose();
}
| Method | Behavior |
|---|---|
next() / swipeLeft() |
Always advance, using the selected transition |
swipeRight() |
Advance or return to the previous item according to swipeBehavior |
previous() |
Always go back: enter from the left with slideOut, reverse around the right with cycleToBack; wrap only when looping |
moveTo(index) |
Cancel motion and immediately position at a valid index |
reset() |
Cancel motion and return to initialIndex, clamped to current data |
Read currentIndex, isBusy and isAttached. The controller extends ChangeNotifier, so ListenableBuilder can observe changes. isBusy includes dragging and animations. New navigation commands are ignored during animations; users can catch a returning card by dragging again. Jump and reset cancel motion immediately.
Commands while detached do nothing. With a deck attached, an invalid moveTo index throws RangeError. One controller can bind to only one deck; a disposed controller cannot be reused. Binding and data updates synchronize values without notifying listeners during build; use onChanged for index changes caused by data updates.
Style and physics
Pass these objects through the widget's style and physics properties:
const style = CardDeckStyle(
spacing: 12,
scaleStep: 0.04,
rotationStep: -0.015,
dragRotation: 0.08,
maxRotation: 0.08,
borderRadius: 24,
);
const physics = CardDeckPhysics(
swipeThreshold: 0.28,
velocityThreshold: 700,
spring: SpringDescription(mass: 1, stiffness: 300, damping: 30),
swipeDuration: Duration(milliseconds: 280),
swipeCurve: Curves.easeOutCubic,
);
These are the defaults. Import package:flutter/material.dart alongside the package for SpringDescription and Curves.
spacingis the vertical distance between layers;scaleStepis the scale reduction per layer. Deep stacks clamp scale to a positive minimum.- Angles use radians.
rotationStepaffects rear layers;dragRotationmaps horizontal displacement to top-card rotation;maxRotationcaps it. shadowsaccepts a list ofBoxShadow; useconst []for none. The default is a subtle shadow.swipeThresholdis a fraction of card width, in(0, 1].velocityThresholduses logical pixels per second. Fast releases use velocity direction; slower releases use displacement.- The spring drives return motion. Duration and curve drive both transition styles. Only
slideOutuses the viewport width to determine exit distance.
Lists, empty states and images
- The deck mounts at most
visibleCount + 1cards and never repeats an item inside that window. Animation ticks update transforms without rebuilding card content. - Dragging keeps the current top card under the pointer in both directions. Only a committed backward transition reuses the forward buffer for the previous item. Only the idle top card accepts child gestures and semantics; card interaction is disabled during navigation.
- Supply a new list when data changes. With
itemKeyBuilder, the active item follows reordering; if removed, the index is clamped. Data changes cancel motion. Stable identities must be unique. - Cards remaining in the window retain their State; off-window cards are disposed. Store persistent business state outside individual card widgets.
- An empty list shows
emptyBuilder. A single looping item can be dragged and returned but does not advance. - With
loop: false, removing the last item enters the empty state.currentIndexandonChangedthen equalitems.length—check the range before indexing your list.previous()restores the last card;reset()restores the starting position.
Images are supplied through itemBuilder; the package does not download or cache them. The default example uses twelve local transparent animal illustrations, so it works offline. Its custom AnimalCard combines an image, handwritten text and a reset button. You can supply any Widget through itemBuilder. The optional PhotoCard recipe demonstrates network loading, errors, retry and decode sizing without changing the global image cache.
Input and accessibility
Horizontal touch/mouse drags and trackpad pans move the deck. Vertical gestures remain available to a parent scroll view. Horizontal wheel events accumulate until 120 ms of inactivity before release; vertical wheel events are ignored.
Focus the deck with Tab or a drag, then use left/right keys to exit in that direction. Accessibility exposes the current card number and increase/decrease actions; rear card semantics are excluded. System reduced-motion settings skip automatic travel and rebound while retaining direct drag feedback.
Examples
The surrounding interface uses monochrome text navigation and lightweight controls. The animal cards and their motion stay independent of the application chrome.
cd example
flutter pub get
flutter run -d chrome
# or: flutter run -d windows
| Page | What it demonstrates |
|---|---|
| Animal cards | 12 transparent illustrations, handwritten captions and navigation |
| Playground | Compact settings panel; collapsible on narrow screens |
| 1,000 cards | A large list reusing 12 local illustrations, with a live mounted-card count |
Playground preview · 1,000-card preview
The shared toolbar provides Previous and Next. The More actions menu contains Reset, Jump and About the artwork. Each card also has a reset button. White rounded cards, soft shadows and handwritten captions are built entirely in the example. See artwork and font provenance.
Platforms and development
The example includes Android, iOS, Web, Windows, macOS and Linux runners. Windows/Web builds and Android debug APK compilation have been verified; Web interaction and Windows startup have been smoke-tested. Android device runtime and iOS/macOS/Linux builds require additional verification on the corresponding devices/hosts. See the example guide for details and manual checks.
flutter pub get
flutter analyze
flutter test
cd example
flutter pub get
flutter analyze
flutter test
Implementation lives in lib/src/; consumers should use lib/card_deck_view.dart. The package has no CI configuration or automatic publication workflow.
License
Package source code is available under the MIT License. Example illustrations are not relicensed under MIT; see artwork and font notices.
Libraries
- card_deck_view
- Physics-driven, interactive card stacks for Flutter.