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

Conversation

intergalacticspacehighway
Copy link
Contributor

@intergalacticspacehighway intergalacticspacehighway commented Mar 25, 2025

Summary:

Adds android changes for radial gradient. Previous PR - #50209

Changelog:

[ANDROID] [ADDED] - Radial gradient

Test Plan:

@facebook-github-bot facebook-github-bot added CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Mar 25, 2025
@facebook-github-bot
Copy link
Contributor

@jorge-cab has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

Copy link
Contributor

@jorge-cab jorge-cab left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 bug, crashing on radius of 0 and the rest code styling stuff

val shader = AndroidRadialGradient(
centerX,
centerY,
radiusX,
Copy link
Contributor

@jorge-cab jorge-cab Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will crash when passing a radius of 0?

Something like radial-gradient(0px 0px at center, red, blue)

Negative radius seems fine though it should fail like on web, I would add test for both cases

Comment on lines 33 to 46
private enum class Shape {
CIRCLE,
ELLIPSE;

companion object {
fun fromString(value: String): Shape {
return when (value.lowercase()) {
"circle" -> CIRCLE
"ellipse" -> ELLIPSE
else -> ELLIPSE
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can do:

Suggested change
private enum class Shape {
CIRCLE,
ELLIPSE;
companion object {
fun fromString(value: String): Shape {
return when (value.lowercase()) {
"circle" -> CIRCLE
"ellipse" -> ELLIPSE
else -> ELLIPSE
}
}
}
}
private enum class Shape(val value: String) {
CIRCLE("circle"),
ELLIPSE("ellipse");
companion object {
fun fromString(value: String): Shape =
enumValues<Shape>().find { it.value == value.lowercase() } ?: ELLIPSE
}
}

Since the fromString() method feels a bit arbitrary like that

Comment on lines 48 to 64
private enum class SizeKeyword {
CLOSEST_SIDE,
FARTHEST_SIDE,
CLOSEST_CORNER,
FARTHEST_CORNER;

companion object {
fun fromString(value: String?): SizeKeyword {
return when (value?.lowercase()) {
"closest-side" -> CLOSEST_SIDE
"farthest-side" -> FARTHEST_SIDE
"closest-corner" -> CLOSEST_CORNER
else -> FARTHEST_CORNER
}
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private enum class SizeKeyword {
CLOSEST_SIDE,
FARTHEST_SIDE,
CLOSEST_CORNER,
FARTHEST_CORNER;
companion object {
fun fromString(value: String?): SizeKeyword {
return when (value?.lowercase()) {
"closest-side" -> CLOSEST_SIDE
"farthest-side" -> FARTHEST_SIDE
"closest-corner" -> CLOSEST_CORNER
else -> FARTHEST_CORNER
}
}
}
}
private enum class SizeKeyword {
CLOSEST_SIDE("closest-side"),
FARTHEST_SIDE("farthest-side"),
CLOSEST_CORNER("closest-corner"),
FARTHEST_CORNER("farthest-corner");
companion object {
fun fromString(value: String?): SizeKeyword =
enumValues<SizeKeyword>().find { it.value == value.lowercase() } ?: FARTHEST_CORNER
}
}

Comment on lines 88 to 91
private val shape: Shape = run {
val shapeString = gradientMap.getString("shape") ?: throw IllegalArgumentException("Radial gradient must have shape")
Shape.fromString(shapeString)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private val shape: Shape = run {
val shapeString = gradientMap.getString("shape") ?: throw IllegalArgumentException("Radial gradient must have shape")
Shape.fromString(shapeString)
}
private val shape: Shape = gradientMap.getString("shape")?.let { Shape.fromString(it) }
?: throw IllegalArgumentException("Radial gradient must have shape")

}

else -> throw IllegalArgumentException("Invalid direction type: $type")
private val direction: Direction = run {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are parsing AND initializing here. I think we should do a parseDirection function to split the logic a bit.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A way that would be nice to layer these, is if we have some ctor taking the concrete data types, and then some function that extracts those out of ReadableMap, and constructs from it. That will also make these types generic in the future to changes in how we might store the intermediates.

}
private val isCircle: Boolean = shape == Shape.CIRCLE

private val position: Position = run {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all run blocks. Maybe instead we can have a parsePosition(), parseSize and parseColorStops instead of doing the logic on initialization so we are not both initializing and parsing at the same time? @NickGerleman What do you think?

linearGradient.getShader(bounds.width().toFloat(), bounds.height().toFloat())
}
}
public interface Gradient {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it intentional we moved this from internal to public?

Suggested change
public interface Gradient {
internal interface Gradient {

Comment on lines 21 to 22
"linearGradient" -> LinearGradient(gradientMap, context)
"radialGradient" -> RadialGradient(gradientMap, context)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a comment on the JS PR, but we might want to change these for consistency

Suggested change
"linearGradient" -> LinearGradient(gradientMap, context)
"radialGradient" -> RadialGradient(gradientMap, context)
"linear-gradient" -> LinearGradient(gradientMap, context)
"radial-gradient" -> RadialGradient(gradientMap, context)

Comment on lines 27 to 30
null
}
} else {
null
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is a little tricky to read. Consider

  1. Early return null on null (or, only accept non-null? Seems like caller would need to check that anyways)
  2. Explicitly check data types and return null instead of swallowing exception. That could help avoid over-swallowing logic errors too

@@ -14,16 +14,22 @@ import com.facebook.react.bridge.ReadableMap

public class BackgroundImageLayer(gradientMap: ReadableMap?, context: Context) {
Copy link
Contributor

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?

Copy link
Contributor Author

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! 😕

Copy link
Contributor

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 like OutsetBoxShadowDrawable

Copy link
Contributor Author

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.


public fun getShader(bounds: Rect): Shader? = gradient?.getShader(bounds)
public fun getShader(bounds: Rect): Shader? =
gradient?.getShader(bounds.width().toFloat(), bounds.height().toFloat())
Copy link
Contributor

Choose a reason for hiding this comment

The 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 BackgroundImageLayer? Wondering how this will also eventually fit into the work @jorge-cab will be taking on more in the future about image support for background-image.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If parsing any gradient fails we return null failing silently and do not draw the gradient like web. So this won't return a shader in that case!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of allowing constructing a BackgroundImageLayer which is in an invalid state, could we pull parsing out of the constructor? E.g. so that some static parse function returns null if passed invalid data, but then, if we can correctly parse a BackgroundImageLayer, and returns one, it must be valid.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added parse function here. i had to make the constructor private because of access modifier issues (can't keep the constructor public since Gradient interface is internal). So BackgroundImageLayer will stay public because of this but it's constructor will stay private.


public data class ProcessedColorStop(var color: Int? = null, val position: Float? = null)

public object ColorStopUtils {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is pulled out of old code, but now this seems pretty unit testable. Could we maybe add some tests for these? BorderRadiusStyleTest is a test for something in the same structure that could be matched I think.

import com.facebook.react.uimanager.PixelUtil
import kotlin.math.ln

public data class ColorStop(var color: Int? = null, val position: LengthPercentage? = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we should prefer class over data class where we don't need equality or hashing bc they can lead to binary bloat. Some folks have been looking to remove data class usages from RN to save a bit on binary.


public data class ColorStop(var color: Int? = null, val position: LengthPercentage? = null)

public data class ProcessedColorStop(var color: Int? = null, val position: Float? = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we allow each part to be nullable?

Could we document what these types mean, and how they are used?

}

else -> throw IllegalArgumentException("Invalid direction type: $type")
private val direction: Direction = run {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A way that would be nice to layer these, is if we have some ctor taking the concrete data types, and then some function that extracts those out of ReadableMap, and constructs from it. That will also make these types generic in the future to changes in how we might store the intermediates.

return when (value.lowercase()) {
"circle" -> CIRCLE
"ellipse" -> ELLIPSE
else -> ELLIPSE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we ever get here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, JS processor handles all the defaults. Removed.

@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Mar 29, 2025

@jorge-cab @NickGerleman Thanks for all the inputs and catching a bug. I have addressed all the comments (also pushed related changes to iOS and JS PRs). Let me know if anything else is needed!

@NickGerleman
Copy link
Contributor

Left a couple quick comments, but otherwise mostly LGTM

@facebook-github-bot
Copy link
Contributor

@jorge-cab has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

2 similar comments
@facebook-github-bot
Copy link
Contributor

@jorge-cab has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

@facebook-github-bot
Copy link
Contributor

@jorge-cab has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

facebook-github-bot pushed a commit that referenced this pull request Apr 17, 2025
Summary:
Adds JS changes for radial gradient. Previous PR - #50209

## Changelog:
[GENERAL] [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: #50268

Test Plan:
- Added tests in `processBackgroundImage-test.js` in #50268
- Merge this, #50266 and #50269 and check examples in `RadialGradientExample.js`

Reviewed By: NickGerleman

Differential Revision: D71898524

Pulled By: jorge-cab

fbshipit-source-id: 39bdf0f647ba19839b6e6deb09b30a1c337e6457
@facebook-github-bot facebook-github-bot added the Merged This PR has been merged. label Apr 17, 2025
@facebook-github-bot
Copy link
Contributor

@jorge-cab merged this pull request in a240994.

uffoltzl pushed a commit to uffoltzl/react-native that referenced this pull request Apr 18, 2025
Summary:
Adds android changes for radial gradient. Previous PR - facebook#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: facebook#50269

Test Plan:
- Added tests in processBackgroundImage-test.js in facebook#50268
- Merge this and [this](facebook#50268) and check examples in `RadialGradientExample.js`

Reviewed By: NickGerleman

Differential Revision: D71898546

Pulled By: jorge-cab

fbshipit-source-id: 2872bcf16a492c7bc829be10eedb0f6c7dad32c7
uffoltzl pushed a commit to uffoltzl/react-native that referenced this pull request Apr 18, 2025
Summary:
Adds JS changes for radial gradient. Previous PR - facebook#50209

## Changelog:
[GENERAL] [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: facebook#50268

Test Plan:
- Added tests in `processBackgroundImage-test.js` in facebook#50268
- Merge this, facebook#50266 and facebook#50269 and check examples in `RadialGradientExample.js`

Reviewed By: NickGerleman

Differential Revision: D71898524

Pulled By: jorge-cab

fbshipit-source-id: 39bdf0f647ba19839b6e6deb09b30a1c337e6457
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged. Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants