morphing_sheet 0.6.2 copy "morphing_sheet: ^0.6.2" to clipboard
morphing_sheet: ^0.6.2 copied to clipboard

A production-ready Flutter package for contextual morphing containers — supporting both card carousel snapping and Instamart-style overlay card transitions.

pub version likes popularity license

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 #

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 AnimationController drives all visual properties; no widget-level animations.
  • Deterministic state machineState = f(progress). No hidden state, no implicit UI mutations.
  • Pure-function engineMorphEngine is entirely static, side-effect free, and testable.
  • State-management agnostic — All controllers extend ChangeNotifier; works with Provider, Riverpod, Bloc, or vanilla ListenableBuilder.
  • Zero external dependencies — Only flutter/widgets.dart and flutter/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 MorphShared widget 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 integrationPopScope consumption when expanded.

⚡ Performance #

  • RepaintBoundary isolation — 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 — Raw Opacity and Transform widgets 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.

3
likes
150
points
127
downloads

Publisher

unverified uploader

Weekly Downloads

A production-ready Flutter package for contextual morphing containers — supporting both card carousel snapping and Instamart-style overlay card transitions.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on morphing_sheet