Skip to content

Commit a240994

Browse files
feat: radial gradient android changes (#50269)
Summary: Adds android changes for radial gradient. Previous PR - #50209 ## Changelog: [ANDROID] [ADDED] - Radial gradient <!-- Help reviewers and the release process by writing your own changelog entry. Pick one each for the category and type tags: For more details, see: https://reactnative.dev/contributing/changelogs-in-pull-requests Pull Request resolved: #50269 Test Plan: - Added tests in processBackgroundImage-test.js in #50268 - Merge this and [this](#50268) and check examples in `RadialGradientExample.js` Reviewed By: NickGerleman Differential Revision: D71898546 Pulled By: jorge-cab fbshipit-source-id: 2872bcf16a492c7bc829be10eedb0f6c7dad32c7
1 parent d7533dc commit a240994

File tree

9 files changed

+853
-300
lines changed

9 files changed

+853
-300
lines changed

packages/react-native/ReactAndroid/api/ReactAndroid.api

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5146,10 +5146,16 @@ public abstract interface class com/facebook/react/uimanager/layoutanimation/Lay
51465146
}
51475147

51485148
public final class com/facebook/react/uimanager/style/BackgroundImageLayer {
5149-
public fun <init> (Lcom/facebook/react/bridge/ReadableMap;Landroid/content/Context;)V
5149+
public static final field Companion Lcom/facebook/react/uimanager/style/BackgroundImageLayer$Companion;
5150+
public fun <init> ()V
5151+
public synthetic fun <init> (Lcom/facebook/react/uimanager/style/Gradient;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
51505152
public final fun getShader (Landroid/graphics/Rect;)Landroid/graphics/Shader;
51515153
}
51525154

5155+
public final class com/facebook/react/uimanager/style/BackgroundImageLayer$Companion {
5156+
public final fun parse (Lcom/facebook/react/bridge/ReadableMap;Landroid/content/Context;)Lcom/facebook/react/uimanager/style/BackgroundImageLayer;
5157+
}
5158+
51535159
public final class com/facebook/react/uimanager/style/BorderRadiusProp : java/lang/Enum {
51545160
public static final field BORDER_BOTTOM_END_RADIUS Lcom/facebook/react/uimanager/style/BorderRadiusProp;
51555161
public static final field BORDER_BOTTOM_LEFT_RADIUS Lcom/facebook/react/uimanager/style/BorderRadiusProp;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ internal class BackgroundDrawable(
153153
backgroundImageLayers?.let { layers ->
154154
var compositeShader: Shader? = null
155155
for (backgroundImageLayer in layers) {
156-
val currentShader = backgroundImageLayer.getShader(bounds) ?: continue
156+
val currentShader = backgroundImageLayer.getShader(bounds)
157157

158158
compositeShader =
159159
if (compositeShader == null) {

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/BackgroundImageLayer.kt

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,37 @@ import android.content.Context
1111
import android.graphics.Rect
1212
import android.graphics.Shader
1313
import com.facebook.react.bridge.ReadableMap
14+
import com.facebook.react.bridge.ReadableType
1415

15-
public class BackgroundImageLayer(gradientMap: ReadableMap?, context: Context) {
16-
private val gradient: Gradient? =
17-
if (gradientMap != null) {
18-
try {
19-
Gradient(gradientMap, context)
20-
} catch (e: IllegalArgumentException) {
21-
// Gracefully reject invalid styles
22-
null
23-
}
24-
} else {
25-
null
16+
public class BackgroundImageLayer() {
17+
private lateinit var gradient: Gradient
18+
19+
private constructor(gradient: Gradient) : this() {
20+
this.gradient = gradient
21+
}
22+
23+
public companion object {
24+
public fun parse(gradientMap: ReadableMap?, context: Context): BackgroundImageLayer? {
25+
if (gradientMap == null) {
26+
return null
27+
}
28+
val gradient = parseGradient(gradientMap, context) ?: return null
29+
return BackgroundImageLayer(gradient)
30+
}
31+
32+
private fun parseGradient(gradientMap: ReadableMap, context: Context): Gradient? {
33+
if (!gradientMap.hasKey("type") || gradientMap.getType("type") != ReadableType.String) {
34+
return null
35+
}
36+
37+
return when (gradientMap.getString("type")) {
38+
"linear-gradient" -> LinearGradient.parse(gradientMap, context)
39+
"radial-gradient" -> RadialGradient.parse(gradientMap, context)
40+
else -> null
2641
}
42+
}
43+
}
2744

28-
public fun getShader(bounds: Rect): Shader? = gradient?.getShader(bounds)
45+
public fun getShader(bounds: Rect): Shader =
46+
gradient.getShader(bounds.width().toFloat(), bounds.height().toFloat())
2947
}
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
package com.facebook.react.uimanager.style
9+
10+
import androidx.core.graphics.ColorUtils
11+
import com.facebook.react.uimanager.FloatUtil
12+
import com.facebook.react.uimanager.LengthPercentage
13+
import com.facebook.react.uimanager.LengthPercentageType
14+
import com.facebook.react.uimanager.PixelUtil
15+
import kotlin.math.ln
16+
17+
// ColorStop type is passed by user, so color and position both could be null.
18+
// e.g.
19+
// color is null in transition hint syntax: (red, 20%, green)
20+
// position can be null too (red 20%, green, purple)
21+
internal class ColorStop(var color: Int? = null, val position: LengthPercentage? = null)
22+
23+
// ProcessedColorStop type describes type after processing.
24+
// Here both types are nullable to keep it convenient for the color stop fix up algorithm.
25+
// Final Color stop will have both non-null, we check for non null after calling getFixedColorStop.
26+
internal class ProcessedColorStop(var color: Int? = null, val position: Float? = null)
27+
28+
internal object ColorStopUtils {
29+
public fun getFixedColorStops(
30+
colorStops: List<ColorStop>,
31+
gradientLineLength: Float
32+
): List<ProcessedColorStop> {
33+
val fixedColorStops = Array<ProcessedColorStop>(colorStops.size) { ProcessedColorStop() }
34+
var hasNullPositions = false
35+
var maxPositionSoFar =
36+
resolveColorStopPosition(colorStops[0].position, gradientLineLength) ?: 0f
37+
38+
for (i in colorStops.indices) {
39+
val colorStop = colorStops[i]
40+
var newPosition = resolveColorStopPosition(colorStop.position, gradientLineLength)
41+
42+
// Step 1:
43+
// If the first color stop does not have a position,
44+
// set its position to 0%. If the last color stop does not have a position,
45+
// set its position to 100%.
46+
newPosition =
47+
newPosition
48+
?: when (i) {
49+
0 -> 0f
50+
colorStops.size - 1 -> 1f
51+
else -> null
52+
}
53+
54+
// Step 2:
55+
// If a color stop or transition hint has a position
56+
// that is less than the specified position of any color stop or transition hint
57+
// before it in the list, set its position to be equal to the
58+
// largest specified position of any color stop or transition hint before it.
59+
if (newPosition != null) {
60+
newPosition = maxOf(newPosition, maxPositionSoFar)
61+
fixedColorStops[i] = ProcessedColorStop(colorStop.color, newPosition)
62+
maxPositionSoFar = newPosition
63+
} else {
64+
hasNullPositions = true
65+
}
66+
}
67+
68+
// Step 3:
69+
// If any color stop still does not have a position,
70+
// then, for each run of adjacent color stops without positions,
71+
// set their positions so that they are evenly spaced between the preceding and
72+
// following color stops with positions.
73+
if (hasNullPositions) {
74+
var lastDefinedIndex = 0
75+
for (i in 1 until fixedColorStops.size) {
76+
val endPosition = fixedColorStops[i].position
77+
val startPosition = fixedColorStops[lastDefinedIndex].position
78+
val unpositionedStops = i - lastDefinedIndex - 1
79+
if (endPosition != null && startPosition != null && unpositionedStops > 0) {
80+
val increment = (endPosition - startPosition) / (unpositionedStops + 1)
81+
for (j in 1..unpositionedStops) {
82+
fixedColorStops[lastDefinedIndex + j] =
83+
ProcessedColorStop(
84+
colorStops[lastDefinedIndex + j].color, startPosition + increment * j)
85+
}
86+
lastDefinedIndex = i
87+
}
88+
}
89+
}
90+
91+
return processColorTransitionHints(fixedColorStops)
92+
}
93+
94+
// Spec: https://drafts.csswg.org/css-images-4/#coloring-gradient-line (Refer transition hint
95+
// section)
96+
// Browsers add 9 intermediate color stops when a transition hint is present
97+
// Algorithm is referred from Blink engine
98+
// [source](https://github.com/chromium/chromium/blob/a296b1bad6dc1ed9d751b7528f7ca2134227b828/third_party/blink/renderer/core/css/css_gradient_value.cc#L240).
99+
private fun processColorTransitionHints(
100+
originalStops: Array<ProcessedColorStop>
101+
): List<ProcessedColorStop> {
102+
val colorStops = originalStops.toMutableList()
103+
var indexOffset = 0
104+
105+
for (i in 1 until originalStops.size - 1) {
106+
// Skip if not a color hint
107+
if (originalStops[i].color != null) {
108+
continue
109+
}
110+
111+
val x = i + indexOffset
112+
if (x < 1) {
113+
continue
114+
}
115+
116+
val offsetLeft = colorStops[x - 1].position
117+
val offsetRight = colorStops[x + 1].position
118+
val offset = colorStops[x].position
119+
if (offsetLeft == null || offsetRight == null || offset == null) {
120+
continue
121+
}
122+
val leftDist = offset - offsetLeft
123+
val rightDist = offsetRight - offset
124+
val totalDist = offsetRight - offsetLeft
125+
val leftColor = colorStops[x - 1].color
126+
val rightColor = colorStops[x + 1].color
127+
128+
if (FloatUtil.floatsEqual(leftDist, rightDist)) {
129+
colorStops.removeAt(x)
130+
--indexOffset
131+
continue
132+
}
133+
134+
if (FloatUtil.floatsEqual(leftDist, 0f)) {
135+
colorStops[x].color = rightColor
136+
continue
137+
}
138+
139+
if (FloatUtil.floatsEqual(rightDist, 0f)) {
140+
colorStops[x].color = leftColor
141+
continue
142+
}
143+
144+
val newStops = ArrayList<ProcessedColorStop>(9)
145+
146+
// Position the new color stops
147+
if (leftDist > rightDist) {
148+
for (y in 0..6) {
149+
newStops.add(ProcessedColorStop(null, offsetLeft + leftDist * ((7f + y) / 13f)))
150+
}
151+
newStops.add(ProcessedColorStop(null, offset + rightDist * (1f / 3f)))
152+
newStops.add(ProcessedColorStop(null, offset + rightDist * (2f / 3f)))
153+
} else {
154+
newStops.add(ProcessedColorStop(null, offsetLeft + leftDist * (1f / 3f)))
155+
newStops.add(ProcessedColorStop(null, offsetLeft + leftDist * (2f / 3f)))
156+
for (y in 0..6) {
157+
newStops.add(ProcessedColorStop(null, offset + rightDist * (y / 13f)))
158+
}
159+
}
160+
161+
// Calculate colors for the new stops
162+
val hintRelativeOffset = leftDist / totalDist
163+
val logRatio = ln(0.5) / ln(hintRelativeOffset)
164+
165+
for (newStop in newStops) {
166+
if (newStop.position == null) {
167+
continue
168+
}
169+
val pointRelativeOffset = (newStop.position - offsetLeft) / totalDist
170+
val weighting = Math.pow(pointRelativeOffset.toDouble(), logRatio).toFloat()
171+
172+
if (!weighting.isFinite() || weighting.isNaN()) {
173+
continue
174+
}
175+
176+
// Interpolate color using the calculated weighting
177+
leftColor?.let { left ->
178+
rightColor?.let { right -> newStop.color = ColorUtils.blendARGB(left, right, weighting) }
179+
}
180+
}
181+
182+
// Replace the color hint with new color stops
183+
colorStops.removeAt(x)
184+
colorStops.addAll(x, newStops)
185+
indexOffset += 8
186+
}
187+
188+
return colorStops
189+
}
190+
191+
private fun resolveColorStopPosition(
192+
position: LengthPercentage?,
193+
gradientLineLength: Float
194+
): Float? {
195+
if (position == null) {
196+
return null
197+
}
198+
199+
return when (position.type) {
200+
LengthPercentageType.POINT ->
201+
PixelUtil.toPixelFromDIP(position.resolve(0f)) / gradientLineLength
202+
203+
LengthPercentageType.PERCENT -> position.resolve(1f)
204+
}
205+
}
206+
}

packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/style/Gradient.kt

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,44 +7,8 @@
77

88
package com.facebook.react.uimanager.style
99

10-
import android.content.Context
11-
import android.graphics.Rect
1210
import android.graphics.Shader
13-
import com.facebook.react.bridge.ReadableMap
1411

15-
internal class Gradient(gradient: ReadableMap?, context: Context) {
16-
private enum class GradientType {
17-
LINEAR_GRADIENT
18-
}
19-
20-
private val type: GradientType
21-
private val linearGradient: LinearGradient
22-
23-
init {
24-
gradient ?: throw IllegalArgumentException("Gradient cannot be null")
25-
26-
val typeString = gradient.getString("type")
27-
type =
28-
when (typeString) {
29-
"linearGradient" -> GradientType.LINEAR_GRADIENT
30-
else -> throw IllegalArgumentException("Unsupported gradient type: $typeString")
31-
}
32-
33-
val directionMap =
34-
gradient.getMap("direction")
35-
?: throw IllegalArgumentException("Gradient must have direction")
36-
37-
val colorStops =
38-
gradient.getArray("colorStops")
39-
?: throw IllegalArgumentException("Invalid colorStops array")
40-
41-
linearGradient = LinearGradient(directionMap, colorStops, context)
42-
}
43-
44-
fun getShader(bounds: Rect): Shader? {
45-
return when (type) {
46-
GradientType.LINEAR_GRADIENT ->
47-
linearGradient.getShader(bounds.width().toFloat(), bounds.height().toFloat())
48-
}
49-
}
12+
internal interface Gradient {
13+
public fun getShader(width: Float, height: Float): Shader
5014
}

0 commit comments

Comments
 (0)