Skip to content
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: linear gradient android #45433

Conversation

intergalacticspacehighway
Copy link
Contributor

@intergalacticspacehighway intergalacticspacehighway commented Jul 15, 2024

Summary:

  • Adds backgroundImage prop that supports CSS's linear gradient. Later this can be extended to support various other gradients and possibly CSS's background image (less motivation as better solutions exists for image)
  • Extended CSSBackgroundDrawable to draw Linear Gradient shader while preserving the border style support.
  • Style supports JS object to specify LinearGradient, so it can support Animated libraries.

Changelog:

[ANDROID] [ADDED] - linear gradient

Test Plan:

  • Check out processBackground-test.js for supported syntax testcases.
  • Checkout examples added in LinearGradientExample.js

Although the PR is tested well but open to any changes/feedback on the approach taken.

iOS PR - #45434. Separated the PRs to keep it easier to review. Both PRs can be reviewed individually.

wip

wip

wip

parse position

linear gradient start end points

wip

wip

simplify angle to points

wip gradients array

wip

wip

wip

wip

wip

wip

wip

merge main

wip

wip

nit

weird white space parser

wip

cleanup

cleanup

cleanup

gradient example
@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 Jul 15, 2024
@analysis-bot
Copy link

analysis-bot commented Jul 15, 2024

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 21,386,065 +18,584
android hermes armeabi-v7a n/a --
android hermes x86 n/a --
android hermes x86_64 n/a --
android jsc arm64-v8a 24,581,462 +17,241
android jsc armeabi-v7a n/a --
android jsc x86 n/a --
android jsc x86_64 n/a --

Base commit: 2eb7bcb
Branch: main

@NickGerleman
Copy link
Contributor

This is a fun surprise 😃. I chatted with a couple other engineers on RN to think about how this fits into some of the CSS stuff we have been planning.

I think broadly:

  1. This change makes sense in RN core, instead of a separate custom component, to be able to support web patterns, and complex backgrounds without adding extra views to the hierarchy
  2. We don't want to ship a background prop that only does anything today for linear gradients, instead of acting like web for colors, shorthands, etc.
  3. Despite adding more recently, we want to be able to delete all the viewconfig processors eventually, replacing with Fabric CSS parser. This has meant, us intentionally limiting some newer capabilities like this to Fabric, so that once the Fabric CSS parser is ready, we don't have any more reliance on the viewconfig processors in Paper (see 2d67b72 as example)

A way to unblock some of this would be to follow a pattern we are using, of adding the functionality first under experimental_foo. We are testing some of these against existing web code using react-strict-dom, to get some production validation before rolling out (if @necolas wanted to shim gradients using this as a primitive). But, this would let us fill out more of the right shorthands, etc, using Fabric CSS parser later, to roll out a version of background that acted more like the web.

We are also in the middle of refactoring of background management in order to start rolling out box shadow. Part of the strategy we are taking there is to put less directly in CSSBackgroundDrawable, and to have different drawables for different patterns, that we may composite using a LayerDrawable under the hood. I think CompositeShader is technically more efficient than multiple shader drawables, but if we start to support different background layer types, we cannot generically combine them all in the right rendering order. I would consider factoring the gradient background as its own drawable, that we will composite in higher level area.

@necolas
Copy link
Contributor

necolas commented Jul 15, 2024

Just to add to what @NickGerleman said. Even in StyleX we don't allow background as a style, because it's specced as a shorthand for multiple other background properties. React Native doesn't support shortform styles with values that could potentially override the values of more specific longform styles. So CSS background (as defined by web) doesn't fit into RN's styling merging model. Instead, this would be better implemented as part of backgroundImage, which is the background property that supports gradients.

@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Jul 15, 2024

Thanks @NickGerleman @necolas

I had been checking the commits on Fabric CSS Parser by @NickGerleman 🙌 and knew it will eventually gonna replace the JS processors. Also i was wondering if team was planning to leverage static hermes in future so JS processors kinda becomes auto native. Anyways, I still wanted to make the PR to get some feedback 😄

So based on your feedback I think next steps for me are.

  • Rename background to more like experimental_backgroundImage.
  • Gate this prop for fabric only.
  • Try out composing drawables instead of shader. Only reason i went for Shader was to get the border styles right as currently CSSBackgroundDrawable draws it so it was easier to hook the shader into paint and draw. But I just checked box shadow drawables in the source and it uses clipping to clip border paths. I will experiment with that. If there are some additional pointers in this direction let me know.

Let me know how this sounds @NickGerleman. Thank you for your contributions and quick feedback! 🙏

@NickGerleman
Copy link
Contributor

NickGerleman commented Jul 17, 2024

That sounds about right to me!

I didn’t think about clipping considerations. Currently we clip most often in onDraw(), so all foreground content gets clipped to the padding box. This is why background drawables can still draw outside of the clip region, but why they aren’t automatically clipped.

Feel free to keep this inside CSSBackgroundDrawable for now, if it makes border use easier. The connective tissue the shadows are using is still being checked in right now.

@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Jul 20, 2024

@NickGerleman updated the PR. Will experiment with Drawables and closely monitor the BoxShadow updates. Let me know if any more feedback. Next, will update the iOS implementation for prop name changes (it will use most of the stuff from this PR)

Comment on lines 29 to 37
export type BackgroundImagePrimitive = {
type: 'linearGradient',
start: {x: number, y: number},
end: {x: number, y: number},
colorStops: $ReadOnlyArray<{
color: ____ColorValue_Internal,
position: number,
}>,
};
Copy link
Contributor

@NickGerleman NickGerleman Jul 23, 2024

Choose a reason for hiding this comment

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

We probably want to keep this in StyleSheetTypes, since apart from the processor, this forms the allowed style input.

A nit here, is that Primitive started propagating after "filter primitive" was used, referring to something in the filters spec unrelated to the actual input. I would love to avoid it in the future if we can.

The spec tends to refer to a single usage of this in a background to be either "background image" or "background image layer" to refer to what this is representing. And each background layer itself an <image>, which is either a either <url> or <gradient>, where then a <gradient> can be one of multiple types. But these may be used outside backgrounds.

Putting that altogether, I think this might make sense as something like a GradientValue type, or even just LinearGradient so we can built that hierarchy in the future, and reuse this for other places with gradients.

};

export default function processBackgroundImage(
backgroundImage: $ReadOnlyArray<BackgroundImagePrimitive> | string,
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add handling for nullish input? Recently hit us somewhere else.

}

function parseCSSLinearGradient(
cssString: string,
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this handle case insensitivity correctly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch. It didn't, fixed and added testcases.

start: bg.start,
end: bg.end,
colorStops: bg.colorStops.map(stop => {
const processedColor = processColor(stop.color) ?? TRANSPARENT;
Copy link
Contributor

@NickGerleman NickGerleman Jul 23, 2024

Choose a reason for hiding this comment

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

This seems like it should be a parse error (return nothing) instead of becoming transparent. Could we add test for this/other error paths?


export default function processBackgroundImage(
backgroundImage: $ReadOnlyArray<BackgroundImagePrimitive> | string,
): $ReadOnlyArray<BackgroundImagePrimitive> {
Copy link
Contributor

Choose a reason for hiding this comment

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

The output has some more guarantees that the input doesn't. E.g. output of processor ColorValue should become ProcessedColorValue, right?

Comment on lines 79 to 80
void setBackgroundImage(T view, @Nullable ReadableArray backgroundImage);

Copy link
Contributor

Choose a reason for hiding this comment

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

If we will only implement this for <View> for now, we probably want to keep this off of BaseViewManager.

I've been working to get us to share more of the background drawing infra, eventually moving some of these to BaseViewManager, but it will take time.

Comment on lines 167 to 169
experimental_backgroundImage: {
process: require('../StyleSheet/processBackgroundImage').default,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Related to comment below about keeping this off of BaseViewManager, if we start with just <View>, could we keep local to that view manager and viewconfig?

@@ -0,0 +1,74 @@
package com.facebook.react.uimanager.drawable;
Copy link
Contributor

@NickGerleman NickGerleman Jul 23, 2024

Choose a reason for hiding this comment

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

  1. We're wanting all new code to be Kotlin instead of Java.
  2. Could we put this in com.facebook.react.uimanager.style instead of com.facebook.react.uimanager.drawable? Potentially just named something like Gradient or LinearGradient?

@@ -166,6 +166,14 @@ BaseViewProps::BaseViewProps(
"experimental_filter",
sourceProps.filter,
{})),
backgroundImage(
CoreFeatures::enablePropIteratorSetter ? sourceProps.backgroundImage
Copy link
Contributor

Choose a reason for hiding this comment

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

Please also add to the area around RAW_SET_PROP_SWITCH_CASE_BASIC

enablePropIteratorSetter is still enabled in a few places internally, and if enabled, it goes through that path, in setProp, instead of the convertRawProp here.

@@ -62,6 +62,7 @@ void ViewShadowNode::initialize() noexcept {
viewProps.importantForAccessibility != ImportantForAccessibility::Auto ||
viewProps.removeClippedSubviews || viewProps.cursor != Cursor::Auto ||
!viewProps.filter.empty() ||
!viewProps.backgroundImage.empty() ||
Copy link
Contributor

@NickGerleman NickGerleman Jul 23, 2024

Choose a reason for hiding this comment

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

This is not the only prop guilty of this, but I think this one should go in formsView, instead of formsStackingContext. I.e. we don't want views with a background image to be flattened, but they also shouldn't form their own stacking context for z-ordering (at least on browsers https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_positioned_layout/Understanding_z-index/Stacking_context)

LinearGradient,
};

struct BackgroundImagePrimitive {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Nick already mentioned it, but as a reminder for this location as well. The original "primitive" was erroneously added by myself after misinterpreting some verbiage in the spec. We have since renamed it and should probably rename it here too.

@intergalacticspacehighway intergalacticspacehighway force-pushed the feat/linear-gradient-android branch from aa80d7e to 0c4e20a Compare July 27, 2024 20:35
@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Jul 29, 2024

cc - @NickGerleman @joevilches addressed all the PR changes, let me know if anything else is needed. Thanks for review!

@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Jul 29, 2024

There are some more specs like supporting px values in color stop position (right now only supports percentage) and color stop hints syntax. We can do that as quick follow ups. Wanted to keep initial implementation simple but matching to spec.

@NickGerleman
Copy link
Contributor

Looking good! One final bit, that I missed initially for box shadows, is that some checks related to view configs need us to do something special when tying to viewconfig process function. I.e. to pattern match f44476d

@intergalacticspacehighway
Copy link
Contributor Author

intergalacticspacehighway commented Jul 29, 2024

checks related to view configs need us to do something special

yeah faced the same issue. Added this check here and customType that fixed the issue.

@facebook-github-bot
Copy link
Contributor

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

mPaint.setShader(getGradientShader());
canvas.drawRect(getBounds(), mPaint);
mPaint.setShader(null);
} else if (Color.alpha(useColor) != 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not important for this change, but I think we want to support background color and images at the same time.

https://www.w3.org/TR/css-backgrounds-3/#background-color

This property sets the background color of a box. This color is drawn behind any background images.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This color is drawn behind any background images.

True, letting it get drawn behind image should be good.

e.g. also tested, below works fine.

if (Color.alpha(useColor) != 0) {}
if (mGradients != null && mGradients.length > 0) {}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated. 2f1bf9e.

@@ -88,6 +91,22 @@ public void setTVPreferredFocus(ReactViewGroup view, boolean hasTVPreferredFocus
}
}

@ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage")
public void setBackgroundImage(ReactViewGroup view, @Nullable ReadableArray backgroundImage) {
Copy link
Contributor

Choose a reason for hiding this comment

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

After this lands, this change will intersect a bit with another one, where I have been refactoring to be able to eventually apply background styles as part of BaseViewManager. bbcca54

This also enables the infra for CompositeBackgroundDrawable, for any future cases where we want to use individual Drawables as units of layering (like, if we wanted to mix Fresco provided bitmap image drawables, between gradients, or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

CompositeBackgroundDrawable would be cool!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is kind of similar to iOS view layers. Exciting!

@facebook-github-bot facebook-github-bot added the Merged This PR has been merged. label Aug 1, 2024
@facebook-github-bot
Copy link
Contributor

@NickGerleman merged this pull request in bd0aedc.

@react-native-bot
Copy link
Collaborator

This pull request was successfully merged by @intergalacticspacehighway in bd0aedc

When will my fix make it into a release? | How to file a pick request?

facebook-github-bot pushed a commit that referenced this pull request Aug 6, 2024
Summary:
- Adds `background` prop that supports CSS's linear gradient. Later this can be extended to support various other gradients and possibly CSS's background image (less motivation as better solutions exists for image)
- Uses `CAGradientlayer` to draw Linear Gradient layers. So it is GPU optimised under the hood.
- Style supports JS object to specify `LinearGradient`, so it can support Animated libraries.

## Changelog:
[IOS] [ADDED] - linear gradient

<!-- Help reviewers and the release process by writing your own changelog entry.

Pick one each for the category and type tags:

[ANDROID|GENERAL|IOS|INTERNAL] [BREAKING|ADDED|CHANGED|DEPRECATED|REMOVED|FIXED|SECURITY] - Message

For more details, see:
https://reactnative.dev/contributing/changelogs-in-pull-requests

Pull Request resolved: #45434

Test Plan:
- Check out `processBackground-test.js` for supported syntax testcases.
- Checkout example added in ViewExample.js

Although the PR is tested well but open to any changes/feedback on the approach taken.

Android PR - #45433. Separated the PRs to keep it easier to review. Both PRs can be reviewed individually.

Reviewed By: NickGerleman

Differential Revision: D60791581

Pulled By: joevilches

fbshipit-source-id: 051088fdf68d9fe20c0c306f1f1c591cbd77f3d5
@intergalacticspacehighway intergalacticspacehighway deleted the feat/linear-gradient-android branch August 9, 2024 22:42
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.

7 participants