{"id":19521813,"url":"https://github.com/smarttoolfactory/compose-screenshot","last_synced_at":"2025-04-26T09:32:02.929Z","repository":{"id":43601725,"uuid":"482527882","full_name":"SmartToolFactory/Compose-Screenshot","owner":"SmartToolFactory","description":"🚀📸  Screenshot Composables and convert to Bitmap or ImageBitmap on user action or periodically.","archived":false,"fork":false,"pushed_at":"2022-10-19T18:09:09.000Z","size":12421,"stargazers_count":159,"open_issues_count":3,"forks_count":11,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-04T10:26:24.871Z","etag":null,"topics":["android","jetpack-compose","screenshot"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/SmartToolFactory.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-04-17T13:17:05.000Z","updated_at":"2025-04-04T00:28:12.000Z","dependencies_parsed_at":"2023-01-20T05:16:17.044Z","dependency_job_id":null,"html_url":"https://github.com/SmartToolFactory/Compose-Screenshot","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FCompose-Screenshot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FCompose-Screenshot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FCompose-Screenshot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/SmartToolFactory%2FCompose-Screenshot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/SmartToolFactory","download_url":"https://codeload.github.com/SmartToolFactory/Compose-Screenshot/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250967132,"owners_count":21515543,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["android","jetpack-compose","screenshot"],"created_at":"2024-11-11T00:35:08.432Z","updated_at":"2025-04-26T09:31:58.711Z","avatar_url":"https://github.com/SmartToolFactory.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Compose ScreenshotBox\n\n[![](https://jitpack.io/v/SmartToolFactory/Compose-Screenshot.svg)](https://jitpack.io/#SmartToolFactory/Compose-Screenshot)\n\nScreenshot Composables and convert to Bitmap on user action or periodically.\n\n| Screenshot with State| Single Screenshot | Periodic Screenshot |\n| ----------|-----------| -----------|\n| \u003cimg src=\"./art/screenshot.gif\"/\u003e | \u003cimg src=\"./art/screenshot2.gif\"/\u003e | \u003cimg src=\"./art/periodic_screenshot.gif\"/\u003e |\n\n## Gradle Setup\n\nTo get a Git project into your build:\n\n* Step 1. Add the JitPack repository to your build file Add it in your root build.gradle at the end\n  of repositories:\n\n```\nallprojects {\n  repositories {\n      ...\n      maven { url 'https://jitpack.io' }\n  }\n}\n```\n\n* Step 2. Add the dependency\n\n```\ndependencies {\n    implementation 'com.github.SmartToolFactory:Compose-Screenshot:Tag'\n}\n```\n\n## Implementation\n\n### Single Shot\n\nCreate a `ScreenshotBox` which covers your Composables you want to take screenshot of\n\n```kotlin\nScreenshotBox(screenshotState = screenshotState) {\n    Column(\n        modifier = Modifier\n            .border(2.dp, Color.Green)\n            .padding(5.dp)\n    ) {\n\n        Image(\n            bitmap = ImageBitmap.imageResource(\n                LocalContext.current.resources,\n                R.drawable.landscape\n            ),\n            contentDescription = null,\n            modifier = Modifier\n                .background(Color.LightGray)\n                .fillMaxWidth()\n                // This is for displaying different ratio, optional\n                .aspectRatio(4f / 3),\n            contentScale = ContentScale.Crop\n        )\n\n        Text(text = \"Counter: $counter\")\n        Slider(value = progress, onValueChange = { progress = it })\n    }\n}\n```\n\nProvide a `ScreenshotState` which stores Bitmap\n\n```\nval screenshotState = rememberScreenshotState()\n```\n\nTake screenshot by calling `screenshotState.capture()`\n\n```kotlin\nButton(\n    onClick = {\n        screenshotState.capture()\n    }\n) {\n    Text(text = \"Take Screenshot\")\n}\n```\n\nGet `Bitmap` or `ImageBitmap` as\n\n```kotlin\nscreenshotState.imageBitmap?.let {\n    Image(\n        modifier = Modifier\n            .width(200.dp)\n            .height(150.dp),\n        bitmap = it,\n        contentDescription = null\n    )\n}\n```\n\ninitially `Bitmap` is null because `onGloballyPositioned` might not return correct coordinates\ninitially, experienced this with `Pager` first few calls return incorrect position then actual\nposition is returned, or sometimes width or height is returned zero, nullable makes sure that you\nget the latest one after calling `screenshotState.capture()` from a Composable that is laid out.\n\n### Success or Error State\n\nImageResult Sealed class return data as Bitmap or Exception if you are interested in displaying\nerror result if any has occurred\n\n```kotlin\nsealed class ImageResult {\n    object Initial : ImageResult()\n    data class Error(val exception: Exception) : ImageResult()\n    data class Success(val data: Bitmap) : ImageResult()\n}\n```\n\nImageState of `ScreenshotState` has\n`val imageState = mutableStateOf\u003cImageResult\u003e(ImageResult.Initial)` that can be observed as\n\n```kotlin\nwhen (imageResult) {\n    is ImageResult.Success -\u003e {\n        Image(bitmap = imageResult.data.asImageBitmap(), contentDescription = null)\n    }\n    is ImageResult.Error -\u003e {\n        Text(text = \"Error: ${imageResult.exception.message}\")\n    }\n    else -\u003e {}\n}\n```\n\n### Periodic Screenshot\n\nCollect `screenshotState.liveScreenshotFlow` to get periodic screenshots of your composables with\n\n```kotlin\nLaunchedEffect(Unit) {\n    screenshotState.liveScreenshotFlow\n        .onEach { bitmap: ImageBitmap -\u003e\n            imageBitmap = bitmap\n        }\n        .launchIn(this)\n}\n```\n\n## ScreenshotState\n\nSet a delay after each shot by setting `delayInMillis`\n\n```kotlin\n/**\n * Create a State of screenshot of composable that is used with that is kept on each recomposition.\n * @param delayInMillis delay before each screenshot if [liveScreenshotFlow] is collected.\n */\n@Composable\nfun rememberScreenshotState(delayInMillis: Long = 20) = remember {\n        ScreenshotState(delayInMillis)\n    }\n\n/**\n * State of screenshot of composable that is used with.\n * @param timeInMillis delay before each screenshot if [liveScreenshotFlow] is collected.\n */\nclass ScreenshotState internal constructor(\n    private val timeInMillis: Long = 20,\n) {\n    val imageState = mutableStateOf\u003cImageResult\u003e(ImageResult.Initial)\n\n    internal var callback: (() -\u003e Unit)? = null\n\n    /**\n     * Captures current state of Composables inside [ScreenshotBox]\n     */\n    fun capture() {\n        callback?.invoke()\n    }\n\n    val liveScreenshotFlow = flow {\n        while (true) {\n            callback?.invoke()\n            delay(timeInMillis)\n            bitmapState.value?.let {\n                emit(it)\n            }\n        }\n    }\n        .map {\n            it.asImageBitmap()\n        }\n        .flowOn(Dispatchers.Default)\n\n    internal val bitmapState = mutableStateOf\u003cBitmap?\u003e(null)\n\n    val bitmap: Bitmap?\n        get() = bitmapState.value\n\n    val imageBitmap: ImageBitmap?\n        get() = bitmap?.asImageBitmap()\n}\n\n```\n\n### Standalone Functions\nIf you wish to use function instead of `ScreenshotBox` you can use it as\n\n```\nval view: View = LocalView.current\n\nval imageResult:ImageResult = view.screenshot(bounds)\n```\n\nbounds is Compose rectangle that covers bounds of view that is needed to be screenshow\nwhich should be retrieved using `Modifier.onGloballyPositioned()`\n\n```\nModifier.onGloballyPositioned {\n    composableBounds = if (Build.VERSION.SDK_INT \u003e= Build.VERSION_CODES.O) {\n        it.boundsInWindow()\n    } else {\n        it.boundsInRoot()\n    }\n}\n```\n\n```kotlin\nfun View.screenshot(\n    bounds: Rect\n): ImageResult {\n\n    try {\n\n        val bitmap = Bitmap.createBitmap(\n            bounds.width.toInt(),\n            bounds.height.toInt(),\n            Bitmap.Config.ARGB_8888,\n        )\n\n        if (Build.VERSION.SDK_INT \u003e= Build.VERSION_CODES.O) {\n\n            // Above Android O not using PixelCopy throws exception\n            // https://stackoverflow.com/questions/58314397/java-lang-illegalstateexception-software-rendering-doesnt-support-hardware-bit\n            PixelCopy.request(\n                (this.context as Activity).window,\n                bounds.toAndroidRect(),\n                bitmap,\n                {},\n                Handler(Looper.getMainLooper())\n            )\n        } else {\n            val canvas = Canvas(bitmap)\n                .apply {\n                    translate(-bounds.left, -bounds.top)\n                }\n            this.draw(canvas)\n            canvas.setBitmap(null)\n        }\n        return ImageResult.Success(bitmap)\n    } catch (e: Exception) {\n        return ImageResult.Error(e)\n    }\n}\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmarttoolfactory%2Fcompose-screenshot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsmarttoolfactory%2Fcompose-screenshot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsmarttoolfactory%2Fcompose-screenshot/lists"}