Skip to content

feat(ui): Add Miuix blur and custom card backdrop blur support - #222

Merged
fanziyun merged 25 commits into
devfrom
codex/miuix-blur-port
Aug 14, 2026
Merged

feat(ui): Add Miuix blur and custom card backdrop blur support#222
fanziyun merged 25 commits into
devfrom
codex/miuix-blur-port

Conversation

@fanziyun

Copy link
Copy Markdown
Collaborator

Summary

Ports Miuix blur (frosted-glass top / bottom bars) into the Expressive UI and adds an optional custom-background card/tile blur (frosted cards over the wallpaper), including a software fallback for pre-API-33 devices.

What's included

1. Miuix blur for the app chrome (ui/blur/BlurBackdrop, BlurHost, BlurState, BlurScreenScaffold, BlurBackgroundPainter)

  • ~20 screens migrated from Scaffold to BlurScreenScaffold.
  • The master blur switch is shown on all devices and is decoupled from device capability: the software card-blur path does not require runtime shaders, so it works on API 26-32 too.

2. Custom card/tile backdrop blur (BlurredCardBackdrop.kt)

  • The custom background is pre-blurred once (downsampled 4x, StackBlur radius 45) and the matching sub-rect is drawn under each frosted card.
  • Uses a software StackBlur fallback on pre-API-33 devices (no RuntimeShader dependency).

Design decisions (and how prior review concerns are addressed)

  • minSdk-33 AAR on an API-26 app (tools:overrideLibrary in the manifest): every path into the library is gated on isRuntimeShaderSupported() (API 33+), and the AAR registers no ContentProvider / initializer, so nothing library-side executes on API 26-32. This is documented in the manifest comment.
  • Decode sizing: the shared wallpaper painter is sized to the display (ImageRequest.size(displayMetrics)), and the card-blur decode is constrained to viewport/4 (ImageRequest.size(...)) with allowHardware(false) — never a full-resolution decode.
  • Memory / bitmaps: blur runs on Dispatchers.Default; the JPEG cache write runs on Dispatchers.IO; intermediate bitmaps are recycled; ContentScale.Crop is applied everywhere.
  • On-disk blur cache: keyed by SHA-256(uri) (collision-resistant, not hashCode()), with a version + radius + downsample component so a future algorithm change cannot silently reuse stale data. Entries are count-pruned (keep 4 newest) rather than deleted on switch, so toggling between wallpapers reuses the cache. Corrupt files are deleted on decode failure; orphaned .tmp files are pruned on save.
  • Viewport resize: the anchor size is mirrored into snapshot state (LocalBlurBackgroundSize, because LayoutCoordinates.size is not observable). Re-blur is driven by a coverage check — the cached bitmap is re-blurred only when the viewport grows past it (rotation, split-screen, freeform taller, fold unfold), and skipped for shrink-only changes (IME), where the taller bitmap still samples the correct region. This avoids both stale-stretched backdrops and per-keypress re-blurs.
  • IME handling: windowSoftInputMode="adjustResize" (matches dev), so the keyboard never covers content on Android 11+; the coverage check ensures an IME height change does not re-run the blur.
  • No first-frame flash: LocalBlurredCardBackgroundEnabled is a synchronous signal, so cards stay translucent from the first frame instead of flashing opaque→translucent when the frosted backdrop arrives.

Testing

  • BlurConfigTest unit-tests the blur config → enabled-state mapping.
  • The resize / IME / cache-reuse paths are exercised on the emulator and a physical API-26 device (software fallback).

fanziyun and others added 21 commits August 11, 2026 18:37
Co-authored-by: xingguangcuican_bot <xingguangcuican821@gmail.com>
…st.kt

Co-authored-by: xingguangcuican_bot <xingguangcuican821@gmail.com>
Co-authored-by: xingguangcuican_bot <xingguangcuican821@gmail.com>
Make frosted glass visible and the software card-blur path usable on all APIs:

- Cap the bar blur tint alpha (AbkBlurTintAlpha) and keep card surfaces
  translucent when a pre-blurred custom background is present, instead of
  blending an opaque surfaceContainer over the backdrop at the default surface
  opacity, so the Blur switch has a visible effect by default.
- Decouple BlurConfig.wantsBackgroundPainter from the AGSL master switch and
  stop hiding the "render custom background into blur" toggle behind
  isBlurCapableDevice(), so API 26-32 devices can enable the StackBlur path.
- Turn off the bottom-bar backdrop and its blur pipeline while the bar is
  off-screen (child page or OOBE) to drop the invisible full-screen
  recordLayer + AGSL blur pass every frame.
- Replace per-card localToScreen/screenToLocal round-trips with
  LayoutCoordinates.localPositionOf; align blurSource/blurEffect guards to the
  actual API 33 runtime-shader capability instead of API 31.
- Debounce the card-background decode + StackBlur reload across viewport
  changes so rotation/split-screen do not re-run it per intermediate size.
- Share one background painter from AppBackgroundHost across all blur hosts
  instead of one Coil request per screen scaffold.
- Fix the surface-alpha preview state: an interrupted slider drag no longer
  sticks the in-memory preview for the session, and a late DataStore emission
  no longer yanks the slider thumb mid-drag.
- Drop dead LocalBlurredSurfaceDepth plumbing; route the extension manager
  through BlurConfig instead of duplicating its condition.
The painter-sharing change dropped the contentScale from the background
Image, so wallpapers with a mismatched aspect ratio were letterboxed with
the default Fit and the blur backdrops / card sampling could read the wrong
region of the wallpaper. Render with ContentScale.Crop to match the previous
AsyncImage behavior and the blur pipeline's crop math.

Coil's AsyncImagePainter sizes its request from the draw viewport
(size { drawSize...first() }), so the shared painter stays bounded to screen
resolution; a large user wallpaper is not decoded at its original dimensions.
Follow-up recheck of the blur subsystem (independent multi-agent audit +
manual verification):

- Fix the bottom-bar blur gate direction: navProgress is 1f when the bar is
  shown and animates to 0f when a child page hides it, so the previous
  `navProgress < 1f` gate disabled blur in the normal tab state and kept it
  running while the bar was fully off-screen. Gate on `navProgress > 0.02f`.
- Restore the master Blur switch as the overall on/off for every blur surface:
  show it on all API levels and re-tie BlurConfig.wantsBackgroundPainter to
  blurEnabled, so turning the master off disables the card blur too (it was
  decoupled in a prior commit, leaving frosted cards with opaque bars).
- Stop swallowing CancellationException and OutOfMemoryError in the card-blur
  reload: catch Exception only and rethrow CancellationException so a viewport
  restart cannot overwrite the newer effect's result with a stale one, and an
  OOM from the StackBlur buffers is not masked as a silent fallback.
- Reset the un-persisted surface-alpha preview when the app is backgrounded
  (Home/recents mid-drag never fires onValueChangeFinished), and re-check the
  dirty flag after the DataStore read to avoid stomping a newer drag.
- Clear the blur composition locals under the opaque OOBE overlay so its cards
  render opaque instead of showing a frosted backdrop.
- Only draw the background dim overlay when a wallpaper painter is present.
- Make the master blur switch description match its new master-toggle role.
Address the review comment on the shared rememberAsyncImagePainter: instead of
relying on the implicit draw-time viewport sizing, build the painter's request
from an ImageRequest explicitly sized to this host's onSizeChanged viewport, so
a large user wallpaper is decoded at screen resolution rather than its original
dimensions. The request is cached on (uri, viewportSize) so it is rebuilt only
when the wallpaper or the viewport changes, and the painter stays shared across
the visible image and every blur backdrop (no N+1 decode).
The SUSFS child page was migrated to BlurScreenScaffold but its ExpressiveTopBar
did not forward enableBlur, so its top bar stayed opaque while every other
migrated page frosted its bar. Add enableBlur = state.blurEnabled for
consistency across the UI.
Opening the soft keyboard resizes the window (the bottom bar rides above the
IME), which recomposed the wallpaper request and restarted the card-blur
loader. Both flashed to an empty/opaque state before reloading:

- AppBackgroundHost: size the shared background painter's request to the stable
  full-screen resolution (resources.displayMetrics) instead of the onSizeChanged
  viewport, so a transient IME inset change no longer re-decodes the wallpaper
  (which blanked the background while it reloaded). Full-screen is always >= any
  window viewport, so the crop stays correct.
- rememberBlurredCardBackground: keep the previous frosted backdrop while the
  replacement decodes + StackBlurs, and only swap it in once ready, instead of
  nulling it first (which flashed every card to an opaque/transparent surface).
Port ReSukiSU's approach for the software card-blur backdrop: instead of a
Composable-remembered bitmap that is re-decoded and re-blurred whenever the
viewport changes, keep the pre-blurred wallpaper in a module-level cache and
persist it to filesDir. Combined with the existing keep-until-replaced swap,
transient viewport changes (e.g. the soft keyboard resizing the window on
adjustResize) no longer flash cards opaque or re-run the full decode +
StackBlur pass; matching viewports are restored from disk on the next launch /
re-entry.
Port ReSukiSU's approach to avoiding the visible blur-loading flash: its card
alpha is a synchronous config that only depends on the custom background being
enabled, not on the async pre-blurred bitmap being ready.

Add a synchronous LocalBlurredCardBackgroundEnabled signal (feature on, bitmap
may still be decoding) and use it in blurredCardSurfaceColor and
ExpressiveListItem's drawsOwnBlurSurface instead of checking
LocalBlurredCardBackground != null. Surfaces are now translucent from the very
first frame and only gain the frosted backdrop when it lands, so opening the
app no longer flashes cards from opaque to translucent while the wallpaper
decode + StackBlur runs. OOBE clears the new signal too.
The soft keyboard opening used to resize the window (adjustResize), which shrank
the AppBackgroundHost layer and re-ran the frosted-backdrop blur for every
show/hide. Switch to adjustNothing so the window (and backdrop layer) keeps its
size and the blur is generated once per real size change:

- Manifest: adjustNothing on MainActivity and AbkExtensionManagerActivity.
- AppBackgroundHost: wrap app content in Modifier.imePadding() so content is
  lifted over the keyboard while the wallpaper/backdrop layer stays put.
- API 26-29 do not deliver full IME insets to imePadding, so those devices fall
  back to adjustResize at runtime (Android 10 and below).

Compose Dialog windows are unaffected (they keep their own UNSPECIFIED
soft-input behavior).
Co-authored-by: xingguangcuican_bot <xingguangcuican821@gmail.com>
The bot's cache fix (f8011d6) left the old delete call in place, so the
pre-blurred card cache was still deleted before being read on every launch.
Key the cache file by uri.hashCode() so a stale wallpaper's blur is never
reused across launches, and only delete the old cache on a real in-process
wallpaper switch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ain thread

- Mirror the background anchor's size into snapshot state. LayoutCoordinates.size
  is not observable, so split-screen / freeform / IME resize never re-ran the
  blur and cards drew a stale viewport stretched across the new size.
- Run saveBlurCache (JPEG compress + disk write) on Dispatchers.IO instead of
  the main thread.
- Stop resetting the nested 'render background into blur' pref when the master
  blur switch is toggled off, so the user's setting is preserved.
… switches

- Key the on-disk cache by SHA-256(uri) instead of String.hashCode(), so two
  distinct background URIs cannot collide into the same file and serve the
  wrong wallpaper's blur.
- Stop deleting the previous URI's cache on wallpaper change; switching back
  now reuses it. Bound disk usage with a count-based prune (keep 4 newest).
- Include the blur version/radius/downsample in the filename so a future
  algorithm change cannot silently reuse an incompatible cache.
…avior

- Revert windowSoftInputMode to adjustResize and drop the now-redundant
  imePadding(): adjustNothing without edge-to-edge never delivered IME insets,
  so the keyboard covered content on Android 11+.
- Key the card-blur re-blur on width (not height) so a height-only IME resize
  no longer re-runs the decode + StackBlur pass.
- Skip drawing the blurred strip while the cached blur's width is stale
  (rotation / split-screen), instead of stretching the old-viewport bitmap.
- Prune orphaned .tmp cache files and delete corrupt cache files on decode
  failure.
The width-only key avoided IME reloads but also missed real height growth
(freeform taller, fold unfold). Key on both dimensions and skip the expensive
pass only while the cached bitmap still covers the new viewport: growth past
the cached size re-blurs, shrink-only changes (IME) keep the taller bitmap
which still samples the correct region.

@xingguangcuicanbot xingguangcuicanbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Summary

I did not find any actionable correctness, security, regression, or maintainability issues in the visible origin/dev...HEAD diff. The blur gating, fallback paths, scaffold migration, and preference plumbing are internally consistent. Validation was limited to static review because this checkout does not include a Gradle wrapper and no system gradle binary is available here.

Decision

request_changes

Notes

Codex recommendation: approve
This PR has 2024 changed lines, so policy requires request changes.

Keep only the comments that state non-obvious constraints; drop verbose
restatements of the code.

@xingguangcuicanbot xingguangcuicanbot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated PR Review

Summary

I reviewed the checked-out PR against origin/dev...HEAD, including the new blur infrastructure, cache/bitmap lifecycle, API-33 gating, manifest/build changes, and the migrated screen scaffolds. I did not find any distinct actionable issues in the visible diff from a read-only static review. Residual risk remains around runtime-only UI/inset behavior on device classes that cannot be exercised here.

Decision

request_changes

Notes

Codex recommendation: approve
This PR has 1968 changed lines, so policy requires request changes.

Child pages (AppPageBackground) re-decoded the wallpaper with an unsized, placeholder-less AsyncImage, so the opaque surface (black in dark theme) flashed for a frame before the image landed. The blurred-card backdrop also issued a second Coil decode, competing with the main wallpaper decode on cold start. Reuse the shared painter/bitmap everywhere so the wallpaper is decoded once, and decode it as a software bitmap so the StackBlur pass can sample it.
Remove the standalone blur card in theme settings. The blur toggles now live inside the background card and animate in above the opacity slider once a custom background is configured, reusing the original expand/fade animation.
Cold starts re-decoded the custom background through a possibly-cold
content provider (slow wallpaper / black flash) and the card blur waited
on the full-size wallpaper decode before applying. Port the ReSukiSU
approach:

- copy the picked image into app-private storage (file:// URI) at pick
  time and migrate legacy content:// URIs on startup, so every launch
  decodes a local file (type-checked, size-capped, race-safe temp)
- when the blur cache misses, decode a downsampled source directly from
  the URI instead of waiting on the wallpaper painter
- run the first blur load immediately (debounce only resize re-runs)
- seed the blur viewport from display metrics and wait on a neutral
  surfaceContainer with a crossfade instead of near-black surface
@fanziyun
fanziyun merged commit 5f211e2 into dev Aug 14, 2026
10 of 12 checks passed
@fanziyun
fanziyun deleted the codex/miuix-blur-port branch August 14, 2026 17:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants