
Interactive, high-performance page-curl effect for UI: gesture-driven curl geometry, consistent shadow rendering, configurable shadow/back-page parameters, programmatic state control and demo with FPS readout.
Page curl (page turn) effect for Compose Multiplatform: Android and iOS from one Kotlin codebase.
This is a multiplatform fork of oleksandrbalan/pagecurl
(Apache-2.0). The original library is Android-only; this fork moves all gesture, state and
curl-geometry code to commonMain and keeps only the page-edge shadow platform-specific
(expect fun CacheDrawScope.prepareCurlPageShadow): Android uses the original native blur
(Paint.setShadowLayer, with upstream's software-bitmap fallback below API 28); iOS uses a Skia blur MaskFilter with the same radius→sigma model, so shadow parameters render equivalently on both platforms.
I wrote up the port in Turning the Page: a Real Page Curl on iOS with Compose Multiplatform: what actually blocked a common-code version, how the shadow seam works, and how the 60 fps claim was verified on a real device.
Create an effect of turning pages, which can be used in book reader applications, custom on-boarding screens or elsewhere, on both mobile platforms with identical behavior.
| Target | Status |
|---|---|
Android (minSdk 21) |
✅ Same behavior as upstream, native blur shadow |
iOS (iosArm64, iosSimulatorArm64) |
✅ Verified on device at a steady 60 fps |
| Android | iOS |
|---|---|
![]() |
![]() |
Both recordings show the sample app in this repo running the same commonMain composable.
Available on Maven Central:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.zeyad-37:pagecurl-cmp:2.1.0")
}
}
}Note: the classes live in the
com.zeyadgasser.pagecurlpackage (renamed from upstream'seu.wewox.pagecurlso both libraries can coexist on an Android classpath).
Provide a count of pages and content for each page, exactly like the upstream API:
@OptIn(ExperimentalPageCurlApi::class)
@Composable
fun Book(pages: List<String>) {
PageCurl(count = pages.size) { index ->
Text(pages[index])
}
}Drag from the right edge to turn a page forward, from the left edge to go back; taps on the
right / left half work too. Use rememberPageCurlState() to observe or drive the current page
programmatically, and rememberPageCurlConfig() to configure shadow color / alpha / radius /
offset, back-page color, and drag & tap interaction zones.
The sample/ directory contains one shared demo UI (sample/shared, plain commonMain
Compose) consumed by both platform apps. It includes an on-screen FPS readout, which helps
when judging curl smoothness on a real device:
./gradlew :sample:androidApp:installDebug
sample/iosApp/iosApp.xcodeproj in Xcode and run. The project is generated
from project.yml with XcodeGen; the generated
.xcodeproj is committed, so XcodeGen is only needed if you change project.yml.The legacy Android-only demo/ module from upstream has been removed; the multiplatform
sample replaces it.
Every push to cmp publishes a snapshot of the next patch version (for example 2.1.1-SNAPSHOT)
to the Central Portal snapshot repository, available within minutes. To consume one, add the
repository and point at the snapshot version:
repositories {
maven("https://central.sonatype.com/repository/maven-snapshots/") {
mavenContent { snapshotsOnly() }
}
}Snapshots are for pre-release integration testing; releases on Maven Central remain the way to depend on this library.
When iterating on a fix inside an app that consumes this library, skip the publish cycle entirely
with a Gradle composite build. In the app's settings.gradle.kts:
if (providers.gradleProperty("pagecurl.dev").isPresent) {
includeBuild(providers.gradleProperty("pagecurl.dev").get().ifEmpty { "../pagecurl" })
}Then build the app with -Ppagecurl.dev (or -Ppagecurl.dev=/path/to/pagecurl): Gradle
substitutes the published io.github.zeyad-37:pagecurl-cmp with this source checkout, so edits
here show up in the app on the next build with no publishing involved. The flag keeps CI and
other machines on the released artifact.
Forked from upstream v1.5.1. This fork starts at 2.0.0 to signal the package rename and
the multiplatform conversion; it is not binary-compatible with io.github.oleksandrbalan:pagecurl.
All of the curl mathematics, gesture handling and API design come from Oleksandr Balan's excellent pagecurl library. This fork only ports it to Compose Multiplatform.
This project is licensed under the MIT License (Copyright © 2026 Zeyad Gasser).
It is a derivative work of oleksandrbalan/pagecurl (Copyright 2022 Oleksandr Balan, Apache License 2.0). Apache-2.0 §4 permits licensing a derivative work as a whole under different terms, and its conditions are honored here: the upstream license text is retained in LICENSE-upstream-APACHE-2.0, the NOTICE file carries the attribution, and modified files carry change notices in their headers.
Page curl (page turn) effect for Compose Multiplatform: Android and iOS from one Kotlin codebase.
This is a multiplatform fork of oleksandrbalan/pagecurl
(Apache-2.0). The original library is Android-only; this fork moves all gesture, state and
curl-geometry code to commonMain and keeps only the page-edge shadow platform-specific
(expect fun CacheDrawScope.prepareCurlPageShadow): Android uses the original native blur
(Paint.setShadowLayer, with upstream's software-bitmap fallback below API 28); iOS uses a Skia blur MaskFilter with the same radius→sigma model, so shadow parameters render equivalently on both platforms.
I wrote up the port in Turning the Page: a Real Page Curl on iOS with Compose Multiplatform: what actually blocked a common-code version, how the shadow seam works, and how the 60 fps claim was verified on a real device.
Create an effect of turning pages, which can be used in book reader applications, custom on-boarding screens or elsewhere, on both mobile platforms with identical behavior.
| Target | Status |
|---|---|
Android (minSdk 21) |
✅ Same behavior as upstream, native blur shadow |
iOS (iosArm64, iosSimulatorArm64) |
✅ Verified on device at a steady 60 fps |
| Android | iOS |
|---|---|
![]() |
![]() |
Both recordings show the sample app in this repo running the same commonMain composable.
Available on Maven Central:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.zeyad-37:pagecurl-cmp:2.1.0")
}
}
}Note: the classes live in the
com.zeyadgasser.pagecurlpackage (renamed from upstream'seu.wewox.pagecurlso both libraries can coexist on an Android classpath).
Provide a count of pages and content for each page, exactly like the upstream API:
@OptIn(ExperimentalPageCurlApi::class)
@Composable
fun Book(pages: List<String>) {
PageCurl(count = pages.size) { index ->
Text(pages[index])
}
}Drag from the right edge to turn a page forward, from the left edge to go back; taps on the
right / left half work too. Use rememberPageCurlState() to observe or drive the current page
programmatically, and rememberPageCurlConfig() to configure shadow color / alpha / radius /
offset, back-page color, and drag & tap interaction zones.
The sample/ directory contains one shared demo UI (sample/shared, plain commonMain
Compose) consumed by both platform apps. It includes an on-screen FPS readout, which helps
when judging curl smoothness on a real device:
./gradlew :sample:androidApp:installDebug
sample/iosApp/iosApp.xcodeproj in Xcode and run. The project is generated
from project.yml with XcodeGen; the generated
.xcodeproj is committed, so XcodeGen is only needed if you change project.yml.The legacy Android-only demo/ module from upstream has been removed; the multiplatform
sample replaces it.
Every push to cmp publishes a snapshot of the next patch version (for example 2.1.1-SNAPSHOT)
to the Central Portal snapshot repository, available within minutes. To consume one, add the
repository and point at the snapshot version:
repositories {
maven("https://central.sonatype.com/repository/maven-snapshots/") {
mavenContent { snapshotsOnly() }
}
}Snapshots are for pre-release integration testing; releases on Maven Central remain the way to depend on this library.
When iterating on a fix inside an app that consumes this library, skip the publish cycle entirely
with a Gradle composite build. In the app's settings.gradle.kts:
if (providers.gradleProperty("pagecurl.dev").isPresent) {
includeBuild(providers.gradleProperty("pagecurl.dev").get().ifEmpty { "../pagecurl" })
}Then build the app with -Ppagecurl.dev (or -Ppagecurl.dev=/path/to/pagecurl): Gradle
substitutes the published io.github.zeyad-37:pagecurl-cmp with this source checkout, so edits
here show up in the app on the next build with no publishing involved. The flag keeps CI and
other machines on the released artifact.
Forked from upstream v1.5.1. This fork starts at 2.0.0 to signal the package rename and
the multiplatform conversion; it is not binary-compatible with io.github.oleksandrbalan:pagecurl.
All of the curl mathematics, gesture handling and API design come from Oleksandr Balan's excellent pagecurl library. This fork only ports it to Compose Multiplatform.
This project is licensed under the MIT License (Copyright © 2026 Zeyad Gasser).
It is a derivative work of oleksandrbalan/pagecurl (Copyright 2022 Oleksandr Balan, Apache License 2.0). Apache-2.0 §4 permits licensing a derivative work as a whole under different terms, and its conditions are honored here: the upstream license text is retained in LICENSE-upstream-APACHE-2.0, the NOTICE file carries the attribution, and modified files carry change notices in their headers.