Skip to content

Commit fba24c9

Browse files
android radial gradient
1 parent d1a4d6a commit fba24c9

File tree

5 files changed

+594
-208
lines changed

5 files changed

+594
-208
lines changed

packages/react-native/React/Fabric/Utils/RCTRadialGradient.mm

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919

2020
static RadiusVector RadiusToSide(CGFloat centerX, CGFloat centerY, CGFloat width, CGFloat height,
2121
bool isCircle, RadialGradientSize::SizeKeyword size) {
22-
CGFloat radiusXFromLeftSide = std::abs(centerX);
23-
CGFloat radiusYFromTopSide = std::abs(centerY);
24-
CGFloat radiusXFromRightSide = std::abs(centerX - width);
25-
CGFloat radiusYFromBottomSide = std::abs(centerY - height);
22+
CGFloat radiusXFromLeftSide = centerX;
23+
CGFloat radiusYFromTopSide = centerY;
24+
CGFloat radiusXFromRightSide = width - centerX;
25+
CGFloat radiusYFromBottomSide = height - centerY;
2626
CGFloat radiusX;
2727
CGFloat radiusY;
2828

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

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

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,41 +10,70 @@ package com.facebook.react.uimanager.style
1010
import android.content.Context
1111
import android.graphics.Rect
1212
import android.graphics.Shader
13+
import com.facebook.react.bridge.Arguments
1314
import com.facebook.react.bridge.ReadableMap
15+
import com.facebook.react.bridge.ReadableType
1416

1517
internal class Gradient(gradient: ReadableMap?, context: Context) {
1618
private enum class GradientType {
17-
LINEAR_GRADIENT
19+
LINEAR_GRADIENT,
20+
RADIAL_GRADIENT
1821
}
1922

2023
private val type: GradientType
21-
private val linearGradient: LinearGradient
24+
private val linearGradient: LinearGradient?
25+
private val radialGradient: RadialGradient?
2226

2327
init {
2428
gradient ?: throw IllegalArgumentException("Gradient cannot be null")
2529

2630
val typeString = gradient.getString("type")
2731
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")
32+
when (typeString) {
33+
"linearGradient" -> GradientType.LINEAR_GRADIENT
34+
"radialGradient" -> GradientType.RADIAL_GRADIENT
35+
else -> throw IllegalArgumentException("Unsupported gradient type: $typeString")
36+
}
3637

3738
val colorStops =
38-
gradient.getArray("colorStops")
39-
?: throw IllegalArgumentException("Invalid colorStops array")
40-
41-
linearGradient = LinearGradient(directionMap, colorStops, context)
39+
gradient.getArray("colorStops")
40+
?: throw IllegalArgumentException("Invalid colorStops array")
41+
when (type) {
42+
GradientType.LINEAR_GRADIENT -> {
43+
val directionMap =
44+
gradient.getMap("direction")
45+
?: throw IllegalArgumentException("Gradient must have direction")
46+
linearGradient = LinearGradient(directionMap, colorStops, context)
47+
radialGradient = null
48+
}
49+
GradientType.RADIAL_GRADIENT -> {
50+
val shape = gradient.getString("shape") ?: "ellipse"
51+
val size = if (gradient.hasKey("size")) {
52+
if (gradient.getType("size") == ReadableType.String) {
53+
val sizeKeyword = gradient.getString("size") ?: "farthest-corner"
54+
val sizeMap = Arguments.createMap()
55+
sizeMap.putString("keyword", sizeKeyword)
56+
sizeMap
57+
} else {
58+
gradient.getMap("size")
59+
}
60+
} else {
61+
null
62+
}
63+
val positionMap = gradient.getMap("position")
64+
radialGradient = RadialGradient(shape, size, positionMap, colorStops, context)
65+
linearGradient = null
66+
}
67+
}
4268
}
4369

44-
fun getShader(bounds: Rect): Shader? {
45-
return when (type) {
46-
GradientType.LINEAR_GRADIENT ->
47-
linearGradient.getShader(bounds.width().toFloat(), bounds.height().toFloat())
70+
fun getShader(bounds: Rect): Shader? {
71+
return when (type) {
72+
GradientType.LINEAR_GRADIENT ->
73+
linearGradient?.getShader(bounds.width().toFloat(), bounds.height().toFloat())
74+
75+
GradientType.RADIAL_GRADIENT ->
76+
radialGradient?.getShader(bounds.width().toFloat(), bounds.height().toFloat())
77+
}
4878
}
49-
}
5079
}

0 commit comments

Comments
 (0)