Skip to content

Repository files navigation

Liquid Boarding

Cuberto's liquid swipe onboarding transition, rebuilt in Jetpack Compose.

The next page hangs off the trailing edge as a droplet. Pull it and it stretches into a wave that swallows the page you were on; let go past the threshold and it takes over. Drag in from the leading edge to pour the previous page back.

Left to right Right to left
A page dragged in from the right edge as a wave, then dragged back from the left The same flow in Arabic, with the wave entering from the left edge

Same component, same recording script. The second one is the demo running in Arabic - the wave, the arrow and the drag zones all follow the locale.

Why this exists

The original library is built on ViewPager, fragments and PorterDuff bitmap masking. It has not been touched since 2019 and no longer builds against a current toolchain.

This is not a port of that code. The wave curves are the same - those numbers are tuned by eye and worth keeping - but everything around them is rewritten: no views, no bitmaps, no fragments. Two composables clipped to a path, a state holder, and a pointer handler on the edges.

Requirements

Min SDK 24
Target / compile SDK 37
Kotlin 2.4.10
Compose BOM 2026.06.01
AGP 9.3.1
Java target 21
Layout direction left to right and right to left

Built with Gradle 9.7.0. The daemon runs on a JDK 25 toolchain that Gradle provisions itself, so no local JDK setup is needed beyond whatever runs the wrapper.

Installing

Published through JitPack. Add the repository:

// settings.gradle.kts
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") }
    }
}

The coordinate, in a version catalog:

# gradle/libs.versions.toml
[versions]
liquidswipe = "1.0.0"

[libraries]
liquidswipe = { group = "com.github.sam-a1a", name = "LiquidBoarding", version.ref = "liquidswipe" }
// app/build.gradle.kts
dependencies {
    implementation(libs.liquidswipe)
}

It pulls in Compose runtime, ui and ui-graphics as api dependencies, so a project that already has Compose needs nothing else. No Material dependency - the component draws its own arrow.

The coordinate is JitPack's rather than mine. It serves everything under com.github.<user>, and because this repo publishes a single module it names the artifact after the repository instead of the module. What you import is unaffected: the package is com.ghazaleh.liquidswipe.

Publishing somewhere else instead only changes the coordinate. The module already declares com.ghazaleh:liquidswipe, which is what ./gradlew :liquidswipe:publishToMavenLocal puts in your local repository.

Running the sample

./gradlew installDebug     # onto a connected device or emulator
./gradlew test             # wave geometry unit tests

The sample ships in English and Arabic. Switch it with per-app languages in Settings, or from a terminal:

adb shell cmd locale set-app-locales com.ghazaleh.liquidboarding --locales ar

Using the component

LiquidSwipe takes a page count and a composable that renders a page. Pages supply their own backgrounds - the transition works by moving a colour boundary, so there has to be a colour to move.

val state = rememberLiquidSwipeState()

LiquidSwipe(
    pageCount = pages.size,
    state = state,
    // The droplet wears the next page's colour, so tint the arrow to suit.
    button = { nextPage -> LiquidSwipeArrow(tint = pages[nextPage].arrowTint) },
) { page ->
    Box(
        Modifier
            .fillMaxSize()
            .background(pages[page].background)
    ) {
        // whatever this page shows
    }
}

LiquidSwipeState is the whole API surface:

Member
currentPage the page filling the screen
canGoForward, canGoBack whether there is a page either side
isSwitching true while a page change is committing
animateToNextPage() runs the same animation a finished drag would
animateToPreviousPage() the same, backwards
snapToPage(page) jumps, no transition - what the demo's skip link uses

The suspending functions are cancellable: a new gesture preempts a running animation rather than queueing behind it.

How it works

One number drives everything. Progress runs 0f to 1f. At 0f the wave is a 48dp droplet clinging to the edge; at 1f the incoming page owns the screen. WaveGeometry maps that number onto three measurements - how far the bulge reaches sideways, how tall it is, and how wide a strip of the incoming page is showing.

The bulge is a spring, not a slide. The horizontal radius grows linearly for the first 40% of the drag, then follows a damped harmonic oscillator to zero. Past the halfway point the cosine goes negative and the wave curls back on itself, which is what makes the page look like it snaps into place. The spring constants come from the original library unchanged.

The wave is one outline used twice. WavePath traces a rectangle pinned to an edge with a droplet bulging out of its side, as ten cubic segments whose control points are fractions of the two radii rather than pixel offsets. The same ten segments describe the resting droplet and a wave large enough to cover a tablet. Left and right waves differ only in which edge the rectangle is pinned to - the boundary between pages always sweeps right to left.

Clipping happens in the draw phase. Pages are ordinary composables inside a drawWithCache that rebuilds the path from one reusable Path and clips to it. A wave that moves with every frame of a drag never invalidates composition or layout, only the draw.

Details worth knowing

The system wants those edges. On gesture navigation the platform owns roughly the outer 24dp of both sides, which is exactly where the wave listens - without systemGestureExclusion a drag backs out of the app instead of turning the page. Each edge claims a 200dp strip, the most Android will hand back, and the strip follows the wave so the budget is spent where the drag starts.

A full drag does not finish the job. Dragging the entire width only opens the forward wave to 45%; the commit animation covers the rest. Without that the transition feels weightless. The threshold to commit is 40% of the width, well inside a comfortable thumb reach.

Going back has an extra beat. Running the curves in reverse gets the previous page across the viewport but leaves the sliver the resting droplet normally occupies. Swapping the page index under that sliver would flash the outgoing colour down the edge, so the last 15% of backward progress pulls it shut.

Touch slop before the wave takes hold. Otherwise tapping anything inside an edge strip - a skip link in the top corner - yanks the droplet up to it.

Right to left is a mirror, not a second implementation. Every measurement is worked out left to right and flipped at the two points where it meets the screen: the finger position coming in, and the wave outline going out. In an RTL locale the droplet hangs off the left edge, the arrow points left, and the drag zones swap - nothing else in the component knows the difference.

Layout

liquidswipe/                    the library, published as com.ghazaleh:liquidswipe
└── src/main/java/com/ghazaleh/liquidswipe/
    ├── LiquidSwipe.kt          stacks and clips the pages, places the button
    ├── LiquidSwipeState.kt     page index and the three animated values
    ├── LiquidSwipeGestures.kt  edge drag detection and the button tap
    ├── LiquidSwipeArrow.kt     the chevron drawn inside the droplet
    ├── WaveGeometry.kt         progress to radii and side width
    └── WavePath.kt             the ten cubic segments, mirrored for RTL

app/                            the sample: five onboarding pages, en and ar
└── src/main/java/com/ghazaleh/liquidboarding/onboarding/

Credits

The wave curves and the shape of the interaction are Cuberto's, from liquid-swipe-android and the iOS original liquid-swipe, both MIT.

License

MIT. See LICENSE.

About

Cuberto's liquid swipe onboarding transition, rebuilt in Jetpack Compose. One composable, one state holder, one bezier path - no views, no bitmaps, no fragments. Drag the droplet off the edge and the next page pours in behind it. RTL supported.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages