Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Xiryl/compose-stories

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

21 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ€– πŸ“– Compose Stories

A lightweight, customizable Instagram-like Stories player for Jetpack Compose.
Supports images, videos, progress bars, gestures, titles, and custom styling.

Detekt Status Maven Central License

Compose Stories demo 1 Compose Stories demo 2 Compose Stories demo 3


🧭 Table of Contents


✨ Features

  • πŸ–ΌοΈ 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

πŸ“¦ Installation

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)

πŸš€ Quick Start

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 */ }
)

⏱️ Progress Control with rememberStoryTimer

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)
    }
)

🎨 Customization Reference

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

Gesture Customization

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)

Title Customization

Show title customization
val config = StoryPlayerConfig(
    titleConfig = StoryTitleConfig(
        storyTitleTextStyle = MaterialTheme.typography.titleMedium,
        align = TextAlign.Center
    )
)

πŸ“– Full Customization Example

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 */ }
)

πŸ“² Demo App

Clone the repo and run the app/ module to try the demo:

  • Preview images
  • Preview video
  • Custom styled stories
  • Debug overlay toggle

🀝 Contributing

  1. Fork the repo
  2. Create a branch: git checkout -b feature/my-feature
  3. Commit: git commit -m 'Add feature'
  4. Push: git push origin feature/my-feature
  5. Open a pull request

Run before submitting:

./gradlew detekt

πŸ“œ License

Distributed under the Apache 2.0 License.
See LICENSE for details.


⭐ Support

If you enjoy this library, please give it a star ⭐ on GitHub!