-
Notifications
You must be signed in to change notification settings - Fork 24.6k
feat: radial gradient android changes #50269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
0d286c8
bc75900
84d8883
2c17c44
e28db5a
d978a9b
cb62d34
74adb21
538ef70
b197aae
e175879
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,19 +11,22 @@ import android.content.Context | |
import android.graphics.Rect | ||
import android.graphics.Shader | ||
import com.facebook.react.bridge.ReadableMap | ||
import com.facebook.react.bridge.ReadableType | ||
|
||
public class BackgroundImageLayer(gradientMap: ReadableMap?, context: Context) { | ||
private val gradient: Gradient? = | ||
if (gradientMap != null) { | ||
try { | ||
Gradient(gradientMap, context) | ||
} catch (e: IllegalArgumentException) { | ||
// Gracefully reject invalid styles | ||
null | ||
} | ||
} else { | ||
null | ||
} | ||
private val gradient: Gradient? = parseGradient(gradientMap, context) | ||
|
||
public fun getShader(bounds: Rect): Shader? = gradient?.getShader(bounds) | ||
private fun parseGradient(gradientMap: ReadableMap?, context: Context): Gradient? { | ||
if (gradientMap == null) return null | ||
if (!gradientMap.hasKey("type") || gradientMap.getType("type") != ReadableType.String) return null | ||
|
||
return when (gradientMap.getString("type")) { | ||
"linear-gradient" -> LinearGradient.parse(gradientMap, context) | ||
"radial-gradient" -> RadialGradient.parse(gradientMap, context) | ||
else -> null | ||
} | ||
} | ||
|
||
public fun getShader(bounds: Rect): Shader? = | ||
gradient?.getShader(bounds.width().toFloat(), bounds.height().toFloat()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Backing up a bit, wondering what the possible null state means here? I.e. why can we have a no-op There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If parsing any gradient fails we return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of allowing constructing a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
/* | ||
* Copyright (c) Meta Platforms, Inc. and affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
*/ | ||
|
||
import androidx.core.graphics.ColorUtils | ||
import com.facebook.react.uimanager.FloatUtil | ||
import com.facebook.react.uimanager.LengthPercentage | ||
import com.facebook.react.uimanager.LengthPercentageType | ||
import com.facebook.react.uimanager.PixelUtil | ||
import kotlin.math.ln | ||
|
||
// ColorStop type is passed by user, so color and position both could be null. | ||
// e.g. | ||
// color is null in transition hint syntax: (red, 20%, green) | ||
// position can be null too (red 20%, green, purple) | ||
internal class ColorStop(var color: Int? = null, val position: LengthPercentage? = null) | ||
|
||
// ProcessedColorStop type describes type after processing. | ||
// Here both types are nullable to keep it convenient for the color stop fix up algorithm. | ||
// Final Color stop will have both non-null, we check for non null after calling getFixedColorStop. | ||
internal class ProcessedColorStop(var color: Int? = null, val position: Float? = null) | ||
|
||
internal object ColorStopUtils { | ||
public fun getFixedColorStops( | ||
colorStops: List<ColorStop>, | ||
gradientLineLength: Float | ||
): List<ProcessedColorStop> { | ||
val fixedColorStops = Array<ProcessedColorStop>(colorStops.size) { ProcessedColorStop() } | ||
var hasNullPositions = false | ||
var maxPositionSoFar = | ||
resolveColorStopPosition(colorStops[0].position, gradientLineLength) ?: 0f | ||
|
||
for (i in colorStops.indices) { | ||
val colorStop = colorStops[i] | ||
var newPosition = resolveColorStopPosition(colorStop.position, gradientLineLength) | ||
|
||
// Step 1: | ||
// If the first color stop does not have a position, | ||
// set its position to 0%. If the last color stop does not have a position, | ||
// set its position to 100%. | ||
newPosition = | ||
newPosition | ||
?: when (i) { | ||
0 -> 0f | ||
colorStops.size - 1 -> 1f | ||
else -> null | ||
} | ||
|
||
// Step 2: | ||
// If a color stop or transition hint has a position | ||
// that is less than the specified position of any color stop or transition hint | ||
// before it in the list, set its position to be equal to the | ||
// largest specified position of any color stop or transition hint before it. | ||
if (newPosition != null) { | ||
newPosition = maxOf(newPosition, maxPositionSoFar) | ||
fixedColorStops[i] = ProcessedColorStop(colorStop.color, newPosition) | ||
maxPositionSoFar = newPosition | ||
} else { | ||
hasNullPositions = true | ||
} | ||
} | ||
|
||
// Step 3: | ||
// If any color stop still does not have a position, | ||
// then, for each run of adjacent color stops without positions, | ||
// set their positions so that they are evenly spaced between the preceding and | ||
// following color stops with positions. | ||
if (hasNullPositions) { | ||
var lastDefinedIndex = 0 | ||
for (i in 1 until fixedColorStops.size) { | ||
val endPosition = fixedColorStops[i].position | ||
if (endPosition != null) { | ||
val unpositionedStops = i - lastDefinedIndex - 1 | ||
if (unpositionedStops > 0) { | ||
val startPosition = fixedColorStops[lastDefinedIndex].position | ||
if (startPosition != null) { | ||
val increment = (endPosition - startPosition) / (unpositionedStops + 1) | ||
for (j in 1..unpositionedStops) { | ||
fixedColorStops[lastDefinedIndex + j] = | ||
ProcessedColorStop( | ||
colorStops[lastDefinedIndex + j].color, startPosition + increment * j | ||
) | ||
} | ||
} | ||
} | ||
lastDefinedIndex = i | ||
} | ||
} | ||
} | ||
|
||
return processColorTransitionHints(fixedColorStops) | ||
} | ||
|
||
// Spec: https://drafts.csswg.org/css-images-4/#coloring-gradient-line (Refer transition hint section) | ||
// Browsers add 9 intermediate color stops when a transition hint is present | ||
// 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). | ||
private fun processColorTransitionHints( | ||
originalStops: Array<ProcessedColorStop> | ||
): List<ProcessedColorStop> { | ||
val colorStops = originalStops.toMutableList() | ||
var indexOffset = 0 | ||
|
||
for (i in 1 until originalStops.size - 1) { | ||
// Skip if not a color hint | ||
if (originalStops[i].color != null) { | ||
continue | ||
} | ||
|
||
val x = i + indexOffset | ||
if (x < 1) { | ||
continue | ||
} | ||
|
||
val offsetLeft = colorStops[x - 1].position | ||
val offsetRight = colorStops[x + 1].position | ||
val offset = colorStops[x].position | ||
if (offsetLeft == null || offsetRight == null || offset == null) { | ||
continue | ||
} | ||
val leftDist = offset - offsetLeft | ||
val rightDist = offsetRight - offset | ||
val totalDist = offsetRight - offsetLeft | ||
val leftColor = colorStops[x - 1].color | ||
val rightColor = colorStops[x + 1].color | ||
|
||
if (FloatUtil.floatsEqual(leftDist, rightDist)) { | ||
colorStops.removeAt(x) | ||
--indexOffset | ||
continue | ||
} | ||
|
||
if (FloatUtil.floatsEqual(leftDist, 0f)) { | ||
colorStops[x].color = rightColor | ||
continue | ||
} | ||
|
||
if (FloatUtil.floatsEqual(rightDist, 0f)) { | ||
colorStops[x].color = leftColor | ||
continue | ||
} | ||
|
||
val newStops = ArrayList<ProcessedColorStop>(9) | ||
|
||
// Position the new color stops | ||
if (leftDist > rightDist) { | ||
for (y in 0..6) { | ||
newStops.add(ProcessedColorStop(null, offsetLeft + leftDist * ((7f + y) / 13f))) | ||
} | ||
newStops.add(ProcessedColorStop(null, offset + rightDist * (1f / 3f))) | ||
newStops.add(ProcessedColorStop(null, offset + rightDist * (2f / 3f))) | ||
} else { | ||
newStops.add(ProcessedColorStop(null, offsetLeft + leftDist * (1f / 3f))) | ||
newStops.add(ProcessedColorStop(null, offsetLeft + leftDist * (2f / 3f))) | ||
for (y in 0..6) { | ||
newStops.add(ProcessedColorStop(null, offset + rightDist * (y / 13f))) | ||
} | ||
} | ||
|
||
// Calculate colors for the new stops | ||
val hintRelativeOffset = leftDist / totalDist | ||
val logRatio = ln(0.5) / ln(hintRelativeOffset) | ||
|
||
for (newStop in newStops) { | ||
if (newStop.position == null) { | ||
continue | ||
} | ||
val pointRelativeOffset = (newStop.position - offsetLeft) / totalDist | ||
val weighting = Math.pow(pointRelativeOffset.toDouble(), logRatio).toFloat() | ||
|
||
if (!weighting.isFinite() || weighting.isNaN()) { | ||
continue | ||
} | ||
|
||
// Interpolate color using the calculated weighting | ||
leftColor?.let { left -> | ||
rightColor?.let { right -> newStop.color = ColorUtils.blendARGB(left, right, weighting) } | ||
} | ||
} | ||
|
||
// Replace the color hint with new color stops | ||
colorStops.removeAt(x) | ||
colorStops.addAll(x, newStops) | ||
indexOffset += 8 | ||
} | ||
|
||
return colorStops | ||
} | ||
|
||
private fun resolveColorStopPosition( | ||
position: LengthPercentage?, | ||
gradientLineLength: Float | ||
): Float? { | ||
if (position == null) return null | ||
|
||
return when (position.type) { | ||
LengthPercentageType.POINT -> | ||
PixelUtil.toPixelFromDIP(position.resolve(0f)) / gradientLineLength | ||
|
||
LengthPercentageType.PERCENT -> position.resolve(1f) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: can we make this
internal
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we're using this in
BackgroundStyleApplicator
so need to keep it public! 😕There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not quite following. E.g.
BackgroundStyleApplicator
as the public API relies on private internals likeOutsetBoxShadowDrawable
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the issue is that it is being exposed as an argument to a public function. So when making it
internal
it results in below.