Skip to content

iOS-style App Switcher for Jetpack Compose with physics-based animations

Notifications You must be signed in to change notification settings

SoxiaLiSA/StackSwipe

Repository files navigation

StackSwipe Demo

StackSwipe

Pixel-Perfect iOS App Switcher for Jetpack Compose

FeaturesDemoInstallationUsagePhysicsLicense中文

Platform API Kotlin Compose License


Why StackSwipe?

Most Android app switcher implementations feel "off" — laggy, robotic, or just plain wrong. StackSwipe recreates the buttery-smooth iOS App Switcher experience with mathematically precise physics:

  • Geometric Series Stacking — Cards stack with natural depth convergence
  • Power-Law Parallax — Right cards move 20% faster for that signature iOS feel
  • Rubber Band Overscroll — Progressive resistance with differential card movement
  • Z-Axis Depth Sink — Cards compress in 3D space when pulling past edges
  • Spring-Based Momentum — Critically damped springs for zero-bounce settling

No dependencies. Pure Compose. ~600 lines of code.


Demo

Left edge rubber band Right edge with Z-sink Card switching

Left Edge Overscroll (Fan Out) • Right Edge Overscroll (Z-Sink) • Card Transitions


Features

Feature Description
iOS-Accurate Layout Asymmetric card positioning with geometric series on left, parallax on right
Rubber Band Physics Smooth overscroll with per-card differential movement
Z-Axis Depth Cards sink into the screen when overscrolling right edge
Title Blur Gaussian blur on titles as cards move to stack
Dark Overlay Depth shadows on stacked cards
Spring Animations Critically damped springs for natural momentum
Card ↔ Fullscreen Seamless expand/shrink transitions with bezier easing
Screenshot Capture Built-in GraphicsLayer-based screenshot store
Performance Optimized Off-screen culling, derived state, shape hoisting

Installation

Add the source files to your project:

app/src/main/java/your/package/appswitcher/
├── Models.kt              # NavRoute, BackStackEntry, AppSwitcherState
├── ScreenshotStore.kt     # Screenshot capture & storage
├── AppSwitcherCard.kt     # Individual card component
├── AppSwitcherOverlay.kt  # Main switcher overlay (~600 LOC)
└── AppSwitcherDemoScreen.kt  # Demo implementation

Usage

Basic Setup

@Composable
fun MyAppSwitcher() {
    val entries = remember {
        listOf(
            BackStackEntry(0, NavRoute.Page("Home")),
            BackStackEntry(1, NavRoute.Page("Settings")),
            BackStackEntry(2, NavRoute.Page("Profile"))
        )
    }

    val screenshotStore = remember { ScreenshotStore() }
    var selectedIndex by remember { mutableIntStateOf(2) }
    var isVisible by remember { mutableStateOf(true) }

    AppSwitcherOverlay(
        backStack = entries,
        screenshotStore = screenshotStore,
        state = AppSwitcherState(isVisible = true, selectedIndex = selectedIndex),
        onCardClick = { index -> selectedIndex = index },
        onSelectedIndexChange = { index -> selectedIndex = index },
        onDismiss = { isVisible = false }
    )
}

With Real Screenshots

// Wrap your pages with ScreenshotCapture
ScreenshotCapture(
    key = entry.screenshotKey,
    screenshotStore = screenshotStore
) {
    YourPageContent()
}

// Capture before showing switcher
LaunchedEffect(showSwitcher) {
    if (showSwitcher) {
        screenshotStore.captureAll()
    }
}

Physics

"The difference between good and great UI is in the math."

Card Positioning

Left Side — Geometric Series:

offset(d) = basePeek × (1 - decay^d) / (1 - decay)

Cards converge to a finite limit (~30% card width). Only 2-3 cards are visually distinguishable.

Right Side — Power Law Parallax:

x(i) = centerX + relPos^1.2 × rightSpacing

The 1.2 exponent makes right cards move 20% faster than linear, creating iOS's signature depth feel.

Card Positioning

Rubber Band Overscroll

Smooth Damping (no jumps):

dampedOverscroll = |overscroll| / (1 + |overscroll| × 0.8)

Differential Card Movement:

  • Left edge: weight = 0.72 + 0.55 × d/(d+0.6) — fan out effect
  • Right edge: weight = 0.65 / (d+1) — rightmost card moves most

Overscroll Weights

Z-Axis Depth Sink

When overscrolling past the right edge, cards shrink with depth:

sinkWeight = 0.3 + 0.7 × d/(d+0.8)
scale = 1 - dampedOverscroll × 0.15 × sinkWeight

Z-Axis Sink

Spring Animation

spring(
    dampingRatio = 1.0,    // Critical damping = no bounce
    stiffness = 80f        // Low stiffness = slow, floaty settle
)

Spring Damping


Parameters

Parameter Value Purpose
Card Width 66% screen Main card size
Card Corner 30dp iOS-style radius
Left Peek 22% card Visible strip of stacked cards
Left Decay 0.28 Geometric series ratio
Right Spacing 85% card Gap between right cards
Parallax Exponent 1.2 Right card speed boost
Base Friction 0.70 Normal drag damping
Edge Friction 0.6/(1+0.5x) Progressive overscroll resistance
Spring Damping 1.0 Critical (no bounce)
Spring Stiffness 80 Slow, floaty settle
Animation Duration 400ms Card ↔ fullscreen
Bezier Easing (0.17, 0.84, 0.44, 1.0) iOS-style curve

Performance

  • Off-screen culling: Only 3-5 cards rendered regardless of backstack size
  • derivedStateOf: Drag vs animation state isolation
  • Shape hoisting: RoundedCornerShape created once
  • Stable keys: Efficient Compose node reuse

License

MIT License. Use it, modify it, ship it.


Built with math, physics, and way too much attention to detail.

If iOS can do it, Compose can do it better.

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages