Skip to content

Use a round rect outline for Views with four equal corners on Android - #59376

Closed
hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:android-round-rect-outline
Closed

hryhoriiK97 wants to merge 1 commit into
react:mainfrom
hryhoriiK97:android-round-rect-outline

Conversation

@hryhoriiK97

@hryhoriiK97 hryhoriiK97 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 22, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown

Visual regression

No visual differences at 049012bf9b4f.

android: no baseline for this environment

ios: no capture

@meta-codesync

meta-codesync Bot commented Oct 23, 2026

Copy link
Copy Markdown

@javache has imported this pull request. If you are a Meta employee, you can view this in D127120615.

@meta-codesync meta-codesync Bot closed this in 42ba24a Oct 23, 2026
@react-native-bot

Copy link
Copy Markdown
Collaborator

This pull request was successfully merged by @hryhoriiK97 in 42ba24a

When will my fix make it into a release? | How to file a pick request?

@react-native-bot react-native-bot added the Merged This PR has been merged. label Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants