Repository navigation
Update Android box-shadow drawables in place when only their values change - #59097
YevheniiKotyrlo wants to merge 5 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>
);
}
|
1518818 to
a5b5339
Compare
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.
a5b5339 to
acc6375
Compare
b8afc91 to
ad00c2f
Compare
The box-shadow drawables cache their geometry, which depends on the screen density and, in RTL, on the left/right swap preference, but rebuilt it only on a bounds or layout direction change and when borderRadius or borderInsets was assigned. Neither the density, which changes in place for an activity that handles density configuration changes, nor I18nManager.swapLeftAndRightInRTL() notifies a drawable, so one drawn again afterwards kept the old geometry. Each drawable now records the density and swap it built its geometry with and rebuilds it when either differs.
ad00c2f to
fd023c0
Compare
The geometry check read the left/right swap preference on every RTL draw, also for a shadow with no border radius and no border insets, whose geometry never reads it. It now reads it only when the drawable has a border radius, or border insets for an inset shadow, as resolving them does.
Visual regressionNo visual differences at ios: no capture |
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. A spread past half the padding box's height now empties the shadow's inner shape, as one past half its width did: the inner rect was left inverted, which drawDoubleRoundRect sorts into a hole the shape does not have and the path drew without one.
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.
fd023c0 to
5af3a0e
Compare
Summary:
An animated
boxShadowsets, on every frame, a list with the same number of inset and outset shadows and new values, andBackgroundStyleApplicator.setBoxShadowanswered each one with new shadow drawables, a newCompositeBackgroundDrawableand a new view background, built on the UI thread during the frame's mount.InsetBoxShadowDrawableandOutsetBoxShadowDrawablenow take their colour, offsets, blur radius and spread together throughsetShadowValues, which invalidates the drawable once, and only when one of them changes, andsetBoxShadowhands the existing drawables their new values when the list keeps the same number of inset and outset shadows, assetOutlineColorhands the existingOutlineDrawableits colour. Any other list 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, whereas new drawables would render every mask again on every frame. The mask filter is rebuilt when the blur radius or the screen density changes, as a new drawable would build it.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 with the same number of inset and outset shadows keeps the view's background and its shadow drawables, draws the new values, also after a density change, and invalidates each shadow once; any other list 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, for the inset and the outset drawable: each value changed on its own draws like a drawable built with it; a blur follows a density change, and the mask filter is kept across draws and rebuilt after a density change; a colour change keeps the drawable's alpha; setting the values to what they already hold invalidates nothing, and changing all five invalidates once. It also gains an inset shadow losing its blur in place, and a blurred inset shadow's colour change and an outset shadow's blur change resolving the border radius only once; #59081's cases for an alpha or colour-filter change also change the colour. Both classes draw throughRecordingCanvas.kt, which also records each draw's paint colour, alpha and blur, and count invalidations withInvalidationCountingCallback.kt.RecordingCanvas.ktis byte for byte the file #59189 adds, so whichever of the two lands first carries it; the calls it records beyond these tests' are #59189's../gradlew :packages:react-native:ReactAndroid:testDebugUnitTest -x :packages:react-native:ReactAndroid:buildCodegenCLI -Preact.internal.useHermesStable=true -PenableWarningsAsErrors=true --rerun --tests com.facebook.react.uimanager.drawable.BoxShadowDrawableTest --tests com.facebook.react.uimanager.BackgroundStyleApplicatorBoxShadowTest --tests com.facebook.react.uimanager.drawable.CompositeBackgroundDrawableTest --tests com.facebook.react.views.switchview.ReactSwitchManagerTest: 65 tests, 0 failures;ReactSwitchManagerTestcoversCompositeBackgroundDrawable.withNewOriginalBackground(Fix Switch losing outline and box shadow when backgroundColor is set on Android #59425), which carries the shadow lists this PR types by kindjava -jar node_modules/ktfmt/lib/ktfmt.jar --do-not-remove-unused-imports --dry-run --set-exit-if-changedon the changed files: 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. Those builds predate three later changes: #59081's density and RTL swap checks rebuild nothing while neither changes, #59083's check for a spread past half the height is beyond these spreads, and
setShadowValuesinvalidates each shadow once a frame for a colour change, as those builds did.I didn't run the full Android
build, the template and HelloWorld app builds, or the Android e2e jobs locally.