morphing_sheet
A production-ready Flutter package for building spatially continuous, physics-based morphing interactions. Transform any widget — from a card, grid item, or list tile — into a fullscreen detail view with gesture-driven, deterministic animations. Zero dependencies beyond Flutter itself.
Inspired by Swiggy Instamart, Apple's container transforms, and Material Motion.
Table of Contents
- Overview
- Key Features
- Three Interaction Paradigms
- Installation
- Quick Start
- Architecture
- API Reference
- Configuration & Customization
- Animation System
- Physics & Gestures
- Performance
- Testing
- Version History
- Minimum Requirements
- License
Overview
morphing_sheet provides a unified spatial morphing system for Flutter. At its core is MorphEngine — a pure-function interpolation engine that maps a single progress value (0.0 → 1.0) to every visual property: rect, radius, elevation, blur, scale, and content crossfade. This ensures all morph transitions are deterministic (State = f(progress)), with no implicit animation widgets, no AnimatedContainer, and no rebuild storms.
The package ships with three ready-to-use interaction paradigms, each built on top of the shared core engine:
| Paradigm | Import | Best For |
|---|---|---|
| MorphingCard | morphing_surface.dart |
Single card → fullscreen (simplest API) |
| MorphingCardCarousel | morphing_card_carousel.dart |
Horizontal card carousel with vertical fullscreen morph |
| MorphingOverlay | morphing_overlay.dart |
Instamart-style: Grid item → floating card → fullscreen with sticky header |
Key Features
🎯 Core Architecture
- Single source of truth — One
AnimationControllerdrives all visual properties; no widget-level animations. - Deterministic state machine —
State = f(progress). No hidden state, no implicit UI mutations. - Pure-function engine —
MorphEngineis entirely static, side-effect free, and testable. - State-management agnostic — All controllers extend
ChangeNotifier; works with Provider, Riverpod, Bloc, or vanillaListenableBuilder. - Zero external dependencies — Only
flutter/widgets.dartandflutter/material.dart.
🎨 Visual Effects
- Rect morphing — Pixel-perfect interpolation from source widget to fullscreen.
- Corner radius animation — Rounded card → zero-radius fullscreen.
- Peak-and-drop elevation — Shadow peaks mid-transition for a physical "lift" effect.
- Background scrim — Semi-transparent barrier with configurable opacity.
- Background blur — Gaussian blur that intensifies with expansion.
- Background scale — Subtle zoom-out of underlying content during morph.
- Content crossfade — Collapsed content fades out, expanded content fades in, with configurable timing.
- Shared element transitions — Tag-based
MorphSharedwidget for per-element animation (Hero-like, scoped to container).
🖐 Gesture System
- Direction-locking — 16px threshold before axis is locked (horizontal vs. vertical).
- Rubber-band resistance — Elastic boundary feedback when dragging past limits.
- Velocity-aware snapping — Flick gestures resolve snap targets based on speed.
- Drag-to-collapse — Pull down from fullscreen to dismiss.
- Two-axis support — Simultaneous horizontal carousel swiping + vertical morph expansion.
- Back button integration —
PopScopeconsumption when expanded.
⚡ Performance
RepaintBoundaryisolation — Separate paint layers for background, scrim, morph surface, and content.- Lazy content rendering — Only builds visible content (collapsed OR expanded, never both at rest).
- No
AnimatedOpacity/AnimatedContainer— RawOpacityandTransformwidgets for zero overhead. - Opacity culling — Content layers with opacity < 0.01 are excluded from the widget tree entirely.
Three Interaction Paradigms
MorphingCard
Tap a card → it expands from its exact on-screen position to fullscreen. Drag down or press back to collapse. Renders via Flutter's Overlay for correct z-ordering.
MorphingCardCarousel
A centered card carousel with horizontal swiping between items and vertical drag-to-fullscreen. Includes a circular thumbnail selector bar. Custom CarouselScrollPhysics (no PageView physics).
MorphingOverlay
Instamart-style contextual morph: tap a grid/list item → it morphs from its exact position into a centered floating card (with a bottom thumbnail selector) → drag up to fullscreen detail (with a sticky header). Seven-phase state machine: idle → appearing → cardMode → expanding → fullscreen → collapsing → disappearing → idle.
Installation
dependencies:
morphing_sheet: ^0.6.0
// Pick the import for your use case:
import 'package:morphing_sheet/morphing_surface.dart'; // MorphingCard + MorphOverlayManager
import 'package:morphing_sheet/morphing_card_carousel.dart'; // MorphingCardCarousel
import 'package:morphing_sheet/morphing_overlay.dart'; // MorphingOverlay (grid → card → full)
Quick Start
1. MorphingCard — Simplest
import 'package:morphing_sheet/morphing_surface.dart';
class ProductGrid extends StatefulWidget {
@override
State<ProductGrid> createState() => _ProductGridState();
}
class _ProductGridState extends State<ProductGrid>
with SingleTickerProviderStateMixin {
late final MorphController _controller;
@override
void initState() {
super.initState();
_controller = MorphController(vsync: this);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MorphingFrame(
controller: _controller,
child: Scaffold(
body: MorphingCard(
controller: _controller,
collapsedBuilder: (context, progress) => ProductCard(product),
expandedBuilder: (context, progress) => ProductDetailPage(product),
duration: const Duration(milliseconds: 450),
dragToCollapse: true,
),
),
);
}
}
2. MorphingCardCarousel
import 'package:morphing_sheet/morphing_card_carousel.dart';
MorphingCardCarousel<Product>(
items: products,
cardBuilder: (context, item, progress, index, isActive) {
return ProductCard(item: item);
},
fullscreenBuilder: (context, item, progress) {
return ProductDetail(item: item);
},
thumbnailBuilder: (context, item, isActive) {
return Image.network(item.thumbUrl, fit: BoxFit.cover);
},
)
3. MorphingOverlay (Flagship Demo)
The premium "Instamart-style" contextual morph. Tap a grid/list item → it morphs from its exact position into a centered floating card (with carousel swiping) → drag up to fullscreen detail.
Features a 7-phase state machine: idle → appearing → cardMode → expanding → fullscreen → collapsing → disappearing → idle.
import 'package:morphing_sheet/morphing_overlay.dart';
// 1. Create controller (requires TickerProviderStateMixin)
final controller = OverlayMorphController(
vsync: this,
flickVelocityThreshold: 500.0, // Soft, premium physics
);
// 2. Wrap your layout with MorphingOverlay
MorphingOverlay<Product>(
controller: controller,
items: products,
maxRadius: 16,
maxElevation: 20,
cardBuilder: (context, item, progress) => ProductCard(item: item, progress: progress),
detailBuilder: (context, item, progress) => ProductDetail(item: item, progress: progress),
child: CustomScrollView(
slivers: [
SliverGrid.builder(
itemCount: products.length,
itemBuilder: (context, index) {
// 3. Wrap each item with MorphingSource
return MorphingSource<Product>(
controller: controller,
item: products[index],
itemIndex: index,
child: ProductTile(products[index]),
);
},
),
],
),
)
Premium add-ons included in the example app:
HeroProductImage: Shared visual element linking the grid, card, and sticky header.CarouselIndexSelector: Bottom-drawer thumbnail scroll bar that automatically hides during morphing.- Card Lift: Elevation scaling during the transition to simulate a physical "lift" effect.
4. MorphOverlayManager — Imperative
import 'package:morphing_sheet/morphing_surface.dart';
final manager = MorphOverlayManager(vsync: this);
// On tap:
manager.show(
context: context,
collapsedBuilder: (ctx, p) => ProductTile(product),
expandedBuilder: (ctx, p) => ProductDetail(product),
);
// On dismiss:
manager.hide();
Architecture
Package Structure
lib/
├── morphing_surface.dart # Barrel: core engine + MorphingCard + MorphOverlayManager
├── morphing_card_carousel.dart # Barrel: MorphingCardCarousel + CardCarouselController
├── morphing_overlay.dart # Barrel: MorphingOverlay + OverlayMorphController
└── src/
├── core/ # Shared foundation
│ ├── morph_engine.dart # Pure-function interpolation (rect, radius, elevation, blur...)
│ ├── morph_controller.dart # Unified AnimationController wrapper + drag support
│ ├── morph_phase.dart # MorphPhase enum + MorphState immutable snapshot
│ ├── morph_origin.dart # MorphOrigin (source rect capture) + MorphOriginType
│ └── morph_shared.dart # MorphShared tag-based shared element widget
├── controller/
│ └── card_carousel_controller.dart # Carousel-specific controller (vertical + horizontal)
├── models/
│ ├── card_item.dart # CardItem<T> stable-identity data wrapper
│ └── card_state.dart # CardCarouselState + CardPhase enum
├── animations/
│ ├── card_morph_tween.dart # Card-specific interpolation helpers (delegates to MorphEngine)
│ └── rect_morph_tween.dart # Rect-based overlay interpolation helpers
├── physics/
│ └── carousel_physics.dart # CarouselScrollPhysics + CarouselSnapResolver
├── gestures/
│ └── morph_drag_controller.dart # Direction-locking + rubber-band drag handling
├── overlay/
│ ├── overlay_controller.dart # OverlayMorphController (two AnimationControllers)
│ ├── overlay_state.dart # OverlayMorphState + OverlayPhase (7 phases)
│ ├── overlay_morphing_container.dart # MorphingOverlay<T> container widget
│ ├── morphing_source.dart # MorphingSource<T> tap-capture wrapper
│ ├── overlay_gesture_layer.dart # Overlay gesture disambiguation
│ └── morph_overlay_manager.dart # Imperative MorphOverlayManager
└── widgets/
├── morphing_surface.dart # Base morph surface (rect + radius + elevation + crossfade)
├── morphing_card.dart # MorphingCard flagship widget
├── morphing_frame.dart # Background scale + blur wrapper
├── card_container.dart # MorphingCardCarousel<T> main widget
├── card_stack_layer.dart # Positioned active + adjacent card rendering
├── thumbnail_selector_bar.dart # Horizontally scrollable circular thumbnails
├── gesture_layer.dart # Carousel gesture disambiguation
└── background_layer.dart # Scale + blur + barrier background
Total: 26 source files | 8 test files | 4 example apps
Layer Responsibilities
| Layer | Purpose |
|---|---|
| Core | MorphEngine (pure math), MorphController (animation driver), MorphPhase/MorphState (state machine), MorphOrigin (geometry capture), MorphShared (shared elements) |
| Models | Immutable state snapshots (CardCarouselState, OverlayMorphState), identity wrappers (CardItem<T>) |
| Animations | Per-paradigm interpolation helpers (CardMorphTween, RectMorphTween) — all delegate to MorphEngine |
| Physics | CarouselScrollPhysics (custom snap physics), CarouselSnapResolver (deterministic index resolution) |
| Gestures | MorphDragController (direction-locking, rubber-band), CardGestureLayer, OverlayGestureLayer |
| Controllers | CardCarouselController, OverlayMorphController — paradigm-specific state management |
| Widgets | Presentation: MorphingSurface, MorphingCard, MorphingFrame, MorphingCardCarousel, MorphingOverlay |
Core Engine — MorphEngine
The engine is an abstract final class with only static methods. Every visual property is a pure function of progress:
// Rect geometry
Rect rect = MorphEngine.morphRect(progress, startRect, endRect);
double left = MorphEngine.morphLeft(progress, startLeft, endLeft);
double top = MorphEngine.morphTop(progress, startTop, endTop);
double width = MorphEngine.morphWidth(progress, startWidth, endWidth);
double height = MorphEngine.morphHeight(progress, startHeight, endHeight);
// Visual properties
double radius = MorphEngine.morphRadius(progress, startRadius: 16, endRadius: 0);
double elevation = MorphEngine.morphElevation(progress, peakElevation: 16); // peaks at 0.5
double scrim = MorphEngine.morphScrim(progress, maxOpacity: 0.5);
double blur = MorphEngine.morphBlur(progress, maxBlur: 6.0);
double scale = MorphEngine.morphScale(progress, minScale: 0.95);
// Content crossfade
double sourceAlpha = MorphEngine.sourceContentOpacity(progress); // 1.0 → 0.0
double targetAlpha = MorphEngine.targetContentOpacity(progress); // 0.0 → 1.0
State Machine
MorphPhase (core — used by MorphingCard and MorphOverlayManager):
collapsed → expanding → expanded → collapsing → collapsed
↕
dragging
CardPhase (carousel):
collapsedCard → transitioningToFull → fullscreen → transitioningToCard → collapsedCard
OverlayPhase (overlay — 7 phases):
idle → appearing → cardMode → expanding → fullscreen → collapsing → cardMode → disappearing → idle
Controllers
| Controller | Extends | AnimationControllers | Used By |
|---|---|---|---|
MorphController |
ChangeNotifier |
1 | MorphingCard, MorphOverlayManager, MorphingFrame |
CardCarouselController |
ChangeNotifier |
1 | MorphingCardCarousel |
OverlayMorphController |
ChangeNotifier |
2 (appear + expand) | MorphingOverlay |
API Reference
MorphController API
// Lifecycle
final controller = MorphController(
vsync: this,
expandDuration: const Duration(milliseconds: 420),
collapseDuration: const Duration(milliseconds: 350),
expandCurve: Curves.easeInOutCubic,
collapseCurve: Curves.easeInOutCubic,
);
// Programmatic control
controller.expandFrom(origin: origin, targetRect: fullscreenRect);
controller.expand();
controller.collapse();
// Interactive drag
controller.startDrag();
controller.updateDrag(delta, availableHeight);
controller.endDrag(velocityPxPerSecond, availableHeight);
// Read-only state
controller.progress; // 0.0 → 1.0
controller.phase; // MorphPhase enum
controller.state; // MorphState immutable snapshot
controller.origin; // MorphOrigin?
controller.targetRect; // Rect?
controller.currentRect; // Rect? (interpolated at current progress)
controller.isCollapsed; // bool
controller.isExpanded; // bool
controller.isAnimating; // bool
controller.isDragging; // bool
CardCarouselController API
final ctrl = CardCarouselController(vsync: this, itemCount: items.length);
// Vertical morph
ctrl.expand();
ctrl.collapse();
ctrl.onVerticalDragStart();
ctrl.onVerticalDragUpdate(delta, availableHeight);
ctrl.onVerticalDragEnd(velocity, availableHeight);
// Horizontal carousel
ctrl.setIndex(index);
ctrl.animateToIndex(index);
ctrl.resolveHorizontalSnap(velocity: v, displacement: d, cardWidth: w);
// State
ctrl.verticalProgress; // 0.0 → 1.0
ctrl.currentIndex; // int
ctrl.phase; // CardPhase
ctrl.state; // CardCarouselState
OverlayMorphController API
// Requires TickerProviderStateMixin (two AnimationControllers)
final ctrl = OverlayMorphController(vsync: this);
// Selection
ctrl.selectItem(item, itemIndex: 0, originRect: rect, screenSize: size);
ctrl.dismiss();
// Vertical expand/collapse
ctrl.expand();
ctrl.collapse();
// Horizontal carousel
ctrl.setIndex(index);
ctrl.onHorizontalDragUpdate(delta);
ctrl.onHorizontalDragEnd(velocity, cardWidth);
// Back button
ctrl.handleBackButton(); // returns bool (consumed or not)
// State
ctrl.state; // OverlayMorphState
ctrl.overlayProgress; // appear/disappear: 0.0 → 1.0
ctrl.expandProgress; // card→fullscreen: 0.0 → 1.0
ctrl.currentIndex; // int
ctrl.phase; // OverlayPhase (7 phases)
ctrl.selectedItem; // Object?
ctrl.hasSelection; // bool
Configuration & Customization
MorphingCard
MorphingCard(
controller: controller,
collapsedBuilder: (context, progress) => CardContent(),
expandedBuilder: (context, progress) => DetailContent(),
duration: const Duration(milliseconds: 450), // expand/collapse duration
expandCurve: Curves.easeInOutCubic,
collapseCurve: Curves.easeInOutCubic,
borderRadius: 16.0, // source corner radius
maxElevation: 12.0, // peak shadow elevation
surfaceColor: Colors.white, // card background
dragToCollapse: true, // enable drag-down dismiss
enableSharedElements: false, // MorphShared tag animations
onExpanded: () {}, // callback
onCollapsed: () {}, // callback
)
MorphingFrame (Background Effects)
MorphingFrame(
controller: controller,
minScale: 0.94, // background scale when fully expanded
maxBlur: 5.0, // background blur intensity
child: Scaffold(...),
)
MorphingCardCarousel
MorphingCardCarousel<T>(
items: items,
cardBuilder: (context, item, progress, index, isActive) => ...,
fullscreenBuilder: (context, item, progress) => ...,
thumbnailBuilder: (context, item, isActive) => ...,
backgroundBuilder: (context, progress, index) => ...,
controller: controller, // optional external controller
cardHeightFraction: 0.75,
cardWidthFraction: 0.85,
maxRadius: 28.0,
maxElevation: 12.0,
expandDuration: const Duration(milliseconds: 350),
collapseDuration: const Duration(milliseconds: 300),
expandCurve: Curves.easeInOutCubic,
collapseCurve: Curves.easeInOutCubic,
)
MorphOverlayManager
MorphOverlayManager(
vsync: this,
borderRadius: 16.0,
maxElevation: 16.0,
scrimOpacity: 0.5,
expandDuration: const Duration(milliseconds: 420),
collapseDuration: const Duration(milliseconds: 420),
expandCurve: Curves.easeInOutCubic,
collapseCurve: Curves.easeInOutCubic,
)
Animation System
All animations are driven by pure-function interpolation. No AnimatedWidget subclasses.
Interpolation Helpers
| Class | Scope | Key Methods |
|---|---|---|
MorphEngine |
Universal | morphRect, morphRadius, morphElevation, morphScrim, morphBlur, morphScale, sourceContentOpacity, targetContentOpacity |
CardMorphTween |
Carousel | morphHeight, morphWidth, morphRadius, thumbnailOpacity, cardContentOpacity, fullscreenContentOpacity, adjacentCardScale |
RectMorphTween |
Overlay | morphRect, morphElevation (peak-drop), computeCardRect, overlayScrimOpacity, overlayBlur, overlayBackgroundScale, adjacentCardOpacity |
Content Crossfade Timing
progress: 0.0 ──── 0.35 ──── 0.4 ──── 0.7 ──── 1.0
source: ████████████████████░░░░░░░░░░░░░░░░░░░░
target: ░░░░░░░░░░░░░░░░░░██████████████████████
↑ overlap zone (0.35–0.4)
Elevation Peak-and-Drop
progress: 0.0 ─── 0.5 ─── 1.0
elevation: 4 ─── 16 ─── 0
↑ peak (mid-transition lift effect)
Physics & Gestures
CarouselScrollPhysics
Custom ScrollPhysics implementation (not PageScrollPhysics) with:
- Velocity-aware snapping — Flick above 600 px/s triggers immediate index change.
- Distance threshold — 30% card width drag triggers snap without velocity.
- Spring simulation — mass: 0.8, stiffness: 120, damping: 14.
CarouselSnapResolver
Deterministic index resolution from velocity + displacement:
int targetIndex = CarouselSnapResolver.resolve(
currentIndex: current,
velocity: velocity,
displacement: displacement,
cardWidth: width,
itemCount: count,
velocityThreshold: 600.0, // px/s
distanceThreshold: 0.3, // fraction of card width
);
MorphDragController
Enhanced drag handler wrapping MorphController:
- Direction-locking — 12px threshold before axis commit.
- Rubber-band — 0.25× input scalar at boundaries.
- Axis tracking — Exposed via
isVerticalLocked/isHorizontalLocked.
Velocity Snap Resolution (Vertical)
const velocityThreshold = 1.5; // normalized units/s
const progressThreshold = 0.5; // progress midpoint
if (normVelocity.abs() > velocityThreshold) {
shouldExpand = normVelocity > 0; // fast flick decides
} else {
shouldExpand = progress >= 0.5; // position decides
}
Performance
| Technique | Where |
|---|---|
RepaintBoundary isolation |
Background, scrim, morph surface, content layers |
| Opacity culling (< 0.01) | MorphingSurface._ContentCrossfade, MorphOverlayManager._buildEntry |
| Single-child rendering at rest | Only collapsed OR expanded content built when progress is 0.0 or 1.0 |
Raw Opacity + Transform |
No AnimatedOpacity, no AnimatedContainer |
ImageFiltered only when blur > 0.05 |
MorphingFrame.build |
IgnorePointer on invisible scrim |
MorphOverlayManager._buildEntry |
Testing
The package includes 8 test files covering:
| Test File | Coverage |
|---|---|
morph_engine_test.dart |
Pure-function interpolation correctness |
morph_controller_test.dart |
Expand, collapse, drag, velocity snap |
overlay_controller_test.dart |
7-phase overlay state machine |
rect_morph_tween_test.dart |
Rect-based interpolation helpers |
default_physics_test.dart |
Physics engine behavior |
snap_resolver_test.dart |
Deterministic snap resolution |
sheet_controller_test.dart |
Legacy sheet controller (v0.1/v0.2) |
morphing_sheet_test.dart |
Widget-level rendering |
Run tests:
flutter test
Version History
| Version | Highlights |
|---|---|
| 0.6.0 | Current — unified MorphEngine, MorphController, MorphingCard, MorphingSurface, MorphingFrame, MorphShared, MorphOverlayManager |
| 0.5.0 | Overlay Carousel Mode — two AnimationControllers, 7-phase state machine, horizontal carousel in overlay |
| 0.4.0 | Overlay Rect Mode — Instamart-style contextual morph from grid/list items |
| 0.3.0 | Card Carousel — centered card-based morph with thumbnail strip |
| 0.2.0 | List-driven mode — item selection, preview/detail layers, magnetic snap |
| 0.1.0 | Initial — bottom sheet with multi-stage snap points |
See CHANGELOG.md for detailed migration guides.
Minimum Requirements
- Flutter >= 3.10.0
- Dart >= 3.10.4
License
MIT — see LICENSE for details.
Libraries
- morphing_card_carousel
- A centered morphing card carousel with horizontal sliding cards, vertical fullscreen morph, circular thumbnail selector strip, and deterministic physics-based animation.
- morphing_overlay
- Contextual overlay-driven morphing card transitions.
- morphing_surface
- Spatial morphing system for Flutter — entry point for the core engine, MorphingCard, MorphingSurface, and overlay manager.