Skip to content

feat(android): add ArcProgressIndicator widget component - #268

Merged
V3RON merged 7 commits into
mainfrom
feat/android-arc-progress-indicator
Sep 9, 2026
Merged

feat(android): add ArcProgressIndicator widget component#268
V3RON merged 7 commits into
mainfrom
feat/android-arc-progress-indicator

Conversation

@V3RON

@V3RON V3RON commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

What is this?

Android widgets gain VoltraAndroid.ArcProgressIndicator, a determinate arc gauge: a partial or closed ring whose stroke fills clockwise with a progress value, 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.drawArc and handed to Glance as an Image backed by ImageProvider(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 dedicated glance/renderers/arc package: an immutable ArcSpec describes one rendering and serves as the cache key, ArcBitmapRenderer turns it into a bitmap using only android.graphics, ArcBitmapCache returns the same bitmap instance for equal specs, and ArcSizing is 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 centered Box with 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 beyond progress: 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.

…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
V3RON force-pushed the feat/android-arc-progress-indicator branch from 35a7b93 to 892e3a5 Compare September 5, 2026 18:35
…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
V3RON marked this pull request as ready for review September 5, 2026 19:24
# Conflicts:
#	docs/adr/README.md
@V3RON
V3RON merged commit a101612 into main Sep 9, 2026
14 checks passed
@V3RON
V3RON deleted the feat/android-arc-progress-indicator branch September 9, 2026 19:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android Widget Arc Progress

1 participant