mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
feat: radial gradient android changes (#50269)
Summary: Adds android changes for radial gradient. Previous PR - https://github.com/facebook/react-native/pull/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: https://github.com/facebook/react-native/pull/50269 Test Plan: - Added tests in processBackgroundImage-test.js in https://github.com/facebook/react-native/pull/50268 - Merge this and [this](https://github.com/facebook/react-native/pull/50268) and check examples in `RadialGradientExample.js` Reviewed By: NickGerleman Differential Revision: D71898546 Pulled By: jorge-cab fbshipit-source-id: 2872bcf16a492c7bc829be10eedb0f6c7dad32c7
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d7533dce1c
commit
a2409941c2
@@ -5146,10 +5146,16 @@ public abstract interface class com/facebook/react/uimanager/layoutanimation/Lay
|
||||
}
|
||||
|
||||
public final class com/facebook/react/uimanager/style/BackgroundImageLayer {
|
||||
public fun <init> (Lcom/facebook/react/bridge/ReadableMap;Landroid/content/Context;)V
|
||||
public static final field Companion Lcom/facebook/react/uimanager/style/BackgroundImageLayer$Companion;
|
||||
public fun <init> ()V
|
||||
public synthetic fun <init> (Lcom/facebook/react/uimanager/style/Gradient;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
|
||||
public final fun getShader (Landroid/graphics/Rect;)Landroid/graphics/Shader;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/uimanager/style/BackgroundImageLayer$Companion {
|
||||
public final fun parse (Lcom/facebook/react/bridge/ReadableMap;Landroid/content/Context;)Lcom/facebook/react/uimanager/style/BackgroundImageLayer;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/uimanager/style/BorderRadiusProp : java/lang/Enum {
|
||||
public static final field BORDER_BOTTOM_END_RADIUS Lcom/facebook/react/uimanager/style/BorderRadiusProp;
|
||||
public static final field BORDER_BOTTOM_LEFT_RADIUS Lcom/facebook/react/uimanager/style/BorderRadiusProp;
|
||||
|
||||
+1
-1
@@ -153,7 +153,7 @@ internal class BackgroundDrawable(
|
||||
backgroundImageLayers?.let { layers ->
|
||||
var compositeShader: Shader? = null
|
||||
for (backgroundImageLayer in layers) {
|
||||
val currentShader = backgroundImageLayer.getShader(bounds) ?: continue
|
||||
val currentShader = backgroundImageLayer.getShader(bounds)
|
||||
|
||||
compositeShader =
|
||||
if (compositeShader == null) {
|
||||
|
||||
+30
-12
@@ -11,19 +11,37 @@ 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
|
||||
public class BackgroundImageLayer() {
|
||||
private lateinit var gradient: Gradient
|
||||
|
||||
private constructor(gradient: Gradient) : this() {
|
||||
this.gradient = gradient
|
||||
}
|
||||
|
||||
public companion object {
|
||||
public fun parse(gradientMap: ReadableMap?, context: Context): BackgroundImageLayer? {
|
||||
if (gradientMap == null) {
|
||||
return null
|
||||
}
|
||||
val gradient = parseGradient(gradientMap, context) ?: return null
|
||||
return BackgroundImageLayer(gradient)
|
||||
}
|
||||
|
||||
private fun parseGradient(gradientMap: ReadableMap, context: Context): Gradient? {
|
||||
if (!gradientMap.hasKey("type") || gradientMap.getType("type") != ReadableType.String) {
|
||||
return null
|
||||
}
|
||||
|
||||
public fun getShader(bounds: Rect): Shader? = gradient?.getShader(bounds)
|
||||
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())
|
||||
}
|
||||
|
||||
+206
@@ -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.
|
||||
*/
|
||||
|
||||
package com.facebook.react.uimanager.style
|
||||
|
||||
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
|
||||
val startPosition = fixedColorStops[lastDefinedIndex].position
|
||||
val unpositionedStops = i - lastDefinedIndex - 1
|
||||
if (endPosition != null && startPosition != null && unpositionedStops > 0) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-38
@@ -7,44 +7,8 @@
|
||||
|
||||
package com.facebook.react.uimanager.style
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Shader
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
|
||||
internal class Gradient(gradient: ReadableMap?, context: Context) {
|
||||
private enum class GradientType {
|
||||
LINEAR_GRADIENT
|
||||
}
|
||||
|
||||
private val type: GradientType
|
||||
private val linearGradient: LinearGradient
|
||||
|
||||
init {
|
||||
gradient ?: throw IllegalArgumentException("Gradient cannot be null")
|
||||
|
||||
val typeString = gradient.getString("type")
|
||||
type =
|
||||
when (typeString) {
|
||||
"linearGradient" -> GradientType.LINEAR_GRADIENT
|
||||
else -> throw IllegalArgumentException("Unsupported gradient type: $typeString")
|
||||
}
|
||||
|
||||
val directionMap =
|
||||
gradient.getMap("direction")
|
||||
?: throw IllegalArgumentException("Gradient must have direction")
|
||||
|
||||
val colorStops =
|
||||
gradient.getArray("colorStops")
|
||||
?: throw IllegalArgumentException("Invalid colorStops array")
|
||||
|
||||
linearGradient = LinearGradient(directionMap, colorStops, context)
|
||||
}
|
||||
|
||||
fun getShader(bounds: Rect): Shader? {
|
||||
return when (type) {
|
||||
GradientType.LINEAR_GRADIENT ->
|
||||
linearGradient.getShader(bounds.width().toFloat(), bounds.height().toFloat())
|
||||
}
|
||||
}
|
||||
internal interface Gradient {
|
||||
public fun getShader(width: Float, height: Float): Shader
|
||||
}
|
||||
|
||||
+76
-250
@@ -10,101 +10,101 @@ package com.facebook.react.uimanager.style
|
||||
import android.content.Context
|
||||
import android.graphics.LinearGradient as AndroidLinearGradient
|
||||
import android.graphics.Shader
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import com.facebook.react.bridge.ColorPropConverter
|
||||
import com.facebook.react.bridge.ReadableArray
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.ReadableType
|
||||
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.atan
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.sqrt
|
||||
import kotlin.math.tan
|
||||
|
||||
private data class ColorStop(var color: Int? = null, val position: LengthPercentage? = null)
|
||||
internal class LinearGradient(val direction: Direction, val colorStops: List<ColorStop>) :
|
||||
Gradient {
|
||||
companion object {
|
||||
fun parse(gradientMap: ReadableMap, context: Context): Gradient? {
|
||||
val direction =
|
||||
gradientMap
|
||||
.takeIf { it.hasKey("direction") }
|
||||
?.let { map ->
|
||||
val directionMap = map.getMap("direction") ?: return null
|
||||
|
||||
private data class ProcessedColorStop(var color: Int? = null, val position: Float? = null)
|
||||
|
||||
internal class LinearGradient(
|
||||
directionMap: ReadableMap,
|
||||
private val colorStopsArray: ReadableArray,
|
||||
private val context: Context
|
||||
) {
|
||||
private sealed class Direction {
|
||||
data class Angle(val value: Double) : Direction()
|
||||
|
||||
enum class Keywords {
|
||||
TO_TOP_RIGHT,
|
||||
TO_BOTTOM_RIGHT,
|
||||
TO_TOP_LEFT,
|
||||
TO_BOTTOM_LEFT
|
||||
}
|
||||
|
||||
data class Keyword(val value: Keywords) : Direction()
|
||||
}
|
||||
|
||||
private val direction: Direction =
|
||||
when (val type = directionMap.getString("type")) {
|
||||
"angle" -> {
|
||||
val angle = directionMap.getDouble("value")
|
||||
Direction.Angle(angle)
|
||||
}
|
||||
|
||||
"keyword" -> {
|
||||
val keyword =
|
||||
when (directionMap.getString("value")) {
|
||||
"to top right" -> Direction.Keywords.TO_TOP_RIGHT
|
||||
"to bottom right" -> Direction.Keywords.TO_BOTTOM_RIGHT
|
||||
"to top left" -> Direction.Keywords.TO_TOP_LEFT
|
||||
"to bottom left" -> Direction.Keywords.TO_BOTTOM_LEFT
|
||||
else ->
|
||||
throw IllegalArgumentException(
|
||||
"Invalid linear gradient direction keyword: ${directionMap.getString("value")}")
|
||||
when (directionMap.getString("type")) {
|
||||
"angle" -> {
|
||||
val angle = directionMap.getDouble("value")
|
||||
Direction.Angle(angle)
|
||||
}
|
||||
"keyword" ->
|
||||
Direction.KeywordType.fromString(directionMap.getString("value"))?.let {
|
||||
keywordType ->
|
||||
Direction.Keyword(keywordType)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
Direction.Keyword(keyword)
|
||||
}
|
||||
|
||||
else -> throw IllegalArgumentException("Invalid direction type: $type")
|
||||
val colorStops =
|
||||
gradientMap
|
||||
.takeIf { it.hasKey("colorStops") }
|
||||
?.let { map ->
|
||||
val colorStopsArray = map.getArray("colorStops") ?: return null
|
||||
|
||||
val stops = ArrayList<ColorStop>(colorStopsArray.size())
|
||||
for (i in 0 until colorStopsArray.size()) {
|
||||
val colorStop = colorStopsArray.getMap(i) ?: continue
|
||||
val color: Int? =
|
||||
when {
|
||||
!colorStop.hasKey("color") || colorStop.isNull("color") -> {
|
||||
null
|
||||
}
|
||||
colorStop.getType("color") == ReadableType.Map -> {
|
||||
ColorPropConverter.getColor(colorStop.getMap("color"), context)
|
||||
}
|
||||
else -> colorStop.getInt("color")
|
||||
}
|
||||
val colorStopPosition =
|
||||
LengthPercentage.setFromDynamic(colorStop.getDynamic("position"))
|
||||
stops.add(ColorStop(color, colorStopPosition))
|
||||
}
|
||||
stops
|
||||
}
|
||||
|
||||
if (direction != null && colorStops != null) {
|
||||
return LinearGradient(direction, colorStops)
|
||||
}
|
||||
|
||||
private val colorStops: ArrayList<ColorStop> = run {
|
||||
val stops = ArrayList<ColorStop>(colorStopsArray.size())
|
||||
for (i in 0 until colorStopsArray.size()) {
|
||||
val colorStop = colorStopsArray.getMap(i) ?: continue
|
||||
val color: Int? =
|
||||
when {
|
||||
!colorStop.hasKey("color") || colorStop.isNull("color") -> {
|
||||
null
|
||||
}
|
||||
colorStop.getType("color") == ReadableType.Map -> {
|
||||
ColorPropConverter.getColor(colorStop.getMap("color"), context)
|
||||
}
|
||||
else -> colorStop.getInt("color")
|
||||
}
|
||||
|
||||
val position = LengthPercentage.setFromDynamic(colorStop.getDynamic("position"))
|
||||
|
||||
stops.add(ColorStop(color, position))
|
||||
return null
|
||||
}
|
||||
stops
|
||||
}
|
||||
|
||||
fun getShader(width: Float, height: Float): Shader {
|
||||
sealed class Direction {
|
||||
class Angle(val angle: Double) : Direction()
|
||||
|
||||
class Keyword(val keyword: KeywordType) : Direction()
|
||||
|
||||
enum class KeywordType(val value: String) {
|
||||
TO_TOP_RIGHT("to top right"),
|
||||
TO_BOTTOM_RIGHT("to bottom right"),
|
||||
TO_TOP_LEFT("to top left"),
|
||||
TO_BOTTOM_LEFT("to bottom left");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String?) = enumValues<KeywordType>().find { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getShader(width: Float, height: Float): Shader {
|
||||
val angle =
|
||||
when (direction) {
|
||||
is Direction.Angle -> direction.value
|
||||
is Direction.Angle -> direction.angle
|
||||
is Direction.Keyword ->
|
||||
getAngleForKeyword(direction.value, width.toDouble(), height.toDouble())
|
||||
getAngleForKeyword(direction.keyword, width.toDouble(), height.toDouble())
|
||||
}
|
||||
val (startPoint, endPoint) = endPointsFromAngle(angle, height, width)
|
||||
val dx = endPoint[0] - startPoint[0]
|
||||
val dy = endPoint[1] - startPoint[1]
|
||||
val gradientLineLength = sqrt(dx * dx + dy * dy)
|
||||
val processedColorStops = getFixedColorStops(colorStops, gradientLineLength)
|
||||
val finalStops = processColorTransitionHints(processedColorStops)
|
||||
val finalStops = ColorStopUtils.getFixedColorStops(colorStops, gradientLineLength)
|
||||
val colors = IntArray(finalStops.size)
|
||||
val positions = FloatArray(finalStops.size)
|
||||
|
||||
@@ -128,18 +128,18 @@ internal class LinearGradient(
|
||||
// Spec: https://www.w3.org/TR/css-images-3/#linear-gradient-syntax
|
||||
// Refer `using keywords` section
|
||||
private fun getAngleForKeyword(
|
||||
keyword: Direction.Keywords,
|
||||
keyword: Direction.KeywordType,
|
||||
width: Double,
|
||||
height: Double
|
||||
): Double {
|
||||
return when (keyword) {
|
||||
Direction.Keywords.TO_TOP_RIGHT -> {
|
||||
Direction.KeywordType.TO_TOP_RIGHT -> {
|
||||
val angleDeg = Math.toDegrees(atan(width / height))
|
||||
90 - angleDeg
|
||||
}
|
||||
Direction.Keywords.TO_BOTTOM_RIGHT -> Math.toDegrees(atan(width / height)) + 90
|
||||
Direction.Keywords.TO_TOP_LEFT -> Math.toDegrees(atan(width / height)) + 270
|
||||
Direction.Keywords.TO_BOTTOM_LEFT -> Math.toDegrees(atan(height / width)) + 180
|
||||
Direction.KeywordType.TO_BOTTOM_RIGHT -> Math.toDegrees(atan(width / height)) + 90
|
||||
Direction.KeywordType.TO_TOP_LEFT -> Math.toDegrees(atan(width / height)) + 270
|
||||
Direction.KeywordType.TO_BOTTOM_LEFT -> Math.toDegrees(atan(height / width)) + 180
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,178 +186,4 @@ internal class LinearGradient(
|
||||
|
||||
return Pair(firstPoint, secondPoint)
|
||||
}
|
||||
|
||||
private fun getFixedColorStops(
|
||||
colorStops: ArrayList<ColorStop>,
|
||||
gradientLineLength: Float
|
||||
): Array<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 fixedColorStops
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+375
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.facebook.react.uimanager.style
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.RadialGradient as AndroidRadialGradient
|
||||
import android.graphics.Shader
|
||||
import com.facebook.react.bridge.ColorPropConverter
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
import com.facebook.react.bridge.ReadableType
|
||||
import com.facebook.react.uimanager.FloatUtil
|
||||
import com.facebook.react.uimanager.LengthPercentage
|
||||
import com.facebook.react.uimanager.LengthPercentageType
|
||||
import com.facebook.react.uimanager.PixelUtil.dpToPx
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
import kotlin.math.sqrt
|
||||
|
||||
internal class RadialGradient(
|
||||
val shape: Shape,
|
||||
val size: GradientSize,
|
||||
val position: Position,
|
||||
val colorStops: List<ColorStop>
|
||||
) : Gradient {
|
||||
companion object {
|
||||
fun parse(gradientMap: ReadableMap, context: Context): Gradient? {
|
||||
val shape =
|
||||
gradientMap
|
||||
.takeIf { it.hasKey("shape") }
|
||||
?.let { map ->
|
||||
map.getString("shape")?.let { shapeString -> Shape.fromString(shapeString) }
|
||||
}
|
||||
val size: GradientSize? =
|
||||
gradientMap
|
||||
.takeIf { it.hasKey("size") }
|
||||
?.let { map ->
|
||||
when (map.getType("size")) {
|
||||
ReadableType.String ->
|
||||
GradientSize.KeywordType.fromString(map.getString("size"))?.let { keywordType
|
||||
->
|
||||
GradientSize.Keyword(keywordType)
|
||||
}
|
||||
ReadableType.Map ->
|
||||
map.getMap("size")
|
||||
?.takeIf { it.hasKey("x") && it.hasKey("y") }
|
||||
?.let { sizeMap ->
|
||||
val x = LengthPercentage.setFromDynamic(sizeMap.getDynamic("x"))
|
||||
val y = LengthPercentage.setFromDynamic(sizeMap.getDynamic("y"))
|
||||
if (x != null && y != null) {
|
||||
GradientSize.Dimensions(x, y)
|
||||
} else null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
val position =
|
||||
gradientMap
|
||||
.takeIf { it.hasKey("position") }
|
||||
?.let { map ->
|
||||
val positionMap = map.getMap("position") ?: return null
|
||||
|
||||
var top: LengthPercentage? = null
|
||||
var left: LengthPercentage? = null
|
||||
var right: LengthPercentage? = null
|
||||
var bottom: LengthPercentage? = null
|
||||
|
||||
if (positionMap.hasKey("top")) {
|
||||
val rawTop = positionMap.getDynamic("top")
|
||||
top = LengthPercentage.setFromDynamic(rawTop)
|
||||
} else if (positionMap.hasKey("bottom")) {
|
||||
val rawBottom = positionMap.getDynamic("bottom")
|
||||
bottom = LengthPercentage.setFromDynamic(rawBottom)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
if (positionMap.hasKey("left")) {
|
||||
val rawLeft = positionMap.getDynamic("left")
|
||||
left = LengthPercentage.setFromDynamic(rawLeft)
|
||||
} else if (positionMap.hasKey("right")) {
|
||||
val rawRight = positionMap.getDynamic("right")
|
||||
right = LengthPercentage.setFromDynamic(rawRight)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
|
||||
Position(top, left, right, bottom)
|
||||
}
|
||||
|
||||
val colorStops =
|
||||
gradientMap
|
||||
.takeIf { it.hasKey("colorStops") }
|
||||
?.let { map ->
|
||||
val colorStopsArray = map.getArray("colorStops") ?: return null
|
||||
|
||||
val stops = ArrayList<ColorStop>(colorStopsArray.size())
|
||||
for (i in 0 until colorStopsArray.size()) {
|
||||
val colorStop = colorStopsArray.getMap(i) ?: continue
|
||||
val color: Int? =
|
||||
when {
|
||||
!colorStop.hasKey("color") || colorStop.isNull("color") -> {
|
||||
null
|
||||
}
|
||||
colorStop.getType("color") == ReadableType.Map -> {
|
||||
ColorPropConverter.getColor(colorStop.getMap("color"), context)
|
||||
}
|
||||
else -> colorStop.getInt("color")
|
||||
}
|
||||
val colorStopPosition =
|
||||
LengthPercentage.setFromDynamic(colorStop.getDynamic("position"))
|
||||
stops.add(ColorStop(color, colorStopPosition))
|
||||
}
|
||||
stops
|
||||
}
|
||||
|
||||
if (shape != null && size != null && position != null && colorStops != null) {
|
||||
return RadialGradient(shape, size, position, colorStops)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
internal enum class Shape {
|
||||
CIRCLE,
|
||||
ELLIPSE;
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String): Shape? {
|
||||
return when (value) {
|
||||
"circle" -> CIRCLE
|
||||
"ellipse" -> ELLIPSE
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class GradientSize {
|
||||
class Keyword(val keyword: KeywordType) : GradientSize()
|
||||
|
||||
class Dimensions(val x: LengthPercentage, val y: LengthPercentage) : GradientSize()
|
||||
|
||||
enum class KeywordType(val value: String) {
|
||||
CLOSEST_SIDE("closest-side"),
|
||||
FARTHEST_SIDE("farthest-side"),
|
||||
CLOSEST_CORNER("closest-corner"),
|
||||
FARTHEST_CORNER("farthest-corner");
|
||||
|
||||
companion object {
|
||||
fun fromString(value: String?) = enumValues<KeywordType>().find { it.value == value }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class Position(
|
||||
val top: LengthPercentage? = null,
|
||||
val left: LengthPercentage? = null,
|
||||
val right: LengthPercentage? = null,
|
||||
val bottom: LengthPercentage? = null
|
||||
)
|
||||
|
||||
override fun getShader(width: Float, height: Float): Shader {
|
||||
var centerX: Float = width / 2f
|
||||
var centerY: Float = height / 2f
|
||||
if (position.top != null) {
|
||||
centerY =
|
||||
if (position.top.type == LengthPercentageType.PERCENT) position.top.resolve(height)
|
||||
else position.top.resolve(height).dpToPx()
|
||||
} else if (position.bottom != null) {
|
||||
centerY =
|
||||
if (position.bottom.type == LengthPercentageType.PERCENT)
|
||||
height - position.bottom.resolve(height)
|
||||
else height - position.bottom.resolve(height).dpToPx()
|
||||
}
|
||||
|
||||
if (position.left != null) {
|
||||
centerX =
|
||||
if (position.left.type == LengthPercentageType.PERCENT) position.left.resolve(width)
|
||||
else position.left.resolve(width).dpToPx()
|
||||
} else if (position.right != null) {
|
||||
centerX =
|
||||
if (position.right.type == LengthPercentageType.PERCENT)
|
||||
width - position.right.resolve(width)
|
||||
else width - position.right.resolve(width).dpToPx()
|
||||
}
|
||||
|
||||
val (radiusX, radiusY) = calculateRadius(centerX, centerY, width, height)
|
||||
|
||||
val finalStops = ColorStopUtils.getFixedColorStops(colorStops, max(radiusX, radiusY))
|
||||
val colors = IntArray(finalStops.size)
|
||||
val positions = FloatArray(finalStops.size)
|
||||
|
||||
finalStops.forEachIndexed { i, colorStop ->
|
||||
val color = colorStop.color
|
||||
if (color != null && colorStop.position != null) {
|
||||
colors[i] = color
|
||||
positions[i] = colorStop.position
|
||||
}
|
||||
}
|
||||
|
||||
// max is used to handle 0 radius user input. Radius has to be a positive float
|
||||
val radius = max(radiusX, 0.00001f)
|
||||
|
||||
val shader =
|
||||
AndroidRadialGradient(centerX, centerY, radius, colors, positions, Shader.TileMode.CLAMP)
|
||||
|
||||
val isCircle = shape == Shape.CIRCLE
|
||||
|
||||
// If not a circle and radiusX != radiusY, apply transformation to make it elliptical
|
||||
if (!isCircle && !FloatUtil.floatsEqual(radiusX, radiusY)) {
|
||||
val matrix = Matrix()
|
||||
matrix.setScale(1f, radiusY / radiusX, centerX, centerY)
|
||||
shader.setLocalMatrix(matrix)
|
||||
}
|
||||
|
||||
return shader
|
||||
}
|
||||
|
||||
private fun radiusToSide(
|
||||
centerX: Float,
|
||||
centerY: Float,
|
||||
width: Float,
|
||||
height: Float,
|
||||
sizeKeyword: GradientSize.KeywordType
|
||||
): Pair<Float, Float> {
|
||||
val radiusXFromLeftSide = centerX
|
||||
val radiusYFromTopSide = centerY
|
||||
val radiusXFromRightSide = width - centerX
|
||||
val radiusYFromBottomSide = height - centerY
|
||||
val radiusX: Float
|
||||
val radiusY: Float
|
||||
|
||||
if (sizeKeyword == GradientSize.KeywordType.CLOSEST_SIDE) {
|
||||
radiusX = min(radiusXFromLeftSide, radiusXFromRightSide)
|
||||
radiusY = min(radiusYFromTopSide, radiusYFromBottomSide)
|
||||
} else { // FARTHEST_SIDE
|
||||
radiusX = max(radiusXFromLeftSide, radiusXFromRightSide)
|
||||
radiusY = max(radiusYFromTopSide, radiusYFromBottomSide)
|
||||
}
|
||||
val isCircle = shape == Shape.CIRCLE
|
||||
if (isCircle) {
|
||||
val radius =
|
||||
if (sizeKeyword == GradientSize.KeywordType.CLOSEST_SIDE) {
|
||||
min(radiusX, radiusY)
|
||||
} else {
|
||||
max(radiusX, radiusY)
|
||||
}
|
||||
return Pair(radius, radius)
|
||||
}
|
||||
|
||||
return Pair(radiusX, radiusY)
|
||||
}
|
||||
|
||||
private fun calculateEllipseRadius(
|
||||
offsetX: Float,
|
||||
offsetY: Float,
|
||||
aspectRatio: Float
|
||||
): Pair<Float, Float> {
|
||||
if (aspectRatio == 0f || !aspectRatio.isFinite()) {
|
||||
return Pair(0f, 0f)
|
||||
}
|
||||
|
||||
// Ellipse that passes through a point formula: (x-h)^2/a^2 + (y-k)^2/b^2 = 1
|
||||
// a = semi major axis length
|
||||
// b = semi minor axis length = a / aspectRatio
|
||||
// x - h = offsetX
|
||||
// y - k = offsetY
|
||||
val a = sqrt(offsetX * offsetX + offsetY * offsetY * aspectRatio * aspectRatio)
|
||||
return Pair(a, a / aspectRatio)
|
||||
}
|
||||
|
||||
private fun radiusToCorner(
|
||||
centerX: Float,
|
||||
centerY: Float,
|
||||
width: Float,
|
||||
height: Float,
|
||||
sizeKeyword: GradientSize.KeywordType
|
||||
): Pair<Float, Float> {
|
||||
val corners =
|
||||
arrayOf(
|
||||
Pair(0f, 0f), // top-left
|
||||
Pair(width, 0f), // top-right
|
||||
Pair(width, height), // bottom-right
|
||||
Pair(0f, height) // bottom-left
|
||||
)
|
||||
|
||||
var cornerIndex = 0
|
||||
var distance =
|
||||
sqrt(
|
||||
(centerX - corners[cornerIndex].first).pow(2) +
|
||||
(centerY - corners[cornerIndex].second).pow(2))
|
||||
val isClosestCorner = sizeKeyword == GradientSize.KeywordType.CLOSEST_CORNER
|
||||
|
||||
for (i in 1 until corners.size) {
|
||||
val newDistance =
|
||||
sqrt((centerX - corners[i].first).pow(2) + (centerY - corners[i].second).pow(2))
|
||||
if (isClosestCorner) {
|
||||
if (newDistance < distance) {
|
||||
distance = newDistance
|
||||
cornerIndex = i
|
||||
}
|
||||
} else {
|
||||
if (newDistance > distance) {
|
||||
distance = newDistance
|
||||
cornerIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val isCircle = shape == Shape.CIRCLE
|
||||
if (isCircle) {
|
||||
return Pair(distance, distance)
|
||||
}
|
||||
|
||||
// https://www.w3.org/TR/css-images-3/#typedef-radial-size
|
||||
// Aspect ratio of corner size ellipse is same as the respective side size ellipse
|
||||
val sideKeyword =
|
||||
if (isClosestCorner) GradientSize.KeywordType.CLOSEST_SIDE
|
||||
else GradientSize.KeywordType.FARTHEST_SIDE
|
||||
val sideRadius = radiusToSide(centerX, centerY, width, height, sideKeyword)
|
||||
|
||||
// Calculate ellipse radii based on the aspect ratio of the side ellipse
|
||||
return calculateEllipseRadius(
|
||||
corners[cornerIndex].first - centerX,
|
||||
corners[cornerIndex].second - centerY,
|
||||
sideRadius.first / sideRadius.second)
|
||||
}
|
||||
|
||||
private fun calculateRadius(
|
||||
centerX: Float,
|
||||
centerY: Float,
|
||||
width: Float,
|
||||
height: Float
|
||||
): Pair<Float, Float> {
|
||||
if (size is GradientSize.Keyword) {
|
||||
return when (val keyword = size.keyword) {
|
||||
GradientSize.KeywordType.CLOSEST_SIDE,
|
||||
GradientSize.KeywordType.FARTHEST_SIDE -> {
|
||||
radiusToSide(centerX, centerY, width, height, keyword)
|
||||
}
|
||||
GradientSize.KeywordType.CLOSEST_CORNER,
|
||||
GradientSize.KeywordType.FARTHEST_CORNER -> {
|
||||
radiusToCorner(centerX, centerY, width, height, keyword)
|
||||
}
|
||||
}
|
||||
} else if (size is GradientSize.Dimensions) {
|
||||
val radiusX =
|
||||
if (size.x.type == LengthPercentageType.PERCENT) size.x.resolve(width)
|
||||
else size.x.resolve(width).dpToPx()
|
||||
val radiusY =
|
||||
if (size.y.type == LengthPercentageType.PERCENT) size.y.resolve(height)
|
||||
else size.y.resolve(height).dpToPx()
|
||||
|
||||
val isCircle = shape == Shape.CIRCLE
|
||||
return if (isCircle) {
|
||||
val radius = max(radiusX, radiusY)
|
||||
Pair(radius, radius)
|
||||
} else {
|
||||
Pair(radiusX, radiusY)
|
||||
}
|
||||
} else {
|
||||
return radiusToCorner(
|
||||
centerX, centerY, width, height, GradientSize.KeywordType.FARTHEST_CORNER)
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -104,8 +104,10 @@ public open class ReactViewManager : ReactClippingViewManager<ReactViewGroup>()
|
||||
val backgroundImageLayers = ArrayList<BackgroundImageLayer>(backgroundImage.size())
|
||||
for (i in 0 until backgroundImage.size()) {
|
||||
val backgroundImageMap = backgroundImage.getMap(i)
|
||||
val layer = BackgroundImageLayer(backgroundImageMap, view.context)
|
||||
backgroundImageLayers.add(layer)
|
||||
val layer = BackgroundImageLayer.parse(backgroundImageMap, view.context)
|
||||
if (layer != null) {
|
||||
backgroundImageLayers.add(layer)
|
||||
}
|
||||
}
|
||||
BackgroundStyleApplicator.setBackgroundImage(view, backgroundImageLayers)
|
||||
} else {
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
package com.facebook.react.uimanager.style
|
||||
|
||||
import ColorStop
|
||||
import android.graphics.Color
|
||||
import android.util.DisplayMetrics
|
||||
import com.facebook.react.uimanager.DisplayMetricsHolder
|
||||
import com.facebook.react.uimanager.LengthPercentage
|
||||
import com.facebook.react.uimanager.LengthPercentageType
|
||||
import org.assertj.core.api.Assertions.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
/** Tests for [ColorStopUtils] */
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class ColorStopTest {
|
||||
@Before
|
||||
fun setUp() {
|
||||
val metrics = DisplayMetrics()
|
||||
metrics.density = 1f
|
||||
DisplayMetricsHolder.setWindowDisplayMetrics(metrics)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testBasicColorStops() {
|
||||
val colorStops =
|
||||
listOf(
|
||||
ColorStop(Color.RED, LengthPercentage(0f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(Color.GREEN, LengthPercentage(42f, LengthPercentageType.PERCENT)))
|
||||
|
||||
val processed = ColorStopUtils.getFixedColorStops(colorStops, 60f)
|
||||
assertThat(processed).hasSize(2)
|
||||
assertThat(processed[0].color).isEqualTo(Color.RED)
|
||||
assertThat(processed[0].position).isEqualTo(0f)
|
||||
assertThat(processed[1].color).isEqualTo(Color.GREEN)
|
||||
assertThat(processed[1].position).isEqualTo(0.42f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testColorStopsWithFirstAndLastPositionsMissing() {
|
||||
val colorStops =
|
||||
listOf(
|
||||
ColorStop(Color.RED),
|
||||
ColorStop(Color.GREEN, LengthPercentage(30f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(Color.BLUE))
|
||||
val processed = ColorStopUtils.getFixedColorStops(colorStops, 80f)
|
||||
|
||||
assertThat(processed).hasSize(3)
|
||||
assertThat(processed[0].color).isEqualTo(Color.RED)
|
||||
assertThat(processed[0].position).isEqualTo(0f)
|
||||
assertThat(processed[1].color).isEqualTo(Color.GREEN)
|
||||
assertThat(processed[1].position).isEqualTo(0.3f)
|
||||
assertThat(processed[2].color).isEqualTo(Color.BLUE)
|
||||
assertThat(processed[2].position).isEqualTo(1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testColorStopsWithLessPositionValueThanPreviousPosition() {
|
||||
val colorStops =
|
||||
listOf(
|
||||
ColorStop(Color.RED),
|
||||
ColorStop(Color.GREEN, LengthPercentage(30f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(Color.BLUE, LengthPercentage(20f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(Color.GRAY, LengthPercentage(60f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(Color.CYAN, LengthPercentage(50f, LengthPercentageType.PERCENT)))
|
||||
val processed = ColorStopUtils.getFixedColorStops(colorStops, 80f)
|
||||
|
||||
assertThat(processed).hasSize(5)
|
||||
assertThat(processed[0].color).isEqualTo(Color.RED)
|
||||
assertThat(processed[0].position).isEqualTo(0f)
|
||||
assertThat(processed[1].color).isEqualTo(Color.GREEN)
|
||||
assertThat(processed[1].position).isEqualTo(0.3f)
|
||||
assertThat(processed[2].color).isEqualTo(Color.BLUE)
|
||||
assertThat(processed[2].position).isEqualTo(0.3f)
|
||||
assertThat(processed[3].color).isEqualTo(Color.GRAY)
|
||||
assertThat(processed[3].position).isEqualTo(0.6f)
|
||||
assertThat(processed[4].color).isEqualTo(Color.CYAN)
|
||||
assertThat(processed[4].position).isEqualTo(0.6f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testColorStopsWithMissingMiddlePositions() {
|
||||
val colorStops =
|
||||
listOf(
|
||||
ColorStop(Color.RED, LengthPercentage(0f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(Color.GREEN),
|
||||
ColorStop(Color.BLUE),
|
||||
ColorStop(Color.TRANSPARENT, LengthPercentage(100f, LengthPercentageType.PERCENT)))
|
||||
val processed = ColorStopUtils.getFixedColorStops(colorStops, 100f)
|
||||
|
||||
assertThat(processed).hasSize(4)
|
||||
assertThat(processed[0].color).isEqualTo(Color.RED)
|
||||
assertThat(processed[0].position).isEqualTo(0f)
|
||||
assertThat(processed[1].color).isEqualTo(Color.GREEN)
|
||||
assertThat(processed[1].position).isEqualTo(0.33333334f)
|
||||
assertThat(processed[2].color).isEqualTo(Color.BLUE)
|
||||
assertThat(processed[2].position).isEqualTo(0.6666667f)
|
||||
assertThat(processed[3].color).isEqualTo(Color.TRANSPARENT)
|
||||
assertThat(processed[3].position).isEqualTo(1f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testColorStopsWithMixedUnits() {
|
||||
val colorStops =
|
||||
listOf(
|
||||
ColorStop(Color.YELLOW, LengthPercentage(100f, LengthPercentageType.POINT)),
|
||||
ColorStop(Color.BLUE, LengthPercentage(50f, LengthPercentageType.PERCENT)))
|
||||
|
||||
val processed200px = ColorStopUtils.getFixedColorStops(colorStops, 200f)
|
||||
assertThat(processed200px).hasSize(2)
|
||||
assertThat(processed200px[0].color).isEqualTo(Color.YELLOW)
|
||||
assertThat(processed200px[0].position).isEqualTo(0.5f)
|
||||
assertThat(processed200px[1].color).isEqualTo(Color.BLUE)
|
||||
assertThat(processed200px[1].position).isEqualTo(0.5f)
|
||||
|
||||
// positions should be corrected
|
||||
val processed150px = ColorStopUtils.getFixedColorStops(colorStops, 150f)
|
||||
assertThat(processed150px).hasSize(2)
|
||||
assertThat(processed150px[0].color).isEqualTo(Color.YELLOW)
|
||||
assertThat(processed150px[0].position).isEqualTo(0.6666667f) // 100px / 150px ≈ 0.6667
|
||||
assertThat(processed150px[1].color).isEqualTo(Color.BLUE)
|
||||
assertThat(processed150px[1].position)
|
||||
.isEqualTo(0.6666667f) // Corrected to match yellow's position
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testColorStopsWithMultipleTransitionHints() {
|
||||
val colorStops =
|
||||
listOf(
|
||||
ColorStop(Color.RED, LengthPercentage(0f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(null, LengthPercentage(10f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(Color.GREEN, LengthPercentage(50f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(null, LengthPercentage(85f, LengthPercentageType.PERCENT)),
|
||||
ColorStop(Color.BLUE, LengthPercentage(100f, LengthPercentageType.PERCENT)))
|
||||
val processed = ColorStopUtils.getFixedColorStops(colorStops, 100f)
|
||||
assertThat(processed.size).isEqualTo(21)
|
||||
assertThat(processed.first().color).isEqualTo(Color.RED)
|
||||
assertThat(processed.first().position).isEqualTo(0f)
|
||||
// 9 interpolated colors and positions between RED and GREEN
|
||||
assertThat(processed.subList(1, 10).all { it.color != null && it.position != null }).isTrue()
|
||||
assertThat(processed[10].color).isEqualTo(Color.GREEN)
|
||||
assertThat(processed[10].position).isEqualTo(0.5f)
|
||||
// 9 interpolated colors and positions between GREEN and BLUE
|
||||
assertThat(processed.subList(11, 20).all { it.color != null && it.position != null }).isTrue()
|
||||
assertThat(processed[20].color).isEqualTo(Color.BLUE)
|
||||
assertThat(processed[20].position).isEqualTo(1f)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user