Skip to content

Latest commit

 

History

History
139 lines (107 loc) · 5.27 KB

README.md

File metadata and controls

139 lines (107 loc) · 5.27 KB

Underlay

Maven Central CI

Русская версия

Blurred backdrop for dialogs and overlays that live in their own window, down to API 23.

The sample on a Galaxy S25: a dialog, a popup, a bottom sheet and a View dialog over a blurred feed, then a translucent activity and an in-window overlay drawing the fallback

Dialog(
    onDismissRequest = ::dismiss,
    properties = DialogProperties(usePlatformDefaultWidth = false, decorFitsSystemWindows = false),
) {
    Box(
        modifier = Modifier
            .fillMaxSize()
            .blurredUnderlay(
                blurRadius = 16.dp,
                tint = Color.Black.copy(alpha = 0.3f),
                fallback = MaterialTheme.colorScheme.surface,
            ),
    ) {
        // dialog content
    }
}

Why

A Dialog is a separate window, so Modifier.blur() inside it can't reach the activity behind (platform issue; Compose UI 1.13 adds blurBehindRadius, which is the API 31+ flag below). Underlay blurs that window: FLAG_BLUR_BEHIND on API 31+, a blurred snapshot otherwise. Unlike haze, it needs no marked Compose subtree: the screen behind can be Views or code you don't own, and the overlay a View Dialog or a PopupWindow.

Details: how it works, the article.

Install

dependencies {
    implementation("com.timkrest:underlay:0.5.0")
}

minSdk 23, Java 11 bytecode. Compose runtime and UI come in as api.

Sources

Source When What is drawn
SystemBlur API 31+, own window, cross-window blur on The system blurs behind the window, the tint on top
Snapshot A host window to capture The host window, captured, downscaled ×4 and blurred, under the tint
Pending A capture is in flight The tint only
Fallback No own window, no host window, or the capture failed The fallback color

Battery saver turns cross-window blur off, moving open overlays to Snapshot. Samsung Galaxy phones and some other devices lack it and always use Snapshot.

State and refresh

val underlay = rememberUnderlayState()

Box(
    modifier = Modifier.blurredUnderlay(
        blurRadius = 16.dp,
        tint = Color.Black.copy(alpha = 0.3f),
        fallback = MaterialTheme.colorScheme.surface,
        state = underlay,
        onSourceChange = { source -> Log.d("underlay", "$source") },
    ),
) {
    Text("${underlay.source}")
    Button(onClick = { underlay.refresh() }) { Text("Refresh") }
}

Snapshot is retaken when the host resizes, on refresh() and, in Compose, on a configuration change (with Views, call refresh()). liveSnapshot = true retakes it on every host frame, a couple of frames behind, for a capture and a blur per frame.

Views

class FrostedDialogFragment : DialogFragment(R.layout.frosted_dialog) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        dialog?.window?.setBackgroundDrawable(Color.TRANSPARENT.toDrawable())
        dialog?.window?.setDimAmount(0f)
        view.blurredUnderlay(
            blurRadius = 16f * resources.displayMetrics.density,
            tint = 0x4D000000,
            fallback = requireContext().getColor(R.color.surface),
        )
    }
}

The underlay becomes the view's background, radius in pixels. ViewUnderlay exposes source, refresh(), setters and remove(), which restores the old background. Call it on your own view, not a decorView or a BottomSheetDialog sheet.

Limits

  • Apply the modifier once per window, to a composable that fills it: SystemBlur covers the whole window, Snapshot only the composable. A Compose Dialog window isn't full-screen by default; the DialogProperties in the snippet above make it so.
  • The snapshot is always of the activity window, even under a dialog opened over another dialog.
  • A translucent activity has no host window: without SystemBlur it draws fallback. An overlay inside the activity window always does; use an in-window blur library there.
  • A FLAG_SECURE host is captured only into a secure overlay window; otherwise Snapshot gives way to Fallback. Compose Dialog and Popup inherit the flag by default.

Compatibility

Every build checks the public API against underlay/api/underlay.api. Patch releases don't break it; before 1.0.0 a minor release may, and the changelog says so. A new parameter on a function taking Dp or Color changes its mangled JVM name and needs a rebuild.

Sample

One overlay per window kind, with the active source and a radius slider:

./gradlew :sample:installDebug

Contributing

CONTRIBUTING.md. The most useful report is which source your device reached. Contributions are covered by the CLA.

License

Apache 2.0, see LICENSE. Vulnerabilities go through SECURITY.md.