feat(android): add ArcProgressIndicator widget component - #268
Merged
Conversation
…widgets Records ADR 0002 for issue #204: an AndroidArcProgressIndicator component rendered as a Canvas-drawn bitmap inside a Glance Image, with the renderer, cache, and sizing policy encapsulated in a dedicated package and a renderer-independent prop contract.
Adds VoltraAndroid.ArcProgressIndicator, a determinate arc gauge drawn as a bitmap and delivered through a Glance Image, per ADR 0002. The renderers.arc package owns the spec, the bitmap renderer, a byte-bounded cache and the sizing policy; the Glance composable resolves colors and centers the element's children on top of the arc.
V3RON
force-pushed
the
feat/android-arc-progress-indicator
branch
from
September 5, 2026 18:35
35a7b93 to
892e3a5
Compare
…izing Addresses review findings on the arc progress indicator: - Spread the sweep gradient across the track's own angular extent instead of the full circle, so the last color lands on the end of a 270 degree gauge rather than past it. Reverse the stops for a counter-clockwise sweep. - Clamp a stroke wider than the radius instead of rendering an empty bitmap. - Give an axis the style leaves unsized the resolved edge, so the default size becomes a real layout size rather than bitmap-only. - Drop the whole gradient when a color fails to parse, instead of silently rendering different stops. - Raise the bitmap cache bound so a responsive widget's own variants cannot evict each other, and drop an unreachable recycled-bitmap guard. - Cover the geometry defaults, the gradient extent, the stroke clamp and the new JSON color-list helper with tests; correct the memory and gradient wording in the docs.
…ovider Second review round: - Do not force a default size on a weighted arc. Glance's weight expands the parent's main axis with its own size modifier, and a renderer cannot tell whether the parent is a Row or a Column, so the previous unconditional width override silently dropped flex inside a Row. - Add a static guard test over the decisions ADR 0002 made about how the arc reaches Glance, so a regression to Icon.createWithBitmap cannot pass. - Assert that the filled arc stops at startAngle + sweepAngle * progress and that it grows with progress; vary the pixel size in the cache test. - Warn when more than nine children are nested, since the arc itself takes one of the ten child slots a Glance Box allows. - Announce the arc to accessibility services instead of hiding it. - Correct the changeset wording about Glance's circular indicator.
V3RON
marked this pull request as ready for review
September 5, 2026 19:24
# Conflicts: # docs/adr/README.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this?
Android widgets gain
VoltraAndroid.ArcProgressIndicator, a determinate arc gauge: a partial or closed ring whose stroke fills clockwise with aprogressvalue, with round or flat ends, a configurable track, stroke width, start angle, sweep, and an optional sweep gradient. Anything placed as children is centered inside the arc, so a percentage label or an icon composes the way the mock in #204 shows. Until now the only circular indicator Voltra could render on Android was Jetpack Glance's indeterminate spinner, because Glance has no determinate circular indicator and no canvas.The design is recorded in ADR 0002, which also documents the alternatives that were evaluated against the AOSP and AndroidX sources and rendered through the real framework.
How does it work?
The indicator is drawn on the device as a square ARGB bitmap with
Canvas.drawArcand handed to Glance as anImagebacked byImageProvider(bitmap), the one image form the framework both counts toward a widget's bitmap budget and deduplicates when the same bitmap appears more than once in an update. The Android side is encapsulated in a dedicatedglance/renderers/arcpackage: an immutableArcSpecdescribes one rendering and serves as the cache key,ArcBitmapRendererturns it into a bitmap using onlyandroid.graphics,ArcBitmapCachereturns the same bitmap instance for equal specs, andArcSizingis the single sizing policy: the smaller style dimension, density clamped to 1x–3.5x, capped at 512 px per edge, degrading resolution instead of failing. A thin composable reads the element props, resolves Voltra color strings including dynamic and day/night colors, and emits a centeredBoxwith the image and the children. The shared dispatcher serves both payload-driven and Dynamic widgets, so one implementation covers both kinds on every supported Android version.The component is declared in
components.json; the generator produces the TypeScript props, Kotlin parameters, component id and short name. Defaults draw the issue's gauge with no props beyondprogress: a 270° sweep starting at 135° with round caps and an 8 dp stroke.Why is this useful?
Battery, fitness and quota style gauges are the most common widget visuals that Voltra could not express on Android. This closes #204 with a component whose look is fully customizable from JSX, whose memory cost is bounded and predictable (a typical gauge costs a fraction of a megabyte of a budget that is 1.5 times the screen), and whose prop contract is renderer-independent, so a native RemoteCompose arc can replace the bitmap on Android 16+ later without a breaking change.