circular_animated_carousel 0.1.0
circular_animated_carousel: ^0.1.0 copied to clipboard
A circular, gesture-driven carousel with garland tilt, side-lift arc, focus halo, and optional entrance + nudge animations. Ideal for coupon, ticket, and card showcases.
circular_animated_carousel #
A circular, gesture-driven carousel for any widget — cards, images, tickets, custom painters. Side items lean inward like a garland (or outward like a bowl), the focused item stays visually emphasised through a focusWeight you can wire into halos, gradients, scale, or opacity, and the entrance animation streams items in from the right.
| ArcDirection.up (default) | ArcDirection.down |
|---|---|
| Side items rise above, tilt inward (smile). | Side items drop below, tilt outward (bowl). |
Videos render inline on GitHub. On pub.dev the
<video>tag is stripped — view the demos on the GitHub repo.
Install #
flutter pub add circular_animated_carousel
import 'package:circular_animated_carousel/circular_animated_carousel.dart';
Features #
- Content-agnostic —
itemBuilderreturns anyWidget. The package never assumes you're rendering cards. - Garland tilt + side-lift arc — neighbouring items tilt and rise (or drop) along a cosine arc; choose
ArcDirection.up(smile, default) orArcDirection.down(bowl). - Two entrance modes — quick displacement slide (default) or full-duration position stream (set
entranceStartOffset: -9.0). - Responsive by default — pixel dimensions are scaled to a configurable
referenceWidth(default360), so a layout tuned on a Figma artboard looks the same on every phone. - Circular wrap (
circular: true) — last item loops back to first;info.indexis auto-wrapped so builders can index data arrays directly. - Programmatic control —
CircularAnimatedCarouselControllerfornext/previous/animateTo/jumpTo, with current-index/position getters. - Autoplay with
pauseOnInteractionso the timer never fights the user's finger. - Snap mode toggle (
enableSnap: false→ free-scroll gallery; default snaps to nearest item). onTapwith tap-to-focus — tapping a side item auto-animates focus to it.- One-shot nudge bob to hint at vertical drag interactions after the entrance settles.
entranceDelayso the slide doesn't fire while a host bottom-sheet / modal is still opening.- Distance-sorted Z order — focused item always paints on top.
RepaintBoundaryper item — every card is rasterised once and cached as a GPU layer.- Stable keys so the distance sort never reorders Element identities.
Quickstart #
CircularAnimatedCarousel(
itemCount: photos.length,
circular: true,
itemBuilder: (context, info) => Image.network(photos[info.index]),
)
That's it — sensible defaults for everything else. Drag, flick, snap-to-index, garland tilt, responsive sizing, all on by default.
Usage #
Cards #
CircularAnimatedCarousel(
itemCount: coupons.length,
itemWidth: 200,
itemHeight: 280,
itemSpacing: 255,
circular: true,
entranceStartOffset: -9.0, // cards visibly stream from the right
entranceDelay: Duration(milliseconds: 180),
entranceDuration: Duration(milliseconds: 2400),
onIndexChanged: (i) => print('focused $i'),
itemBuilder: (context, info) => MyCouponCard(
coupon: coupons[info.index],
showHalo: info.focusWeight > 0.5,
),
)
Images #
CircularAnimatedCarousel(
itemCount: photos.length,
itemWidth: 240,
itemHeight: 320,
itemBuilder: (context, info) => ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.network(
photos[info.index],
fit: BoxFit.cover,
// Dim non-focused photos so the centred one pops.
color: Colors.black.withValues(alpha: 0.6 * (1 - info.focusWeight)),
colorBlendMode: BlendMode.darken,
),
),
)
Any custom widget #
CircularAnimatedCarousel(
itemCount: 5,
itemWidth: 160,
itemHeight: 160,
itemBuilder: (context, info) => Transform.scale(
// Side items shrink slightly; focused one is full size.
scale: 0.85 + 0.15 * info.focusWeight,
child: CustomPaint(painter: SpinningRingPainter(index: info.index)),
),
)
CarouselItemInfo #
Every itemBuilder call receives this struct. Use it to vary visuals based on how close the item is to focus.
| Field | Type | Meaning |
|---|---|---|
index |
int |
This item's index. In circular mode, pre-wrapped to [0, itemCount). |
offset |
double |
Signed distance from focus. 0.0 = centered; negative = left; positive = right. |
distance |
double |
offset.abs() — when you only care about how far. |
focusWeight |
double |
1.0 when exactly focused, falls smoothly to 0.0 at the neighbouring slot. Wire into halos, opacity, scale, glow alpha. |
Programmatic control #
final controller = CircularAnimatedCarouselController();
CircularAnimatedCarousel(
controller: controller,
itemCount: 5,
itemBuilder: (c, info) => MyCard(),
);
// Drive it from buttons / tabs / deep links:
controller.next();
controller.previous();
controller.animateTo(3);
controller.jumpTo(0);
print(controller.currentIndex); // wrapped in circular mode
print(controller.currentPosition); // fractional, live
Note: the verbose name avoids a collision with Flutter material's own CarouselController (added in 3.16 for CarouselView). Remember to dispose() it on State teardown.
Arc direction #
See the demos at the top of this README.
arcDirection: ArcDirection.up // default (smile)
arcDirection: ArcDirection.down // bowl
Both lift and tilt flip together so the geometry stays consistent — the package never produces a half-mirrored arc.
Responsive sizing #
referenceWidth: 360.0 // default — values are designed for a 360 px viewport
The package scales itemWidth, itemHeight, itemSpacing, sideLift, and nudgeAmplitude by actualViewport / referenceWidth. A layout tuned at 360 px renders ~14 % larger on a 411 px Pixel and ~11 % smaller on a 320 px Android Go device — proportions stay identical.
Pass null to opt out and use raw logical pixels.
Spacing #
Two ways to control the gap between adjacent items — pick one:
viewportFraction: 0.65 // gap = viewport * 0.65 (proportional, default)
// OR
itemSpacing: 255 // absolute pixels (scaled by referenceWidth)
// wins over viewportFraction when set
Autoplay + free-scroll + tap #
CircularAnimatedCarousel(
autoplay: true,
autoplayInterval: Duration(seconds: 3),
pauseOnInteraction: true, // resumes when the user lets go
enableSnap: false, // free-scroll gallery (no snap to nearest)
onTap: (i) => print('tapped $i'), // non-focused taps also animate focus to that item
itemBuilder: ...,
)
Continuous interpolation across focus #
onPositionChanged fires on every drag tick, snap tick, and position-stream entrance tick — use it when you need an effect that blends between items mid-drag (e.g. a rim glow that interpolates between two adjacent palettes).
onPositionChanged: (pos) {
final lower = palette[pos.floor() % palette.length];
final upper = palette[pos.ceil() % palette.length];
glow.value = Color.lerp(lower, upper, pos - pos.floor())!;
},
For coarser "the user landed on index 3" reactions, use onIndexChanged instead — it only fires on settled snaps.
Example #
A runnable demo lives in example/ — five gradient quote cards on a dark canvas, position-stream entrance, circular wrap. Run it:
cd example && flutter run
Roadmap #
axis: Axisfor vertical carousels.- Configurable spring physics on snap.
- Vertical drag callbacks for "drag to claim" patterns.
- Accessibility: semantics labels, screen reader announcements, keyboard navigation.
- Golden tests.
License #
MIT — see LICENSE.