88package com.facebook.react.uimanager.style
99
1010import android.content.Context
11+ import android.graphics.Color
1112import android.graphics.Rect
1213import android.graphics.Shader
14+ import androidx.core.graphics.ColorUtils
1315import com.facebook.react.bridge.ColorPropConverter
16+ import com.facebook.react.bridge.ReadableArray
1417import com.facebook.react.bridge.ReadableMap
1518import 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
1727internal 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}
0 commit comments