Skip to content

Commit 9e2c636

Browse files
android color hint
1 parent 7ca3007 commit 9e2c636

File tree

2 files changed

+133
-15
lines changed

2 files changed

+133
-15
lines changed

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

Lines changed: 133 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,21 @@
88
package com.facebook.react.uimanager.style
99

1010
import android.content.Context
11+
import android.graphics.Color
1112
import android.graphics.Rect
1213
import android.graphics.Shader
14+
import androidx.core.graphics.ColorUtils
1315
import com.facebook.react.bridge.ColorPropConverter
16+
import com.facebook.react.bridge.ReadableArray
1417
import com.facebook.react.bridge.ReadableMap
1518
import com.facebook.react.bridge.ReadableType
19+
import com.facebook.react.uimanager.FloatUtil
20+
import kotlin.math.ln
21+
22+
private data class ColorStop(
23+
var color: Int? = null,
24+
val position: Float
25+
)
1626

1727
internal class Gradient(gradient: ReadableMap?, context: Context) {
1828
private enum class GradientType {
@@ -36,23 +46,19 @@ internal class Gradient(gradient: ReadableMap?, context: Context) {
3646
gradient.getMap("direction")
3747
?: throw IllegalArgumentException("Gradient must have direction")
3848

39-
val colorStops =
49+
val colorStopsRaw =
4050
gradient.getArray("colorStops")
4151
?: throw IllegalArgumentException("Invalid colorStops array")
4252

43-
val size = colorStops.size()
44-
val colors = IntArray(size)
45-
val positions = FloatArray(size)
46-
47-
for (i in 0 until size) {
48-
val colorStop = colorStops.getMap(i) ?: continue
49-
colors[i] =
50-
if (colorStop.getType("color") == ReadableType.Map) {
51-
ColorPropConverter.getColor(colorStop.getMap("color"), context)
52-
} else {
53-
colorStop.getInt("color")
54-
}
55-
positions[i] = colorStop.getDouble("position").toFloat()
53+
val colorStops = processColorTransitionHints(colorStopsRaw, context);
54+
val colors = IntArray(colorStops.size)
55+
val positions = FloatArray(colorStops.size)
56+
57+
colorStops.forEachIndexed { i, colorStop ->
58+
colorStop.color?.let { color ->
59+
colors[i] = color
60+
positions[i] = colorStop.position
61+
}
5662
}
5763

5864
linearGradient = LinearGradient(directionMap, colors, positions)
@@ -64,4 +70,117 @@ internal class Gradient(gradient: ReadableMap?, context: Context) {
6470
linearGradient.getShader(bounds.width().toFloat(), bounds.height().toFloat())
6571
}
6672
}
73+
74+
// Spec: https://drafts.csswg.org/css-images-4/#coloring-gradient-line (Refer transition hint section)
75+
// Browsers add 9 intermediate color stops when a transition hint is present
76+
// Algorithm is referred from Blink engine [source](https://github.com/chromium/chromium/blob/a296b1bad6dc1ed9d751b7528f7ca2134227b828/third_party/blink/renderer/core/css/css_gradient_value.cc#L240).
77+
private fun processColorTransitionHints(originalStopsArray: ReadableArray, context: Context): List<ColorStop> {
78+
val colorStops = ArrayList<ColorStop>(originalStopsArray.size())
79+
for (i in 0 until originalStopsArray.size()) {
80+
val colorStop = originalStopsArray.getMap(i) ?: continue
81+
val position = colorStop.getDouble("position").toFloat()
82+
val color = if (colorStop.hasKey("color") && !colorStop.isNull("color")) {
83+
if (colorStop.getType("color") == ReadableType.Map) {
84+
ColorPropConverter.getColor(colorStop.getMap("color"), context)
85+
} else {
86+
colorStop.getInt("color")
87+
}
88+
} else null
89+
90+
colorStops.add(ColorStop(color, position))
91+
}
92+
93+
var indexOffset = 0
94+
for (i in 1 until colorStops.size - 1) {
95+
val colorStop = colorStops[i]
96+
// Skip if not a color hint
97+
if (colorStop.color != null) {
98+
continue
99+
}
100+
101+
val x = i + indexOffset
102+
if (x < 1) {
103+
continue
104+
}
105+
106+
val offsetLeft = colorStops[x - 1].position
107+
val offsetRight = colorStops[x + 1].position
108+
val offset = colorStop.position
109+
val leftDist = offset - offsetLeft
110+
val rightDist = offsetRight - offset
111+
val totalDist = offsetRight - offsetLeft
112+
113+
val leftColor = colorStops[x - 1].color ?: Color.TRANSPARENT
114+
val rightColor = colorStops[x + 1].color ?: Color.TRANSPARENT
115+
116+
if (FloatUtil.floatsEqual(leftDist, rightDist)) {
117+
colorStops.removeAt(x)
118+
--indexOffset
119+
continue
120+
}
121+
122+
if (FloatUtil.floatsEqual(leftDist, .0f)) {
123+
colorStop.color = rightColor
124+
continue
125+
}
126+
127+
if (FloatUtil.floatsEqual(rightDist, .0f)) {
128+
colorStop.color = leftColor
129+
continue
130+
}
131+
132+
val newStops = ArrayList<ColorStop>(9)
133+
// Position the new color stops
134+
if (leftDist > rightDist) {
135+
for (y in 0..6) {
136+
newStops.add(ColorStop(
137+
position = offsetLeft + leftDist * ((7f + y) / 13f)
138+
))
139+
}
140+
newStops.add(ColorStop(
141+
position = offset + rightDist * (1f / 3f)
142+
))
143+
newStops.add(ColorStop(
144+
position = offset + rightDist * (2f / 3f)
145+
))
146+
} else {
147+
newStops.add(ColorStop(
148+
position = offsetLeft + leftDist * (1f / 3f)
149+
))
150+
newStops.add(ColorStop(
151+
position = offsetLeft + leftDist * (2f / 3f)
152+
))
153+
for (y in 0..6) {
154+
newStops.add(ColorStop(
155+
position = offset + rightDist * (y / 13f)
156+
))
157+
}
158+
}
159+
160+
// calculate colors for the new color hints.
161+
// The color weighting for the new color stops will be
162+
// pointRelativeOffset^(ln(0.5)/ln(hintRelativeOffset)).
163+
val hintRelativeOffset = leftDist / totalDist
164+
for (newStop in newStops) {
165+
val pointRelativeOffset = (newStop.position - offsetLeft) / totalDist
166+
val weighting = Math.pow(
167+
pointRelativeOffset.toDouble(),
168+
ln(0.5) / ln(hintRelativeOffset.toDouble())
169+
).toFloat()
170+
171+
if (weighting.isInfinite() || weighting.isNaN()) {
172+
continue
173+
}
174+
175+
newStop.color = ColorUtils.blendARGB(leftColor, rightColor, weighting)
176+
}
177+
178+
// Replace the color hint with new color stops.
179+
colorStops.removeAt(x)
180+
colorStops.addAll(x, newStops)
181+
indexOffset += 8
182+
}
183+
184+
return colorStops
185+
}
67186
}

packages/rn-tester/js/examples/LinearGradient/LinearGradientExample.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,6 @@ exports.examples = [
226226
},
227227
{
228228
title: 'Transition hint',
229-
description: 'Linear gradient with transition hint',
230229
render(): React.Node {
231230
return (
232231
<GradientBox

0 commit comments

Comments
 (0)