lib-pag-cmp is a Compose Multiplatform wrapper for Tencent libpag.
It provides a common PagView composable and platform implementations for rendering PAG animation files.
Android and iOS targets use Tencent libpag 4.5.75.
| Platform | Status | Rendering path |
|---|---|---|
| Android | Supported | Native org.libpag.PAGView through AndroidView |
| iOS | Supported | Native PAGView through UIKitView |
| JVM Desktop | Supported on macOS arm64 and x64 | libpag offscreen render -> pixels -> Compose ImageBitmap |
| JS | Supported | libpag Web SDK canvas integration |
| WasmJS | Supported | libpag Web SDK canvas integration |
The JVM artifact currently bundles native runtime files for macos-arm64 and macos-x64.
Linux and Windows JVM native packages are not included yet.
Add the repository that contains the published artifacts, then depend on the common KMP artifact:
repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("io.github.limuyang2:lib-pag-cmp:0.1.6")
}
}
}Platform publications are produced as:
io.github.limuyang2:lib-pag-cmpio.github.limuyang2:lib-pag-cmp-androidio.github.limuyang2:lib-pag-cmp-jvmio.github.limuyang2:lib-pag-cmp-jsio.github.limuyang2:lib-pag-cmp-wasm-jsio.github.limuyang2:lib-pag-cmp-ios-arm64io.github.limuyang2:lib-pag-cmp-ios-simulator-arm64
Load a PAG file as bytes and pass it to PagView:
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.github.limuyang2.libpag.cmp.PagScaleMode
import io.github.limuyang2.libpag.cmp.PagView
@Composable
fun LoadingAnimation(bytes: ByteArray) {
PagView(
bytes = bytes,
modifier = Modifier.size(160.dp),
isPlaying = true,
repeatCount = 0,
scaleMode = PagScaleMode.LetterBox,
)
}The example above receives raw bytes. This is the most portable loading mode and works on
all supported targets.
To play a local .pag file, drop it under
src/commonMain/composeResources/files/ and read it with the Compose Multiplatform
resource API (org.jetbrains.compose.components:components-resources) — the same
path works on every target:
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.github.limuyang2.libpag.cmp.PagScaleMode
import io.github.limuyang2.libpag.cmp.PagView
import org.jetbrains.compose.resources.ExperimentalResourceApi
// `Res` is generated by the Compose resources plugin; its package follows your module setup.
import your.module.generated.resources.Res
@OptIn(ExperimentalResourceApi::class)
@Composable
fun LocalAnimation() {
var bytes by remember { mutableStateOf<ByteArray?>(null) }
LaunchedEffect(Unit) {
bytes = Res.readBytes("files/loading.pag")
}
bytes?.let {
PagView(
bytes = it,
modifier = Modifier.size(160.dp),
scaleMode = PagScaleMode.LetterBox,
)
}
}Alternatively, pass a local file path, Compose resource URI, or network URL directly with
PagView(path). The library loads it asynchronously (nothing renders until ready; a load failure
is logged, not thrown):
import androidx.compose.foundation.layout.size
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.github.limuyang2.libpag.cmp.PagScaleMode
import io.github.limuyang2.libpag.cmp.PagView
@Composable
fun RemoteAnimation(url: String) {
PagView(
path = url,
modifier = Modifier.size(160.dp),
scaleMode = PagScaleMode.LetterBox,
)
}For .pag files stored in src/commonMain/composeResources/files/, pass the URI returned by
Res.getUri("files/name.pag"):
@Composable
fun ResourcePathAnimation() {
PagView(
path = Res.getUri("files/loading_bmp.pag"),
modifier = Modifier.size(160.dp),
scaleMode = PagScaleMode.LetterBox,
)
}Do not pass the raw resource-relative path ("files/loading_bmp.pag") to PagView(path).
path must be either a platform-accessible local path/URI from Res.getUri(...), or an
http/https URL.
Network URL behavior is platform-dependent:
- Android uses libpag's native path loader.
- JVM downloads through the JDK and then renders from bytes.
- JS and WasmJS use browser
fetch, so the remote server must be reachable from the browser and allow cross-origin access. - iOS uses libpag's native path loader; network loading is best-effort.
For GitHub-hosted PAG files, prefer raw.githubusercontent.com URLs. The Web implementation also
normalizes common github.com/.../raw/... links before fetching.
On Android, iOS, and JVM, repeated rendering of the same file can load a composition once and pass
it to PagView:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import io.github.limuyang2.libpag.cmp.Pag
import io.github.limuyang2.libpag.cmp.PagView
@Composable
fun ReusedAnimation(bytes: ByteArray) {
val composition = remember(bytes) { Pag.load(bytes) }
DisposableEffect(composition) {
onDispose { composition.close() }
}
PagView(composition = composition)
}JS and WasmJS support both PagView(bytes) and PagView(path). PagView(composition) is not
supported on Web targets yet.
The demo app validates three loading paths:
ByteArray:Res.readBytes("files/8.pag")->PagView(bytes).Local path:Res.getUri("files/loading_bmp.pag")->PagView(path).Network URL: remote PAG URL ->PagView(path).
Samples are displayed in a scrollable FlowRow, so adding more cases only requires appending a
new PagSample entry in the demo data.
PagView exposes the same common parameters on every supported platform:
isPlaying: starts or pauses playback.progress: optional manual progress in0.0..1.0. When set while paused, the current frame is flushed.repeatCount: native libpag repeat count.0means infinite repeat.scaleMode: controls how PAG content fits inside the render area.cacheEnabled: enables libpag render cache.videoEnabled: enables video sequence rendering when the PAG file contains video content.useDiskCache: enables libpag disk cache on platforms that expose it.
scaleMode only affects the content transform inside the native PAG renderer. It does not measure,
resize, or otherwise affect the Compose PagView layout bounds.
None: keeps the composition at its original size and position.Stretch: scales width and height independently to fill the render area. Content may be distorted.LetterBox: scales uniformly until the whole composition is visible. Empty space may remain on one axis.Zoom: scales uniformly until the render area is fully covered. Content may be cropped on one axis.
The JS and WasmJS implementations render through the libpag Web SDK on an overlay canvas. The demo
bundles the SDK files under webApp/src/webMain/resources/pag/ and loads pag/libpag-bootstrap.js
on demand.
If your app hosts the library differently, make sure the libpag Web SDK JavaScript and wasm runtime are available at the paths used by your app. Browser network loading is subject to normal web rules: the URL must be reachable from the page and must pass CORS checks.
The JVM implementation uses a JNI bridge and bundled native libpag runtime files. At runtime, the library
extracts those files from the jar and loads them with System.load().
For local debugging, external native files can be supplied with JVM system properties:
-Dlibpag.cmp.libpag=/path/to/libpag
-Dlibpag.cmp.bridge=/path/to/libpag_cmp_jvm.dylibSee BUILD.md for details about rebuilding and replacing JVM native artifacts.
Useful verification commands:
./gradlew :lib-pag-cmp:compileAndroidMain
./gradlew :lib-pag-cmp:compileKotlinIosArm64
./gradlew :lib-pag-cmp:compileKotlinIosSimulatorArm64
./gradlew :lib-pag-cmp:compileKotlinJvm
./gradlew :lib-pag-cmp:compileKotlinJs
./gradlew :lib-pag-cmp:compileKotlinWasmJsPublication metadata can be checked with:
./gradlew :lib-pag-cmp:generatePomFileForWasmJsPublication