A lightweight, customizable Instagram-like Stories player for Jetpack Compose.
Supports images, videos, progress bars, gestures, titles, and custom styling.
- β¨ Features
- π¦ Installation
- π Quick Start
- β±οΈ Progress Control (Timer)
- π¨ Customization Reference
- π Full Customization Example
- π² Demo App
- π€ Contributing
- π License
- β Support
- πΌοΈ Image and video support (resources, URLs, URIs)
- π¬ Built-in video playback (
MediaPlayer+TextureView) - β±οΈ Automatic progress bar with customizable style
- π¨ Theming support (colors, typography, spacing, corner radius, alignment)
- π Gesture layer (tap left/right, swipe down to dismiss, long press to pause)
- π§ Easy to integrate, lightweight, minimal deps
- π οΈ Debug overlay to visualize gesture zones
Add Maven Central:
repositories {
mavenCentral()
}Then add the dependency:
dependencies {
implementation("io.github.xiryl:composestory:<version>")
}or by library catalog:
// inside libs.versions.toml
[versions]
composestory = <version>
[libraries]
composestory = { module = "io.github.xiryl:composestory", version.ref = "composestory" }
// then inside dependency module
implementation(libs.composestory)val state = remember {
StoryPlayerState(
stories = listOf(
StorySpec(
id = "Photo 1",
source = StorySource.ImageUrl("https://some-url.com/image.png"),
durationMs = 5000
),
StorySpec(
id = "Intro Video",
source = StorySource.VideoUri("https://some-url/coolvideo.mp4".toUri())
)
),
playerConfig = StoryPlayerConfig()
)
}
StoryPlayer(
state = state,
title = "Demo Story",
onPrev = { /* go back */ },
onNext = { /* go forward */ },
onPauseChanged = { /* handle pause */ },
onDismiss = { /* close viewer */ }
)rememberStoryTimer is built-in to manage story progress. For images, it advances the progress bar automatically; for videos, progress is driven by playback so the timer is disabled.
Show timer usage
val current = state.currentStory
val isVideo = current?.source is StorySource.VideoUri
val durationMs = current?.durationMs ?: DEFAULT_STORY_DURATION_MS
rememberStoryTimer(
currentIndex = state.currentIndex,
durationMs = if (isVideo) null else durationMs, // disable for videos
isPaused = isPaused,
onProgress = { p -> state = state.copy(progress = p) },
onCompleted = {
val next = (state.currentIndex + 1).coerceAtMost(state.stories.lastIndex)
state = state.copy(currentIndex = next, progress = 0f)
}
)| Element | Type | What it controls |
|---|---|---|
StoryProgressBarStyle |
data class | Height, gap, corner radius, track color, progress color |
StoryTitleConfig |
data class | Text style (TextStyle?) and alignment (TextAlign) |
StoryGestureZones |
data class | Tap fractions (left/right), long-press center area, swipe edges |
StoryPlayerConfig |
data class | Combines debug overlay, progress bar style, title style, gesture zones |
StorySpec |
data class | Per-story id, source, durationMs, plus contentScale, imageAlignment, contentDescription |
Show gesture customization
val customZones = StoryGestureZones(
tapLeftFraction = 0.25f,
tapRightFraction = 0.25f,
longPressCenterWidth = 0.8f,
longPressCenterHeight = 0.8f,
swipeLeftEdge = EdgeFraction(0.15f),
swipeRightEdge = EdgeFraction(0.15f),
swipeDownEdge = EdgeFraction(0.20f)
)
val config = StoryPlayerConfig(gestureZones = customZones)Show title customization
val config = StoryPlayerConfig(
titleConfig = StoryTitleConfig(
storyTitleTextStyle = MaterialTheme.typography.titleMedium,
align = TextAlign.Center
)
)Show full example (config + player + timer)
val customConfig = StoryPlayerConfig(
showDebugUi = true,
progressBarStyle = StoryProgressBarStyle(
height = 8.dp,
gap = 6.dp,
cornerRadius = 4.dp,
trackColor = Color.Gray.copy(alpha = 0.4f),
progressColor = Color.Magenta
),
gestureZones = StoryGestureZones(
tapLeftFraction = 0.25f,
tapRightFraction = 0.25f,
longPressCenterWidth = 0.8f,
longPressCenterHeight = 0.9f,
swipeLeftEdge = EdgeFraction(0.15f),
swipeRightEdge = EdgeFraction(0.15f),
swipeDownEdge = EdgeFraction(0.20f)
),
titleConfig = StoryTitleConfig(
storyTitleTextStyle = MaterialTheme.typography.titleLarge.copy(color = Color.Yellow),
align = TextAlign.Center
)
)
var uiState by remember {
mutableStateOf(
StoryPlayerState(
stories = listOf(
StorySpec(
id = "Custom 1",
source = StorySource.ImageUrl("https://picsum.photos/800/1200"),
durationMs = 4000
),
StorySpec(
id = "Custom 2",
source = StorySource.VideoUri(
"https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4".toUri()
)
)
),
playerConfig = customConfig
)
)
}
var isPaused by remember { mutableStateOf(false) }
val current = uiState.currentStory
val isVideo = current?.source is StorySource.VideoUri
val durationMs = current?.durationMs ?: DEFAULT_STORY_DURATION_MS
rememberStoryTimer(
currentIndex = uiState.currentIndex,
durationMs = if (isVideo) null else durationMs,
isPaused = isPaused,
onProgress = { p -> uiState = uiState.copy(progress = p) },
onCompleted = {
val next = (uiState.currentIndex + 1).coerceAtMost(uiState.stories.lastIndex)
uiState = uiState.copy(currentIndex = next, progress = 0f)
}
)
StoryPlayer(
state = uiState,
title = current?.id ?: "Story",
onPrev = {
val prev = (uiState.currentIndex - 1).coerceAtLeast(0)
uiState = uiState.copy(currentIndex = prev, progress = 0f)
},
onNext = {
val next = (uiState.currentIndex + 1).coerceAtMost(uiState.stories.lastIndex)
uiState = uiState.copy(currentIndex = next, progress = 0f)
},
onPauseChanged = { paused -> isPaused = paused },
onDismiss = { /* close viewer */ }
)Clone the repo and run the app/ module to try the demo:
- Preview images
- Preview video
- Custom styled stories
- Debug overlay toggle
- Fork the repo
- Create a branch:
git checkout -b feature/my-feature - Commit:
git commit -m 'Add feature' - Push:
git push origin feature/my-feature - Open a pull request
Run before submitting:
./gradlew detektDistributed under the Apache 2.0 License.
See LICENSE for details.
If you enjoy this library, please give it a star β on GitHub!