Repository navigation
Commit 42ba24a
Use a round rect outline for Views with four equal corners on Android (#59376)
Summary:
On Android, a View's background is a `CompositeBackgroundDrawable`, and the View's outline (used for `elevation` shadows) comes from its `getOutline`.
**Current behavior.** For a View with rounded corners, `getOutline` allocates a new `Path`, adds a rounded rect with eight radii, and copies the path into the outline with `Outline.setPath`, every time Android asks for the outline. Android rebuilds the outline whenever the View's size changes and whenever its background drawable invalidates itself, for example when the background color changes or the drawable's bounds are updated during draw, which can happen two or three times for one change. Most rounded Views use the same radius for every corner, and for that shape Android has `Outline.setRoundRect`, which needs no path. On a Samsung Galaxy A14, a path outline costs about 15–19 µs per rebuild, and rebuilding it is 72% of the time to change a rounded View's background color.
**With this change.** When all four corners resolve to the same circular radius (the eight values, including border insets, are equal), `getOutline` uses `Outline.setRoundRect(bounds, radius)`. A radius larger than half the shorter side is reduced to half of it, which is what `Path.addRoundRect` does to such radii (Skia scales all radii by the same factor so that they fit). Corners that differ, elliptical corners, and insets that differ by side still build the path as before.
Two differences that don't change rendering in React Native:
- On Android 12 and lower, a round rect outline can clip and a path outline can't. React Native doesn't set `clipToOutline`, so this only matters for a View where an app sets it, which then clips as requested.
- A reduced radius is computed directly instead of through Skia's scale factor, so it can differ in the last bits of a float.
## Changelog:
[ANDROID] [CHANGED] - Views with four equal rounded corners get their outline without building a path
Pull Request resolved: #59376
Test Plan:
- **Unit tests.** New `CompositeBackgroundDrawableTest` (Robolectric): four equal corners give a round rect outline with the radius in px; a radius larger than half the shorter side is reduced to it; differing corners still give a path outline; no rounded corners give a rect. `./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests "com.facebook.react.uimanager.drawable.CompositeBackgroundDrawableTest"`: 4 passed. On the current code, the first two fail and the other two pass.
- The repo's pinned `ktfmt` check passes on both files.
- **Device:** Samsung Galaxy A14 (SM-A145R, Android 15), RNTester built with Gradle in Release (`assembleRelease`, arm64-v8a) and compiled with `cmd package compile -m speed`. The scenarios are local Playground screens that are not part of this PR.
- **A/B setup:** a single binary with a local switch (an intent extra read in `getOutline`, not part of this PR) that either uses this change or builds the path for every outline as before. Launches alternate between the arms (A B B A A B); each reads the UI thread's and RenderThread's CPU time from `/proc/<pid>/task/*/stat` over the same 20 s window and counts the time spent in `getOutline`.
| 300 rounded cards (`borderRadius: 12`, border, background, two texts), one kind of change every 100 ms, 3 launches per arm | path (before) | this PR | change |
|---|--:|--:|--:|
| time per `getOutline`, cards move (a spacer above them changes height) | 14.6–17.6 µs | 3.6–3.9 µs | −77% |
| time per `getOutline`, cards change background color | 18.5–19.4 µs | ~4.1 µs | −78% |
| UI thread CPU, cards move | 6,870 ms | 5,413 ms | −21% |
| UI thread CPU, cards change background color | 5,703 ms | 4,653 ms | −18% |
| UI thread CPU, cards with `elevation: 3` move | 5,723 ms | 4,870 ms | −15% |
The RenderThread's CPU time didn't change measurably, including for the cards with shadows.
Moving cards changes their size too: positions are rounded to whole pixels, so a card's height changes by 1 px when its edges round differently, and each size change rebuilds the outline. Changing a background color doesn't change the shape, but Android rebuilds the outline when the drawable invalidates itself.
- **Scrolling a feed** (a 1,000-row list with rounded cards, avatars and photos, 4 launches per arm): time in `getOutline` went from about 87 to 26 ms per 4,000 calls (−70%). Outlines are about 0.4% of the UI thread there, so its CPU time didn't change measurably.
- **Rendering:** screens captured in both arms and compared pixel by pixel: a local screen of 8 Views with `elevation: 8` (equal corners, a circle, a pill whose radius is larger than half its height, a percentage radius, differing corners, a border, `overflow: hidden` with a child that overflows, square corners), RNTester's View examples and RNTester's Border examples. Every capture is identical between the arms, and between two captures of the same arm.
- **Not run:** other devices, and Android versions other than 15.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Reviewed By: christophpurrer
Differential Revision: D127120615
Pulled By: javache
fbshipit-source-id: fb75f075af5628e576e6ce495056de06eeae03e21 parent 08f96fe commit 42ba24a
2 files changed
Lines changed: 114 additions & 8 deletions
File tree
- packages/react-native/ReactAndroid/src
- main/java/com/facebook/react/uimanager/drawable
- test/java/com/facebook/react/uimanager/drawable
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
187 | 187 | | |
188 | 188 | | |
189 | 189 | | |
190 | | - | |
191 | | - | |
192 | 190 | | |
193 | 191 | | |
194 | 192 | | |
| |||
199 | 197 | | |
200 | 198 | | |
201 | 199 | | |
202 | | - | |
203 | | - | |
204 | | - | |
| 200 | + | |
| 201 | + | |
205 | 202 | | |
206 | 203 | | |
207 | 204 | | |
| |||
211 | 208 | | |
212 | 209 | | |
213 | 210 | | |
214 | | - | |
215 | | - | |
216 | | - | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
217 | 220 | | |
218 | 221 | | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
219 | 225 | | |
220 | 226 | | |
221 | 227 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
0 commit comments