-
Notifications
You must be signed in to change notification settings - Fork 24.6k
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
feat: radial gradient android changes #50269
base: main
Are you sure you want to change the base?
feat: radial gradient android changes #50269
Conversation
@jorge-cab has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
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.
1 bug, crashing on radius of 0 and the rest code styling stuff
val shader = AndroidRadialGradient( | ||
centerX, | ||
centerY, | ||
radiusX, |
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 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
private enum class Shape { | ||
CIRCLE, | ||
ELLIPSE; | ||
|
||
companion object { | ||
fun fromString(value: String): Shape { | ||
return when (value.lowercase()) { | ||
"circle" -> CIRCLE | ||
"ellipse" -> ELLIPSE | ||
else -> ELLIPSE | ||
} | ||
} | ||
} | ||
} |
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 think you can do:
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
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 | ||
} | ||
} | ||
} | ||
} |
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.
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 | |
} | |
} |
private val shape: Shape = run { | ||
val shapeString = gradientMap.getString("shape") ?: throw IllegalArgumentException("Radial gradient must have shape") | ||
Shape.fromString(shapeString) | ||
} |
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.
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 { |
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 are parsing AND initializing here. I think we should do a parseDirection
function to split the logic a bit.
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.
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 { |
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.
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 { |
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.
Is it intentional we moved this from internal
to public
?
public interface Gradient { | |
internal interface Gradient { |
"linearGradient" -> LinearGradient(gradientMap, context) | ||
"radialGradient" -> RadialGradient(gradientMap, context) |
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.
Left a comment on the JS PR, but we might want to change these for consistency
"linearGradient" -> LinearGradient(gradientMap, context) | |
"radialGradient" -> RadialGradient(gradientMap, context) | |
"linear-gradient" -> LinearGradient(gradientMap, context) | |
"radial-gradient" -> RadialGradient(gradientMap, context) |
null | ||
} | ||
} else { | ||
null |
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: this is a little tricky to read. Consider
- Early return null on null (or, only accept non-null? Seems like caller would need to check that anyways)
- 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) { |
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! 😕
|
||
public fun getShader(bounds: Rect): Shader? = gradient?.getShader(bounds) | ||
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 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
.
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.
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!
|
||
public data class ProcessedColorStop(var color: Int? = null, val position: Float? = null) | ||
|
||
public object ColorStopUtils { |
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 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) |
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: 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) |
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.
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 { |
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.
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 |
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.
Should we ever get here?
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.
True, JS processor handles all the defaults. Removed.
@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! |
Summary:
Adds android changes for radial gradient. Previous PR - #50209
Changelog:
[ANDROID] [ADDED] - Radial gradient
Test Plan:
RadialGradientExample.js