Repository navigation
Conversation
Define AndroidColorPair, StaticColor and AndroidColorValue once next to the dynamic colours and export AndroidColorPair from the package root. The modifiers module re-exports it as DayNightColors (alongside StaticColor), so the 2.4.0 paths keep working and background keeps rejecting role tokens inside a pair at compile time. background's payload is unchanged. Add a type-test project (packages/android/type-tests) with @ts-expect-error cases, run by the package's typecheck script.
A style backgroundColor, text color, borderColor or shadowColor can be
{ day, night }. The colour model gains a DayNight variant that maps to
Glance's public day/night ColorProvider, so from Android 12 the launcher
swaps the colours itself; below that composition picks the side from the
configuration's night bit. An invalid pair is dropped with one warning
naming the style key.
Adds the native compose/apply test seam (NativeComposeSeam) that later
colour-pair tests reuse.
…t demo The card sits in one new Row so the root Column stays under Glance's 10-child limit; later colour-pair demos join that Row.
…rt names A "color" parameter emits AndroidColorValue in the generated Android JSX props and @contextual Any? in the Kotlin parameter classes (which are @serializable, so a bare Any does not compile). Short-name validation now rejects "day" and "night", the keys of a colour pair, which decompression would otherwise expand.
Every Android component colour prop is now a "color" parameter, so it accepts a
{ day, night } pair as well as a colour string or role token. Regenerated with pnpm generate.
…ur props Renderers hand the raw prop value to the colour parser instead of casting it to a String, so a pair reaches the View and an invalid one is dropped with a warning naming the prop. TitleBar and Scaffold no longer crash the composition on a non-string colour, and the Text color prop is now read (it wins over the style colour).
…mage pair
Style conversion parses both sides of a `{ day, night }` backgroundImage pair and
rendering draws the side for the composing context's night bit. A pair with a missing
or unparsable side is dropped with one warning naming the key, so backgroundColor
shows instead. Seam tests also pin that token stops draw a distinct bitmap per scheme
and that the gradient cache never hands the day bitmap to the night scheme.
ArcProgressIndicator's gradientColors now accepts colour strings, role
tokens and { day, night } pairs as elements. The prop is still sent as a
JSON-encoded array.
…s pairs
The gradientColors list parser now decodes each element dynamically, so a
{ day, night } object reaches the colour parser and the arc bitmap is drawn
with the current scheme's side. An invalid pair drops the gradient, as an
unparsable string does, and the single warning names gradientColors.
Arc bitmaps stay keyed by resolved ARGB and the cache clear on a scheme flip
is unchanged. Custom-font text needs no change: its only caller resolves the
colour to a static one before drawing; a seam test now covers it.
…roundStyleScale
Chart mark colours are resolved during composition by parsing the raw
`c` value, so a `{ day, night }` pair resolves to the current scheme's
hex before the bitmap renderer reads it. `foregroundStyleScale` entries
now decode dynamically, keeping the series key a string and passing the
raw colour to the parser. An invalid pair falls back to the default
palette with one warning naming the prop.
`foregroundStyleScale` is typed `Record<string, AndroidColorValue>`.
…me it for color values
parseColorStringList returns strings or { day, night } maps now, so it becomes parseColorValueList.
…helper Side detection, the description of the rejected value and the warning text now live in parseDayNightPair.
…nt or a day/night pair VoltraBackgroundImage mirrors VoltraColorValue.DayNight, so callers resolve the gradient for the scheme instead of choosing between two nullable fields.
…r provider mapping VoltraColorValue.toColorProvider(resolveRole) holds the one static and day/night mapping; the modifier scope passes its own theme-role resolver, so its theme-role behavior is unchanged.
…names it JSColorParser.parse keeps its optional key: modifier, notification, gradient-stop and chart-bitmap callers still omit it.
…xplain the DayNightColors alias
…arsing the gradient
StyleConverter.convert takes isNightMode (default false) and narrows a
{ day, night } backgroundImage pair to one CSS string, which the existing
gradient parser then reads. resolveElementStyle passes the composing
context's night bit. A pair whose sides are missing, empty or not strings
is still dropped with one warning; a typo in the unselected side no longer
drops the selected one.
DecorationStyle.backgroundImage is a BackgroundImageValue again, and
VoltraBackgroundImage, resolveGradient and JSGradientParser.parsePair are
gone.
The color list parser keeps its name from main so RendererJsonTest is unchanged; its KDoc says the
elements can be { day, night } maps as well as strings.
…t files Cases whose production unit already has a test file on main move there as new methods: gradient backgrounds into GradientBitmapRendererTest, arc gradient colors into ArcBitmapRendererTest, chart colors into ChartBitmapRendererTest, the modifier scope into VoltraModifierRegistryTest and backgroundImage pair conversion into StyleConverterTest. The rest stay in new files named after the unit they test: JSColorParserTest, VoltraColorValueTest, StyleModifiersTest, GlanceFactoryTest and TextAndImageRenderersTest. No case changes and the test count is unchanged.
Drop helpers and tests the issue does not ask for, cover arc color pairs and decompressed style pairs, and shorten the pair docs.
Type every Android component color prop as a color value that links to the day and night colors section.
Emulator evidenceTest layout: the
Colors were sampled from screencaps by counting pixels within a channel delta of 12 of each expected color. The widget shows its render time, which tells a launcher-side swap apart from a recomposition. Build: a debug APK of the example app from this branch @ Android 15 emulator, API 35 (AVD
|
| Step | Result | Render time |
|---|---|---|
| Light, app running | every demo on its day side, no night pixels | 5:06:00 PM |
| Dark, app running | every demo on its night side, no day pixels | 5:07:25 PM |
| Light again | same pixel counts as the first light run | 5:07:56 PM |
| App process killed, then dark | card and prop pair switch to night; gradient and arc keep day | 5:07:56 PM, unchanged |
With no app process and no new render, the launcher swaps the plain pair colors itself. The gradient and arc are bitmaps, so they keep the side they were drawn with until the widget recomposes.
Android 7.0 emulator, API 24 (AVD Medium_Phone_2)
| Step | Result | Render time |
|---|---|---|
| Light, app running | every demo on its day side, no night pixels | 5:32:25 PM |
| Dark, app running | every demo on its night side, no day pixels | 5:34:05 PM |
| Light again | same pixel counts as the first light run | 5:34:46 PM |
| App process killed, then light | every pair keeps night | 5:35:21 PM, unchanged |
| Widget refresh | every demo back on its day side | 5:47:43 PM |
Below Android 12 the side is picked when the widget is composed, so a theme change needs a recomposition. Refreshing the widget recomposes it, and it picks the side from the new theme. That a recomposition alone is enough, with no JavaScript render, is covered by the API 26 test in StyleModifiersTest.
Charts aren't placed in the demo widget, so they are covered by the ChartBitmapRendererTest pixel tests only.
Steps:
- Start Metro from
example/on port 8104. - Build
./gradlew :app:assembleDebug -PreactNativeArchitectures=arm64-v8a -PreactNativeDevServerPort=8104andadb install -rit. - Flip the theme:
- API 35:
adb shell cmd uimode night yes|no - API 24, which has no
cmd uimode:adb shell service call uimode 4 i32 2for night,i32 1for day
- API 35:
- With the app backgrounded, kill its process:
adb shell am kill <package>on API 35,adb shell run-as <package> kill <pid>on API 24. - After each step, press HOME, then
adb exec-out screencap -p.
Unit tests: pnpm test:kotlin (670 tests), pnpm test, pnpm typecheck (including the type tests), pnpm lint and pnpm format:kotlin:check pass, and so do the pre-push test-js/test-kotlin hooks.
What is this?
Android Dynamic Widget colors can now be a
{ day, night }pair: one color for the light system theme and one for the dark. Until now a color was a single string or anAndroidDynamicColorsrole, and the only way to change colors with the theme was to branch onenv.colorScheme, which runs the widget's JavaScript again on every theme change.Pairs work wherever a color string does: style colors, component color props, chart mark colors and
foregroundStyleScale, and arcgradientColors.backgroundImagetakes a pair of whole gradients.This is the color-pair half of #319. The app color scheme mapped onto
GlanceThemecomes in a separate PR.How does it work?
The pair travels through the payload unchanged, and the native side picks the side to show:
Differences from #319:
{ day, night }instead of{ light, dark }, as agreed with the maintainer.backgroundColor, theTextcolorprop and chart colors also take pairs. TheTextcolorprop is now applied, and it wins over the style'scolor.Why is this useful?
Widget authors can give a widget colors for both themes without branching on
env.colorScheme. On Android 12+ the colors stay right after a theme change even when the app isn't running.Refs #319