Skip to content

Commit ad615d5

Browse files
Stop rebuilding box-shadow geometry on every draw
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.
1 parent 27e7721 commit ad615d5

3 files changed

Lines changed: 467 additions & 124 deletions

File tree

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/InsetBoxShadowDrawable.kt‎

Lines changed: 100 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import android.graphics.ColorFilter
1515
import android.graphics.Paint
1616
import android.graphics.Path
1717
import android.graphics.PixelFormat
18+
import android.graphics.Rect
1819
import android.graphics.RectF
1920
import android.graphics.drawable.Drawable
2021
import androidx.annotation.RequiresApi
@@ -45,12 +46,8 @@ internal class InsetBoxShadowDrawable(
4546
private val offsetY: Float,
4647
private val blurRadius: Float,
4748
private val spread: Float,
48-
/*
49-
* We assume borderRadius & borderInsets to be shared across multiple drawables
50-
* therefore user should invalidate this drawable when changing either of them
51-
*/
52-
var borderInsets: BorderInsets? = null,
53-
var borderRadius: BorderRadiusStyle? = null,
49+
borderInsets: BorderInsets? = null,
50+
borderRadius: BorderRadiusStyle? = null,
5451
) : Drawable() {
5552
private val shadowPaint =
5653
Paint().apply {
@@ -61,6 +58,41 @@ internal class InsetBoxShadowDrawable(
6158
}
6259
}
6360

61+
private var needUpdatePath = true
62+
private val paddingBoxRect = RectF()
63+
private val paddingBoxRadii = FloatArray(8)
64+
private var hasPaddingBoxRadii = false
65+
private val paddingBoxPath = Path()
66+
private val innerRect = RectF()
67+
private val innerRadii = FloatArray(8)
68+
private val outerRect = RectF()
69+
70+
/*
71+
* We assume borderRadius & borderInsets to be shared across multiple drawables
72+
* therefore user should assign them to this drawable again when changing either of them
73+
*/
74+
var borderInsets: BorderInsets? = borderInsets
75+
set(value) {
76+
field = value
77+
needUpdatePath = true
78+
}
79+
80+
var borderRadius: BorderRadiusStyle? = borderRadius
81+
set(value) {
82+
field = value
83+
needUpdatePath = true
84+
}
85+
86+
override fun onBoundsChange(bounds: Rect) {
87+
super.onBoundsChange(bounds)
88+
needUpdatePath = true
89+
}
90+
91+
override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
92+
needUpdatePath = true
93+
return true
94+
}
95+
6496
override fun setAlpha(alpha: Int) {
6597
shadowPaint.alpha = (((alpha / 255f) * (Color.alpha(shadowColor) / 255f)) * 255f).roundToInt()
6698
invalidateSelf()
@@ -82,69 +114,80 @@ internal class InsetBoxShadowDrawable(
82114
}
83115

84116
override fun draw(canvas: Canvas) {
117+
updatePath()
118+
119+
canvas.save().let { saveCount ->
120+
if (hasPaddingBoxRadii) {
121+
canvas.clipPath(paddingBoxPath)
122+
canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, innerRadii, shadowPaint)
123+
} else {
124+
canvas.clipRect(paddingBoxRect)
125+
canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, ZERO_RADII, shadowPaint)
126+
}
127+
128+
canvas.restoreToCount(saveCount)
129+
}
130+
}
131+
132+
private fun updatePath() {
133+
if (!needUpdatePath) {
134+
return
135+
}
136+
needUpdatePath = false
137+
85138
val computedBorderRadii = computeBorderRadii()
86139
val computedBorderInsets = computeBorderInsets()
87140

88-
val paddingBoxRect =
89-
RectF(
90-
bounds.left + (computedBorderInsets?.left ?: 0f),
91-
bounds.top + (computedBorderInsets?.top ?: 0f),
92-
bounds.right - (computedBorderInsets?.right ?: 0f),
93-
bounds.bottom - (computedBorderInsets?.bottom ?: 0f),
94-
)
95-
val paddingBoxRadii =
96-
computedBorderRadii?.let {
97-
floatArrayOf(
98-
innerRadius(it.topLeft.horizontal, computedBorderInsets?.left),
99-
innerRadius(it.topLeft.vertical, computedBorderInsets?.top),
100-
innerRadius(it.topRight.horizontal, computedBorderInsets?.right),
101-
innerRadius(it.topRight.vertical, computedBorderInsets?.top),
102-
innerRadius(it.bottomRight.horizontal, computedBorderInsets?.right),
103-
innerRadius(it.bottomRight.vertical, computedBorderInsets?.bottom),
104-
innerRadius(it.bottomLeft.horizontal, computedBorderInsets?.left),
105-
innerRadius(it.bottomLeft.vertical, computedBorderInsets?.bottom),
106-
)
107-
}
141+
paddingBoxRect.set(
142+
bounds.left + (computedBorderInsets?.left ?: 0f),
143+
bounds.top + (computedBorderInsets?.top ?: 0f),
144+
bounds.right - (computedBorderInsets?.right ?: 0f),
145+
bounds.bottom - (computedBorderInsets?.bottom ?: 0f),
146+
)
147+
hasPaddingBoxRadii = computedBorderRadii != null
148+
if (computedBorderRadii != null) {
149+
paddingBoxRadii[0] =
150+
innerRadius(computedBorderRadii.topLeft.horizontal, computedBorderInsets?.left)
151+
paddingBoxRadii[1] =
152+
innerRadius(computedBorderRadii.topLeft.vertical, computedBorderInsets?.top)
153+
paddingBoxRadii[2] =
154+
innerRadius(computedBorderRadii.topRight.horizontal, computedBorderInsets?.right)
155+
paddingBoxRadii[3] =
156+
innerRadius(computedBorderRadii.topRight.vertical, computedBorderInsets?.top)
157+
paddingBoxRadii[4] =
158+
innerRadius(computedBorderRadii.bottomRight.horizontal, computedBorderInsets?.right)
159+
paddingBoxRadii[5] =
160+
innerRadius(computedBorderRadii.bottomRight.vertical, computedBorderInsets?.bottom)
161+
paddingBoxRadii[6] =
162+
innerRadius(computedBorderRadii.bottomLeft.horizontal, computedBorderInsets?.left)
163+
paddingBoxRadii[7] =
164+
innerRadius(computedBorderRadii.bottomLeft.vertical, computedBorderInsets?.bottom)
165+
}
108166

109167
val x = offsetX.dpToPx()
110168
val y = offsetY.dpToPx()
111169
val spreadExtent = spread.dpToPx()
112-
val innerRect =
113-
RectF(paddingBoxRect).apply {
114-
if (2 * spreadExtent > paddingBoxRect.width()) {
115-
setEmpty()
116-
} else {
117-
inset(spreadExtent, spreadExtent)
118-
}
119-
offset(x, y)
120-
}
170+
innerRect.set(paddingBoxRect)
171+
if (2 * spreadExtent > paddingBoxRect.width()) {
172+
innerRect.setEmpty()
173+
} else {
174+
innerRect.inset(spreadExtent, spreadExtent)
175+
}
176+
innerRect.offset(x, y)
121177

122178
// Graciously stolen from Blink
123179
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/paint/box_painter_base.cc;l=338;drc=0a301506035e13015ea5c8dd39164d0d5954fa60
124180
val blurExtent = FilterHelper.sigmaToRadius(blurRadius)
125-
val outerRect =
126-
RectF(innerRect).apply {
127-
set(paddingBoxRect)
128-
inset(-blurExtent, -blurExtent)
129-
union(RectF(innerRect))
130-
}
131-
132-
canvas.save().let { saveCount ->
133-
if (paddingBoxRadii != null) {
134-
canvas.clipPath(
135-
Path().apply { addRoundRect(paddingBoxRect, paddingBoxRadii, Path.Direction.CW) },
136-
)
137-
138-
val innerRadii =
139-
paddingBoxRadii.map { adjustRadiusForSpread(it, -spreadExtent) }.toFloatArray()
140-
141-
canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, innerRadii, shadowPaint)
142-
} else {
143-
canvas.clipRect(paddingBoxRect)
144-
canvas.drawDoubleRoundRect(outerRect, ZERO_RADII, innerRect, ZERO_RADII, shadowPaint)
181+
outerRect.set(paddingBoxRect)
182+
outerRect.inset(-blurExtent, -blurExtent)
183+
outerRect.union(innerRect)
184+
185+
if (hasPaddingBoxRadii) {
186+
paddingBoxPath.rewind()
187+
paddingBoxPath.addRoundRect(paddingBoxRect, paddingBoxRadii, Path.Direction.CW)
188+
for (index in innerRadii.indices) {
189+
innerRadii[index] = adjustRadiusForSpread(paddingBoxRadii[index], -spreadExtent)
145190
}
146-
147-
canvas.restoreToCount(saveCount)
148191
}
149192
}
150193

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/drawable/OutsetBoxShadowDrawable.kt‎

Lines changed: 73 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import android.graphics.ColorFilter
1515
import android.graphics.Paint
1616
import android.graphics.Path
1717
import android.graphics.PixelFormat
18+
import android.graphics.Rect
1819
import android.graphics.RectF
1920
import android.graphics.drawable.Drawable
2021
import androidx.annotation.RequiresApi
@@ -42,11 +43,7 @@ internal class OutsetBoxShadowDrawable(
4243
private val offsetY: Float,
4344
private val blurRadius: Float,
4445
private val spread: Float,
45-
/*
46-
* We assume borderRadius to be shared across multiple drawables
47-
* therefore we should manually invalidate this drawable when changing it
48-
*/
49-
var borderRadius: BorderRadiusStyle? = null,
46+
borderRadius: BorderRadiusStyle? = null,
5047
) : Drawable() {
5148
private val shadowPaint =
5249
Paint().apply {
@@ -57,6 +54,34 @@ internal class OutsetBoxShadowDrawable(
5754
}
5855
}
5956

57+
private var needUpdatePath = true
58+
private val shadowRect = RectF()
59+
private var hasRoundedBorders = false
60+
private val subpixelInsetBounds = RectF()
61+
private val radii = FloatArray(8)
62+
private val clipOutPath = Path()
63+
private val shadowPath = Path()
64+
65+
/*
66+
* We assume borderRadius to be shared across multiple drawables
67+
* therefore we should assign it to this drawable again when changing it
68+
*/
69+
var borderRadius: BorderRadiusStyle? = borderRadius
70+
set(value) {
71+
field = value
72+
needUpdatePath = true
73+
}
74+
75+
override fun onBoundsChange(bounds: Rect) {
76+
super.onBoundsChange(bounds)
77+
needUpdatePath = true
78+
}
79+
80+
override fun onLayoutDirectionChanged(layoutDirection: Int): Boolean {
81+
needUpdatePath = true
82+
return true
83+
}
84+
6085
override fun setAlpha(alpha: Int) {
6186
shadowPaint.alpha = (((alpha / 255f) * (Color.alpha(shadowColor) / 255f)) * 255f).roundToInt()
6287
invalidateSelf()
@@ -78,6 +103,26 @@ internal class OutsetBoxShadowDrawable(
78103
}
79104

80105
override fun draw(canvas: Canvas) {
106+
updatePath()
107+
108+
canvas.save().let { saveCount ->
109+
if (hasRoundedBorders) {
110+
canvas.clipOutPath(clipOutPath)
111+
canvas.drawPath(shadowPath, shadowPaint)
112+
} else {
113+
canvas.clipOutRect(bounds)
114+
canvas.drawRect(shadowRect, shadowPaint)
115+
}
116+
canvas.restoreToCount(saveCount)
117+
}
118+
}
119+
120+
private fun updatePath() {
121+
if (!needUpdatePath) {
122+
return
123+
}
124+
needUpdatePath = false
125+
81126
val resolutionWidth = bounds.width().toFloat().pxToDp()
82127
val resolutionHeight = bounds.height().toFloat().pxToDp()
83128
val computedBorderRadii =
@@ -94,74 +139,35 @@ internal class OutsetBoxShadowDrawable(
94139
}
95140

96141
val spreadExtent = spread.dpToPx()
97-
val shadowRect =
98-
RectF(bounds).apply {
99-
inset(-spreadExtent, -spreadExtent)
100-
offset(offsetX.dpToPx(), offsetY.dpToPx())
101-
}
142+
shadowRect.set(bounds)
143+
shadowRect.inset(-spreadExtent, -spreadExtent)
144+
shadowRect.offset(offsetX.dpToPx(), offsetY.dpToPx())
102145

103-
canvas.save().let { saveCount ->
104-
if (computedBorderRadii?.hasRoundedBorders() == true) {
105-
drawShadowRoundRect(canvas, shadowRect, spreadExtent, computedBorderRadii)
106-
} else {
107-
drawShadowRect(canvas, shadowRect)
108-
}
109-
canvas.restoreToCount(saveCount)
146+
hasRoundedBorders = computedBorderRadii?.hasRoundedBorders() == true
147+
if (computedBorderRadii == null || !hasRoundedBorders) {
148+
return
110149
}
111-
}
112150

113-
private fun drawShadowRoundRect(
114-
canvas: Canvas,
115-
shadowRect: RectF,
116-
spreadExtent: Float,
117-
computedBorderRadii: ComputedBorderRadius,
118-
) {
119151
// We inset the clip slightly, to avoid Skia artifacts with antialiased
120152
// clipping. This inset is only visible when no background is present.
121153
// https://neugierig.org/software/chromium/notes/2010/07/clipping.html
122-
val subpixelInsetBounds = RectF(bounds).apply { inset(0.4f, 0.4f) }
123-
canvas.clipOutPath(
124-
Path().apply {
125-
addRoundRect(
126-
subpixelInsetBounds,
127-
floatArrayOf(
128-
computedBorderRadii.topLeft.horizontal,
129-
computedBorderRadii.topLeft.vertical,
130-
computedBorderRadii.topRight.horizontal,
131-
computedBorderRadii.topRight.vertical,
132-
computedBorderRadii.bottomRight.horizontal,
133-
computedBorderRadii.bottomRight.vertical,
134-
computedBorderRadii.bottomLeft.horizontal,
135-
computedBorderRadii.bottomLeft.vertical,
136-
),
137-
Path.Direction.CW,
138-
)
139-
},
140-
)
141-
142-
canvas.drawPath(
143-
Path().apply {
144-
addRoundRect(
145-
shadowRect,
146-
floatArrayOf(
147-
adjustRadiusForSpread(computedBorderRadii.topLeft.horizontal, spreadExtent),
148-
adjustRadiusForSpread(computedBorderRadii.topLeft.vertical, spreadExtent),
149-
adjustRadiusForSpread(computedBorderRadii.topRight.horizontal, spreadExtent),
150-
adjustRadiusForSpread(computedBorderRadii.topRight.vertical, spreadExtent),
151-
adjustRadiusForSpread(computedBorderRadii.bottomRight.horizontal, spreadExtent),
152-
adjustRadiusForSpread(computedBorderRadii.bottomRight.vertical, spreadExtent),
153-
adjustRadiusForSpread(computedBorderRadii.bottomLeft.horizontal, spreadExtent),
154-
adjustRadiusForSpread(computedBorderRadii.bottomLeft.vertical, spreadExtent),
155-
),
156-
Path.Direction.CW,
157-
)
158-
},
159-
shadowPaint,
160-
)
161-
}
154+
subpixelInsetBounds.set(bounds)
155+
subpixelInsetBounds.inset(0.4f, 0.4f)
156+
radii[0] = computedBorderRadii.topLeft.horizontal
157+
radii[1] = computedBorderRadii.topLeft.vertical
158+
radii[2] = computedBorderRadii.topRight.horizontal
159+
radii[3] = computedBorderRadii.topRight.vertical
160+
radii[4] = computedBorderRadii.bottomRight.horizontal
161+
radii[5] = computedBorderRadii.bottomRight.vertical
162+
radii[6] = computedBorderRadii.bottomLeft.horizontal
163+
radii[7] = computedBorderRadii.bottomLeft.vertical
164+
clipOutPath.rewind()
165+
clipOutPath.addRoundRect(subpixelInsetBounds, radii, Path.Direction.CW)
162166

163-
private fun drawShadowRect(canvas: Canvas, shadowRect: RectF) {
164-
canvas.clipOutRect(bounds)
165-
canvas.drawRect(shadowRect, shadowPaint)
167+
for (index in radii.indices) {
168+
radii[index] = adjustRadiusForSpread(radii[index], spreadExtent)
169+
}
170+
shadowPath.rewind()
171+
shadowPath.addRoundRect(shadowRect, radii, Path.Direction.CW)
166172
}
167173
}

0 commit comments

Comments
 (0)