Repository navigation
Update Android box-shadow drawables in place when only their values change - #59097
YevheniiKotyrlo wants to merge 3 commits into
Conversation
Device measurement — before / after#59083 — every frame of an animated shadow builds new shadow drawables, a new This PR — 6.4 ms, and the ring's frame falls from 25.0 to 17.9 ms. The blurred shadows keep their cached masks, and their frame takes 22.7 ms. The control, which animates the background colour instead of the shadow, is unchanged. Mi MIX 3 (Android 10, API 29, Adreno 630, 60 Hz):
Galaxy S23 (Android 14, API 34, Adreno 740, 120 Hz):
RNTester release builds for Scenefunction AnimatedBoxShadowStress({inset, blurred = false}) {
const [alpha, setAlpha] = React.useState(1);
React.useEffect(() => {
let frame = requestAnimationFrame(function step(timestamp) {
setAlpha(0.6 + 0.4 * Math.cos((timestamp / 1000) * 2 * Math.PI));
frame = requestAnimationFrame(step);
});
return () => cancelAnimationFrame(frame);
}, []);
const color = `rgba(124, 58, 237, ${alpha.toFixed(3)})`;
const animatedStyle =
inset == null
? {backgroundColor: color}
: {
backgroundColor: '#e5e7eb',
boxShadow: `${inset ? 'inset ' : ''}${blurred ? '0 2px 6px' : '0 0 0 1px'} ${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, borderRadius: 8}, animatedStyle]} />
))}
</View>
);
}
|
InsetBoxShadowDrawable and OutsetBoxShadowDrawable resolved the border radii and built new RectF, Path and radii arrays inside draw(), so every redraw of a view's background, every frame of an animation that touches it, allocated them again and handed the canvas a new Path. Both drawables now keep that geometry in fields and rebuild it only when its inputs change: on a bounds or layout direction change, and when borderRadius or borderInsets is assigned. An alpha or color filter change only redraws.
A blurred inset shadow is drawn with drawDoubleRoundRect, and Skia turns a double round rect with a mask filter into a new volatile path on every draw, which it never caches: the blurred mask is rendered again, offscreen, every time the view is drawn. Draw a blurred inset shadow from a cached even-odd path instead, so Skia can key its blurred mask on the path, the blur and the matrix. An unblurred shadow keeps drawDoubleRoundRect, which Skia draws analytically.
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. CompositeBackgroundDrawable types its shadow lists by kind, so the applicator updates them without casts.
1518818 to
a5b5339
Compare
Summary:
An animated
boxShadowsets a list of the same shape with new values on every frame, andBackgroundStyleApplicator.setBoxShadowanswered each one with new shadow drawables, a newCompositeBackgroundDrawableand a new view background, built on the UI thread while the frame is mounted.setOutlineColoralready sets the colour on the existingOutlineDrawable.InsetBoxShadowDrawableandOutsetBoxShadowDrawablenow take their colour, offsets, blur radius and spread as properties that invalidate only on a change, andsetBoxShadowhands the existing drawables their new values when the list keeps the same number of inset and outset shadows. A list of another shape gets new drawables and a new background, as before. A colour change only redraws: the drawable keeps its geometry, so a blurred inset shadow keeps the mask Skia cached for it (#59083) while its colour animates, where new drawables render every mask again on every frame.CompositeBackgroundDrawabletypes its shadow lists by kind, so the applicator updates them without casts.This builds on #59083 and #59081 and includes their commits.
Changelog:
[ANDROID] [FIXED] - An animated box shadow updates its drawables in place instead of replacing the view's background on every frame
Test Plan:
BackgroundStyleApplicatorBoxShadowTestis new: a list of the same shape keeps the view's background and its shadow drawables and draws the new values, a list of another shape gets new drawables, an empty list removes them, and a border radius or border width set after the shadows reaches them. On #59083 the first case fails, because every call sets a new background.BoxShadowDrawableTestgains nine cases: each value changed on its own draws like a drawable built with it, for both drawables; an inset shadow loses its blur in place; a colour change keeps the drawable's alpha, for both; setting a value to what it already holds invalidates nothing, for both; and a blurred inset shadow's colour change and an outset shadow's blur change resolve the border radius no second time. #59081's two cases for an alpha or colour-filter change now change the colour too. The recordingCanvasthe drawable tests use moves toRecordingCanvas.kt, so both classes share it, and also records each draw's paint colour, alpha and blur../gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -PenableWarningsAsErrors=true: 796 tests, 0 failuresnode ./scripts/format-kotlin.js --check: cleanOn device (evidence), RNTester release builds of #59083 and of this branch, 150 views whose shadow changes colour on every frame: the median frame falls from 25.0 to 17.9 ms for a 1px inset ring, from 23.4 to 16.4 ms for an outset ring and from 405.0 to 22.7 ms for a blurred inset shadow on a Mi MIX 3 (Android 10, API 29), and from 9.2 to 6.4 ms, 9.4 to 6.5 ms and 38.4 to 9.2 ms on a Galaxy S23 (Android 14, API 34), where the blurred shadow's frames run 37 Skia render passes instead of 636. A control that animates the background colour instead holds level on both.
I didn't run the full Android
build, the template and HelloWorld app builds, or the Android e2e jobs locally.