Skip to content

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

Original file line number Diff line number Diff line change
@@ -11,19 +11,33 @@ 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

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)
public fun getShader(bounds: Rect): Shader =
gradient.getShader(bounds.width().toFloat(), bounds.height().toFloat())
}
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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Loading

Unchanged files with check annotations Beta

fromEnc: buffer$Encoding,
toEnc: buffer$Encoding,
): Node$Buffer;
declare var Buffer: Node$Buffer;

Check warning on line 200 in flow-typed/environment/node.js

GitHub Actions / test_js (22)

'Buffer' is already declared in the upper scope on line 53 column 15

Check warning on line 200 in flow-typed/environment/node.js

GitHub Actions / test_js (20)

'Buffer' is already declared in the upper scope on line 53 column 15

Check warning on line 200 in flow-typed/environment/node.js

GitHub Actions / test_js (18)

'Buffer' is already declared in the upper scope on line 53 column 15
}
type child_process$execOpts = {
return assets[assetId - 1];
}
module.exports = {registerAsset, getAssetByID};

Check warning on line 43 in packages/assets/registry.js

GitHub Actions / test_js (22)

Use `export` syntax instead of CommonJS `module.exports`

Check warning on line 43 in packages/assets/registry.js

GitHub Actions / test_js (20)

Use `export` syntax instead of CommonJS `module.exports`

Check warning on line 43 in packages/assets/registry.js

GitHub Actions / test_js (18)

Use `export` syntax instead of CommonJS `module.exports`
const debug = require('debug')('Metro:InspectorProxy');
const PAGES_POLLING_INTERVAL = 1000;
const MIN_MESSAGE_QUEUE_BYTES_TO_REPORT = 2 * 1024 * 1024; // 2 MiB

Check warning on line 40 in packages/dev-middleware/src/inspector-proxy/Device.js

GitHub Actions / test_js (22)

'MIN_MESSAGE_QUEUE_BYTES_TO_REPORT' is assigned a value but never used

Check warning on line 40 in packages/dev-middleware/src/inspector-proxy/Device.js

GitHub Actions / test_js (20)

'MIN_MESSAGE_QUEUE_BYTES_TO_REPORT' is assigned a value but never used

Check warning on line 40 in packages/dev-middleware/src/inspector-proxy/Device.js

GitHub Actions / test_js (18)

'MIN_MESSAGE_QUEUE_BYTES_TO_REPORT' is assigned a value but never used
const WS_CLOSURE_CODE = {
NORMAL: 1000,
'use client';
/* eslint-disable no-shadow, eqeqeq, curly, no-unused-vars, no-void, no-control-regex */

Check warning on line 14 in packages/polyfills/console.js

GitHub Actions / test_js (22)

'curly' rule is disabled but never reported

Check warning on line 14 in packages/polyfills/console.js

GitHub Actions / test_js (20)

'curly' rule is disabled but never reported

Check warning on line 14 in packages/polyfills/console.js

GitHub Actions / test_js (18)

'curly' rule is disabled but never reported
/**
* This pipes all of our console logging functions to native logging so that
nativeTypeDiffingTypesAliases,
nativeTypeDiffingTypesMethodParamLookup,
nativeTypeDiffingTypesMethodLookup,
_,

Check warning on line 85 in packages/react-native-compatibility-check/src/__tests__/TypeDiffing-test.js

GitHub Actions / test_js (22)

'_' is assigned a value but never used

Check warning on line 85 in packages/react-native-compatibility-check/src/__tests__/TypeDiffing-test.js

GitHub Actions / test_js (20)

'_' is assigned a value but never used

Check warning on line 85 in packages/react-native-compatibility-check/src/__tests__/TypeDiffing-test.js

GitHub Actions / test_js (18)

'_' is assigned a value but never used
nativeTypeDiffingTypesEnums,
] = getModule(
'native-module-type-diffing-types',
import type AnimatedValue from '../nodes/AnimatedValue';
import NativeAnimatedHelper from '../../../src/private/animated/NativeAnimatedHelper';
import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags';

Check warning on line 16 in packages/react-native/Libraries/Animated/animations/Animation.js

GitHub Actions / test_js (22)

'ReactNativeFeatureFlags' is defined but never used

Check warning on line 16 in packages/react-native/Libraries/Animated/animations/Animation.js

GitHub Actions / test_js (20)

'ReactNativeFeatureFlags' is defined but never used

Check warning on line 16 in packages/react-native/Libraries/Animated/animations/Animation.js

GitHub Actions / test_js (18)

'ReactNativeFeatureFlags' is defined but never used
import AnimatedProps from '../nodes/AnimatedProps';
export type EndResult = {finished: boolean, value?: number, ...};
* transform which can receive values from multiple parents.
*/
export function flushValue(rootNode: AnimatedNode): void {
// eslint-disable-next-line func-call-spacing

Check warning on line 54 in packages/react-native/Libraries/Animated/nodes/AnimatedValue.js

GitHub Actions / test_js (22)

'func-call-spacing' rule is disabled but never reported

Check warning on line 54 in packages/react-native/Libraries/Animated/nodes/AnimatedValue.js

GitHub Actions / test_js (20)

'func-call-spacing' rule is disabled but never reported

Check warning on line 54 in packages/react-native/Libraries/Animated/nodes/AnimatedValue.js

GitHub Actions / test_js (18)

'func-call-spacing' rule is disabled but never reported
const leaves = new Set<{update: () => void, ...}>();
function findAnimatedStyles(node: AnimatedNode) {
// $FlowFixMe[prop-missing]
remoteModuleConfig: remoteModulesConfig,
};
module.exports = MessageQueueTestConfig;

Check warning on line 38 in packages/react-native/Libraries/BatchedBridge/__mocks__/MessageQueueTestConfig.js

GitHub Actions / test_js (22)

Use `export` syntax instead of CommonJS `module.exports`

Check warning on line 38 in packages/react-native/Libraries/BatchedBridge/__mocks__/MessageQueueTestConfig.js

GitHub Actions / test_js (20)

Use `export` syntax instead of CommonJS `module.exports`

Check warning on line 38 in packages/react-native/Libraries/BatchedBridge/__mocks__/MessageQueueTestConfig.js

GitHub Actions / test_js (18)

Use `export` syntax instead of CommonJS `module.exports`
testHook2: function () {},
};
module.exports = MessageQueueTestModule;

Check warning on line 22 in packages/react-native/Libraries/BatchedBridge/__mocks__/MessageQueueTestModule.js

GitHub Actions / test_js (22)

Use `export` syntax instead of CommonJS `module.exports`

Check warning on line 22 in packages/react-native/Libraries/BatchedBridge/__mocks__/MessageQueueTestModule.js

GitHub Actions / test_js (20)

Use `export` syntax instead of CommonJS `module.exports`

Check warning on line 22 in packages/react-native/Libraries/BatchedBridge/__mocks__/MessageQueueTestModule.js

GitHub Actions / test_js (18)

Use `export` syntax instead of CommonJS `module.exports`