Repository navigation
Rebuild Android background and border geometry only when it changes - #59189
YevheniiKotyrlo wants to merge 1 commit into
Conversation
Galaxy S23: 150 views changing their background or border color every frameIn each build's two rounds, for The measured commit,
Release Each window runs Each variant's settled twin, Scenefunction AnimatedDrawableStressExample({variant, settles = false}) {
const [phase, setPhase] = useState(0);
useEffect(() => {
let frame = requestAnimationFrame(function step(timestamp) {
if (settles) {
setPhase(1);
return;
}
setPhase(0.5 + 0.5 * Math.cos((timestamp / 1000) * 2 * Math.PI));
frame = requestAnimationFrame(step);
});
return () => cancelAnimationFrame(frame);
}, [settles]);
const color = `rgb(${Math.round(124 + 100 * phase)}, 58, 237)`;
const animatedStyle =
variant === 'background'
? {backgroundColor: color}
: variant === 'background-rounded'
? {borderRadius: 8, backgroundColor: color}
: variant === 'background-uneven'
? {borderRadius: 8, borderBottomRightRadius: 2, backgroundColor: color}
: variant === 'border'
? {backgroundColor: '#e5e7eb', borderWidth: 1, borderColor: color}
: {borderRadius: 8, backgroundColor: '#e5e7eb', borderWidth: 1, borderColor: color};
return (
<View style={{flexDirection: 'row', flexWrap: 'wrap', gap: 12, padding: 8}}>
{Array.from({length: 150}, (_, index) => (
<View key={index} style={[{width: 36, height: 36}, animatedStyle]} />
))}
</View>
);
}Each variant is one entry of |
1c6d94e to
e0615e8
Compare
RecordingCanvas.kt is the file react#59189 adds for the background and border drawable tests, byte for byte, so whichever of the two lands first carries it. Those drawables also call translate, drawRect(left, top, right, bottom) and drawRoundRect, and draw borders with stroke paints, so the canvas records those calls and each paint's style and width.
e0615e8 to
f9cfc71
Compare
RecordingCanvas.kt is the file react#59189 adds for the background and border drawable tests, byte for byte, so whichever of the two lands first carries it. Those drawables also call translate, drawRect(left, top, right, bottom) and drawRoundRect, and draw borders with stroke paints, so the canvas records those calls and each paint's style and width.
BackgroundDrawable, BackgroundImageDrawable and BorderDrawable marked their paths stale in invalidateSelf(), so every redraw request rebuilt their geometry on the next draw: a background or border color change, an alpha change, a border style change, a new background image layer. A color animation paid that rebuild for every view on every frame. The geometry depends only on the bounds, the border radius and insets and the layout direction. The drawables now mark it stale in their borderRadius and borderInsets setters, in onBoundsChange and in onLayoutDirectionChanged. A layout direction change now rebuilds the geometry instead of waiting for the next redraw request. BorderDrawable now insets its inner clip by the border widths whatever its edge colors. It used to inset it only while an edge was visible, and nothing is drawn while none is. It also resolves its border widths with the rest of its geometry instead of on every draw, reuses its single-edge path across draws, and stops building an outline path nothing reads, the only geometry setBorderWidth's widths fed. It drops setBorderRadius(property, radius), which nothing calls and which changed the shared BorderRadiusStyle in place without reassigning it.
f9cfc71 to
5452a3d
Compare
RecordingCanvas.kt is the file react#59189 adds for the background and border drawable tests, byte for byte, so whichever of the two lands first carries it. Those drawables also call translate, drawRect(left, top, right, bottom) and drawRoundRect, and draw borders with stroke paints, so the canvas records those calls and each paint's style and width.
An animated box shadow hands BackgroundStyleApplicator.setBoxShadow a list of the same shape with new values on every frame, and setBoxShadow answered each one with new shadow drawables, a new CompositeBackgroundDrawable and a new view background, where setOutlineColor sets the color on the existing OutlineDrawable. Both shadow drawables now take their color, offsets, blur radius and spread as properties that invalidate only on a change, and setBoxShadow hands the existing drawables their new values when the list keeps the same number of inset and outset shadows. A color change keeps the cached geometry, so a blurred inset shadow keeps Skia's cached mask while its color animates. The mask filter is rebuilt when the blur radius or the screen density changes, as a new drawable would build it. CompositeBackgroundDrawable types its shadow lists by kind, so the applicator updates them without casts. The tests draw through RecordingCanvas, the file react#59189 adds for the background and border drawable tests, byte for byte, so whichever of the two lands first carries it.
An animated box shadow hands BackgroundStyleApplicator.setBoxShadow a list of the same shape with new values on every frame, and setBoxShadow answered each one with new shadow drawables, a new CompositeBackgroundDrawable and a new view background, where setOutlineColor sets the color on the existing OutlineDrawable. Both shadow drawables now take their color, offsets, blur radius and spread together through setShadowValues, which invalidates the drawable once, and only when one of them changes, and setBoxShadow hands the existing drawables their new values when the list keeps the same number of inset and outset shadows. A color change keeps the cached geometry, so a blurred inset shadow keeps Skia's cached mask while its color animates. The mask filter is rebuilt when the blur radius or the screen density changes, as a new drawable would build it. CompositeBackgroundDrawable types its shadow lists by kind, so the applicator updates them without casts. The tests draw through RecordingCanvas, the file react#59189 adds for the background and border drawable tests, byte for byte, so whichever of the two lands first carries it.
Summary:
BackgroundDrawable,BackgroundImageDrawableandBorderDrawableoverrideinvalidateSelf()to mark their geometry stale, so every redraw request rebuilds their paths, rects and resolved radii on the nextdraw(): a background or border color change, an alpha change, a border style change, a new background image layer. A color animation pays that rebuild for every view on every frame, on the UI thread, while the display list is recorded.The geometry depends only on the bounds, the border radius and insets, and the layout direction. The three drawables now mark it stale in their
borderRadiusandborderInsetssetters, inonBoundsChangeand inonLayoutDirectionChanged, the same shape as #59081 for the box-shadow drawables.BackgroundStyleApplicatoralready reassigns the sharedBorderRadiusStyleandBorderInsetsafter changing them, so it needs no change. A layout direction change now rebuilds the geometry; before, the backgrounds and a rounded border kept the previous direction's start and end corners and widths until their next redraw request.BorderDrawablenow insets its inner clip by the border widths whatever its edge colors. It used to inset it only while an edge was visible, and nothing is drawn while none is. It also resolves its border widths with the rest of its geometry instead of on every draw, and reuses its single-edge path across draws. It stops building an outline path nothing reads, which was the only geometrysetBorderWidth's widths fed. And it dropssetBorderRadius(property, radius), which nothing calls and which changed the sharedBorderRadiusStylein place without reassigning it.Changelog:
[ANDROID] [FIXED] - Background and border drawables no longer rebuild their geometry when only a color, an alpha or a style changes
Test Plan:
BackgroundAndBorderDrawableTest(new, Robolectric with native graphics) draws each drawable through a recording canvas and into a bitmap, and compares the calls and the pixels with those of a freshly built drawable in the same state. Five of its cases draw a border of one width and one color, whichBorderDrawablestrokes along a single path. 17 of its 25 cases fail onmain:TooManyActualInvocationsonmain), and one that a rectangular border hands the canvas the samePathon every draw;maindoes only at the next redraw request;mainrebuilds only on the redraw requestBackgroundStyleApplicatorsends after reassigning them, so these fail onmainalthough an app onmaindraws correctly.The other eight pass on both and hold what the geometry must still follow:
mainresolves on every draw;setBorderWidth.Each trigger is load-bearing: removing one from a drawable — the stale mark in a setter, in
onBoundsChangeor inonLayoutDirectionChanged— fails exactly the cases that name it.RecordingCanvas.ktis the file #59097 adds, byte for byte, so whichever of the two lands first carries it../gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -PenableWarningsAsErrors=true --tests …over the nine classes that reach these drawables (BackgroundAndBorderDrawableTest,BorderDrawableTest,CompositeBackgroundDrawableTest,BackgroundStyleApplicatorTest,ReactSwitchManagerTest, and the image, scroll view, text and text input property tests): 83 tests, 0 failuresnode ./scripts/format-kotlin.js --check: cleanRNTester release builds of
mainand of this change atc2207dbfbe7, 150 views whose background or border color changes on every frame, on a Galaxy S23 (Android 14, API 34). Draw recording drops from 1.49–1.53 to 0.54–0.55 ms per frame for a rounded 1px border and from 0.75–0.79 to 0.30–0.31 ms for a background with uneven radii, and the median window's frames over the 8.33 ms budget from 13–15 to 0–3 of 120. Settled frames are pixel-identical below the status bar. The numbers, the scene and how to rerun them are in the evidence comment.I didn't run the rest of the ReactAndroid unit tests, the full Android
build, the template and HelloWorld app builds, or the Android e2e jobs locally.