mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
feat: linear gradient android (#45433)
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) - 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 <!-- 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: https://github.com/facebook/react-native/pull/45433 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 - https://github.com/facebook/react-native/pull/45433. Separated the PRs to keep it easier to review. Both PRs can be reviewed individually. Reviewed By: joevilches Differential Revision: D60493360 Pulled By: NickGerleman fbshipit-source-id: 762929c4fe16d87cbbd9ebe83ecce96a9e13192c
This commit is contained in:
committed by
Facebook GitHub Bot
parent
6483a28b3a
commit
bd0aedc8c3
@@ -11,6 +11,7 @@
|
||||
import type {AnyAttributeType} from '../../Renderer/shims/ReactNativeTypes';
|
||||
|
||||
import processAspectRatio from '../../StyleSheet/processAspectRatio';
|
||||
import processBackgroundImage from '../../StyleSheet/processBackgroundImage';
|
||||
import processBoxShadow from '../../StyleSheet/processBoxShadow';
|
||||
import processColor from '../../StyleSheet/processColor';
|
||||
import processFilter from '../../StyleSheet/processFilter';
|
||||
@@ -131,6 +132,11 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
|
||||
*/
|
||||
experimental_boxShadow: {process: processBoxShadow},
|
||||
|
||||
/**
|
||||
* Linear Gradient
|
||||
*/
|
||||
experimental_backgroundImage: {process: processBackgroundImage},
|
||||
|
||||
/**
|
||||
* View
|
||||
*/
|
||||
|
||||
@@ -102,6 +102,9 @@ export const __INTERNAL_VIEW_CONFIG: PartialViewConfig =
|
||||
overflow: true,
|
||||
backfaceVisibility: true,
|
||||
experimental_layoutConformance: true,
|
||||
experimental_backgroundImage: {
|
||||
process: require('../../StyleSheet/processBackgroundImage').default,
|
||||
},
|
||||
},
|
||||
}
|
||||
: {
|
||||
|
||||
@@ -14,6 +14,8 @@ import processBoxShadow from '../StyleSheet/processBoxShadow';
|
||||
|
||||
const ReactNativeStyleAttributes = require('../Components/View/ReactNativeStyleAttributes');
|
||||
const resolveAssetSource = require('../Image/resolveAssetSource');
|
||||
const processBackgroundImage =
|
||||
require('../StyleSheet/processBackgroundImage').default;
|
||||
const processColor = require('../StyleSheet/processColor').default;
|
||||
const processColorArray = require('../StyleSheet/processColorArray');
|
||||
const processFilter = require('../StyleSheet/processFilter').default;
|
||||
@@ -193,6 +195,8 @@ function getProcessorForType(typeName: string): ?(nextProp: any) => any {
|
||||
return processColorArray;
|
||||
case 'Filter':
|
||||
return processFilter;
|
||||
case 'BackgroundImage':
|
||||
return processBackgroundImage;
|
||||
case 'ImageSource':
|
||||
return resolveAssetSource;
|
||||
case 'BoxShadow':
|
||||
|
||||
@@ -273,6 +273,16 @@ export type BlendMode =
|
||||
| 'color'
|
||||
| 'luminosity';
|
||||
|
||||
export type GradientValue = {
|
||||
type: 'linearGradient';
|
||||
// Angle or direction enums
|
||||
direction: string | undefined;
|
||||
colorStops: Array<{
|
||||
color: ColorValue;
|
||||
position: number | undefined;
|
||||
}>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @see https://reactnative.dev/docs/view#style
|
||||
*/
|
||||
|
||||
@@ -709,6 +709,16 @@ export type DropShadowPrimitive = {
|
||||
color?: ____ColorValue_Internal,
|
||||
};
|
||||
|
||||
export type GradientValue = {
|
||||
type: 'linearGradient',
|
||||
// Angle or direction enums
|
||||
direction?: string,
|
||||
colorStops: $ReadOnlyArray<{
|
||||
color: ____ColorValue_Internal,
|
||||
position?: string,
|
||||
}>,
|
||||
};
|
||||
|
||||
export type BoxShadowPrimitive = {
|
||||
offsetX: number | string,
|
||||
offsetY: number | string,
|
||||
@@ -781,6 +791,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
|
||||
experimental_boxShadow?: $ReadOnlyArray<BoxShadowPrimitive> | string,
|
||||
experimental_filter?: $ReadOnlyArray<FilterFunction> | string,
|
||||
experimental_mixBlendMode?: ____BlendMode_Internal,
|
||||
experimental_backgroundImage?: $ReadOnlyArray<GradientValue> | string,
|
||||
}>;
|
||||
|
||||
export type ____ViewStyle_Internal = $ReadOnly<{
|
||||
|
||||
+381
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @format
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import processBackgroundImage from '../processBackgroundImage';
|
||||
|
||||
const processColor = require('../processColor').default;
|
||||
|
||||
describe('processBackgroundImage', () => {
|
||||
it('should process a simple linear gradient string', () => {
|
||||
const input = 'linear-gradient(to right, red, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'linearGradient',
|
||||
start: {x: 0, y: 0.5},
|
||||
end: {x: 1, y: 0.5},
|
||||
colorStops: [
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('blue'), position: 1},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a diagonal linear gradient', () => {
|
||||
const input = 'linear-gradient(to bottom right, red, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'linearGradient',
|
||||
start: {x: 0, y: 0},
|
||||
end: {x: 1, y: 1},
|
||||
colorStops: [
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('blue'), position: 1},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array for null values', () => {
|
||||
let result = processBackgroundImage('');
|
||||
expect(result).toEqual([]);
|
||||
result = processBackgroundImage(null);
|
||||
expect(result).toEqual([]);
|
||||
result = processBackgroundImage(undefined);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for invalid values', () => {
|
||||
let result = processBackgroundImage('linear-');
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient with whitespaces in direction', () => {
|
||||
const input = 'linear-gradient(to bottom right, red, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'linearGradient',
|
||||
start: {x: 0, y: 0},
|
||||
end: {x: 1, y: 1},
|
||||
colorStops: [
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('blue'), position: 1},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient with random whitespaces', () => {
|
||||
const input =
|
||||
' linear-gradient(to bottom right, red 30%, blue 80%) ';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'linearGradient',
|
||||
start: {x: 0, y: 0},
|
||||
end: {x: 1, y: 1},
|
||||
colorStops: [
|
||||
{color: processColor('red'), position: 0.3},
|
||||
{color: processColor('blue'), position: 0.8},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient with angle', () => {
|
||||
const input = 'linear-gradient(45deg, red, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].type).toBe('linearGradient');
|
||||
expect(result[0].start.x).toBeCloseTo(0.146447, 5);
|
||||
expect(result[0].start.y).toBeCloseTo(0.853553, 5);
|
||||
expect(result[0].end.x).toBeCloseTo(0.853553, 5);
|
||||
expect(result[0].end.y).toBeCloseTo(0.146447, 5);
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('blue'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient with case-insensitive angle', () => {
|
||||
const input = 'linear-gradient(45Deg, red, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].type).toBe('linearGradient');
|
||||
expect(result[0].start.x).toBeCloseTo(0.146447, 5);
|
||||
expect(result[0].start.y).toBeCloseTo(0.853553, 5);
|
||||
expect(result[0].end.x).toBeCloseTo(0.853553, 5);
|
||||
expect(result[0].end.y).toBeCloseTo(0.146447, 5);
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('blue'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('linear gradient case-insensitive string', () => {
|
||||
const input = 'LiNeAr-GradieNt(To Bottom, Red, Blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].type).toBe('linearGradient');
|
||||
expect(result[0].start).toEqual({x: 0.5, y: 0});
|
||||
expect(result[0].end).toEqual({x: 0.5, y: 1});
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('blue'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient with case-insensitive direction enum', () => {
|
||||
const input = 'linear-gradient(tO Right, red, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].start).toEqual({x: 0, y: 0.5});
|
||||
expect(result[0].end).toEqual({x: 1, y: 0.5});
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('blue'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient with case-insensitive colors', () => {
|
||||
const input =
|
||||
'linear-gradient(to right, Rgba(0, 0, 0, 0.5), Blue, Hsla(0, 100%, 50%, 0.5))';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].start).toEqual({x: 0, y: 0.5});
|
||||
expect(result[0].end).toEqual({x: 1, y: 0.5});
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('rgba(0, 0, 0, 0.5)'), position: 0},
|
||||
{color: processColor('blue'), position: 0.5},
|
||||
{color: processColor('hsla(0, 100%, 50%, 0.5)'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process multiple linear gradients', () => {
|
||||
const input = `
|
||||
linear-gradient(to right, red, blue),
|
||||
linear-gradient(to bottom, green, yellow)`;
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].type).toEqual('linearGradient');
|
||||
expect(result[0].start).toEqual({x: 0, y: 0.5});
|
||||
expect(result[0].end).toEqual({x: 1, y: 0.5});
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('blue'), position: 1},
|
||||
]);
|
||||
expect(result[1].type).toEqual('linearGradient');
|
||||
expect(result[1].start).toEqual({x: 0.5, y: 0});
|
||||
expect(result[1].end).toEqual({x: 0.5, y: 1});
|
||||
|
||||
expect(result[1].colorStops).toEqual([
|
||||
{color: processColor('green'), position: 0},
|
||||
{color: processColor('yellow'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient with multiple color stops', () => {
|
||||
const input = 'linear-gradient(to right, red 0%, green 50%, blue 100%)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('green'), position: 0.5},
|
||||
{color: processColor('blue'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should add color stop postion if position is not specified', () => {
|
||||
const input =
|
||||
'linear-gradient(to right, red, green, blue 60%, yellow, purple)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('green'), position: 0.25},
|
||||
{color: processColor('blue'), position: 0.6},
|
||||
{color: processColor('yellow'), position: 0.75},
|
||||
{color: processColor('purple'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient with rgba colors', () => {
|
||||
const input =
|
||||
'linear-gradient(to right, rgba(255,0,0,0.5), rgba(0,0,255,0.8))';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('rgba(255,0,0,0.5)'), position: 0},
|
||||
{color: processColor('rgba(0,0,255,0.8)'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient with hsl colors', () => {
|
||||
const input = `linear-gradient(hsl(330, 100%, 45.1%), hsl(0, 100%, 50%))`;
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('hsl(330, 100%, 45.1%)'), position: 0},
|
||||
{color: processColor('hsl(0, 100%, 50%)'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process a linear gradient without direction', () => {
|
||||
const input = 'linear-gradient(#e66465, #9198e5)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('#e66465'), position: 0},
|
||||
{color: processColor('#9198e5'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process multiple gradients with spaces', () => {
|
||||
const input = `linear-gradient(to right ,
|
||||
rgba(255,0,0,0.5), rgba(0,0,255,0.8)),
|
||||
linear-gradient(to bottom , rgba(255,0,0,0.9) , rgba(0,0,255,0.2) )`;
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0].start).toEqual({x: 0, y: 0.5});
|
||||
expect(result[0].end).toEqual({x: 1, y: 0.5});
|
||||
expect(result[1].start).toEqual({x: 0.5, y: 0});
|
||||
expect(result[1].end).toEqual({x: 0.5, y: 1});
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{color: processColor('rgba(255,0,0,0.5)'), position: 0},
|
||||
{color: processColor('rgba(0,0,255,0.8)'), position: 1},
|
||||
]);
|
||||
expect(result[1].colorStops).toEqual([
|
||||
{color: processColor('rgba(255,0,0,0.9)'), position: 0},
|
||||
{color: processColor('rgba(0,0,255,0.2)'), position: 1},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty array for invalid color in linear gradient', () => {
|
||||
const input = 'linear-gradient(45deg, rede, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for invalid angle in linear gradient', () => {
|
||||
const input = 'linear-gradient(45 deg, red, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for invalid direction enum in linear gradient', () => {
|
||||
const input = 'linear-gradient(to left2, red, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array for invalid color stop unit', () => {
|
||||
const input = 'linear-gradient(to left, red 5, blue)';
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should process an array of style objects', () => {
|
||||
const input = [
|
||||
{
|
||||
type: 'linearGradient',
|
||||
direction: 'to bottom right',
|
||||
colorStops: [
|
||||
{color: 'red', position: '0%'},
|
||||
{color: 'blue', position: '100%'},
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result).toEqual([
|
||||
{
|
||||
type: 'linearGradient',
|
||||
start: {x: 0, y: 0},
|
||||
end: {x: 1, y: 1},
|
||||
colorStops: [
|
||||
{color: processColor('red'), position: 0},
|
||||
{color: processColor('blue'), position: 1},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process an style object with default direction', () => {
|
||||
const input = [
|
||||
{
|
||||
type: 'linearGradient',
|
||||
colorStops: [{color: 'red'}, {color: 'blue'}],
|
||||
},
|
||||
];
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].start).toEqual({x: 0.5, y: 0});
|
||||
expect(result[0].end).toEqual({x: 0.5, y: 1});
|
||||
});
|
||||
|
||||
it('should process style object with direction enum', () => {
|
||||
const input = [
|
||||
{
|
||||
type: 'linearGradient',
|
||||
direction: 'to right',
|
||||
colorStops: [{color: 'red'}, {color: 'blue'}],
|
||||
},
|
||||
];
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].start).toEqual({x: 0, y: 0.5});
|
||||
expect(result[0].end).toEqual({x: 1, y: 0.5});
|
||||
});
|
||||
|
||||
it('should process style object with direction angle', () => {
|
||||
const input = [
|
||||
{
|
||||
type: 'linearGradient',
|
||||
direction: '45deg',
|
||||
colorStops: [{color: 'red'}, {color: 'blue'}],
|
||||
},
|
||||
];
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].start.x).toBeCloseTo(0.146447, 5);
|
||||
expect(result[0].start.y).toBeCloseTo(0.853553, 5);
|
||||
expect(result[0].end.x).toBeCloseTo(0.853553, 5);
|
||||
expect(result[0].end.y).toBeCloseTo(0.146447, 5);
|
||||
});
|
||||
|
||||
it('should process an style object with mix of default and undefined stop positions', () => {
|
||||
const input = [
|
||||
{
|
||||
type: 'linearGradient',
|
||||
colorStops: [
|
||||
{color: 'red'},
|
||||
{color: 'blue'},
|
||||
{color: 'green'},
|
||||
{color: 'purple', position: '80%'},
|
||||
{color: 'pink'},
|
||||
],
|
||||
},
|
||||
];
|
||||
const result = processBackgroundImage(input);
|
||||
expect(result[0].colorStops).toEqual([
|
||||
{
|
||||
color: processColor('red'),
|
||||
position: 0,
|
||||
},
|
||||
{
|
||||
color: processColor('blue'),
|
||||
position: 0.25,
|
||||
},
|
||||
{
|
||||
color: processColor('green'),
|
||||
position: 0.5,
|
||||
},
|
||||
{
|
||||
color: processColor('purple'),
|
||||
position: 0.8,
|
||||
},
|
||||
{
|
||||
color: processColor('pink'),
|
||||
position: 1,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @format
|
||||
* @flow strict-local
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {ProcessedColorValue} from './processColor';
|
||||
import type {GradientValue} from './StyleSheetTypes';
|
||||
|
||||
const processColor = require('./processColor').default;
|
||||
const DIRECTION_REGEX =
|
||||
/^to\s+(?:top|bottom|left|right)(?:\s+(?:top|bottom|left|right))?/;
|
||||
const ANGLE_UNIT_REGEX = /^([+-]?\d*\.?\d+)(deg|grad|rad|turn)$/i;
|
||||
|
||||
const TO_BOTTOM_START_END_POINTS = {
|
||||
start: {x: 0.5, y: 0},
|
||||
end: {x: 0.5, y: 1},
|
||||
};
|
||||
|
||||
type ParsedGradientValue = {
|
||||
type: 'linearGradient',
|
||||
start: {x: number, y: number},
|
||||
end: {x: number, y: number},
|
||||
colorStops: $ReadOnlyArray<{
|
||||
color: ProcessedColorValue,
|
||||
position: number,
|
||||
}>,
|
||||
};
|
||||
|
||||
export default function processBackgroundImage(
|
||||
backgroundImage: ?($ReadOnlyArray<GradientValue> | string),
|
||||
): $ReadOnlyArray<ParsedGradientValue> {
|
||||
let result: $ReadOnlyArray<ParsedGradientValue> = [];
|
||||
if (backgroundImage == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (typeof backgroundImage === 'string') {
|
||||
result = parseCSSLinearGradient(backgroundImage);
|
||||
} else if (Array.isArray(backgroundImage)) {
|
||||
for (const bgImage of backgroundImage) {
|
||||
const processedColorStops = [];
|
||||
for (let index = 0; index < bgImage.colorStops.length; index++) {
|
||||
const stop = bgImage.colorStops[index];
|
||||
const processedColor = processColor(stop.color);
|
||||
let processedPosition: number | null = null;
|
||||
|
||||
// Currently we only support percentage and undefined value for color stop position.
|
||||
if (typeof stop.position === 'undefined') {
|
||||
processedPosition =
|
||||
bgImage.colorStops.length === 1
|
||||
? 1
|
||||
: index / (bgImage.colorStops.length - 1);
|
||||
} else if (stop.position.endsWith('%')) {
|
||||
processedPosition = parseFloat(stop.position) / 100;
|
||||
} else {
|
||||
// If a color stop position is invalid, return an empty array and do not apply gradient. Same as web.
|
||||
return [];
|
||||
}
|
||||
|
||||
if (processedColor != null) {
|
||||
processedColorStops.push({
|
||||
color: processedColor,
|
||||
position: processedPosition,
|
||||
});
|
||||
} else {
|
||||
// If a color is invalid, return an empty array and do not apply gradient. Same as web.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
let points: {
|
||||
start: ParsedGradientValue['start'],
|
||||
end: ParsedGradientValue['end'],
|
||||
} | null = null;
|
||||
|
||||
if (typeof bgImage.direction === 'undefined') {
|
||||
points = TO_BOTTOM_START_END_POINTS;
|
||||
} else if (ANGLE_UNIT_REGEX.test(bgImage.direction)) {
|
||||
const angle = parseAngle(bgImage.direction);
|
||||
if (angle != null) {
|
||||
points = calculateStartEndPointsFromAngle(angle);
|
||||
}
|
||||
} else if (DIRECTION_REGEX.test(bgImage.direction)) {
|
||||
const processedPoints = calculateStartEndPointsFromDirection(
|
||||
bgImage.direction,
|
||||
);
|
||||
if (processedPoints != null) {
|
||||
points = processedPoints;
|
||||
}
|
||||
}
|
||||
|
||||
if (points != null) {
|
||||
result = result.concat({
|
||||
type: 'linearGradient',
|
||||
start: points.start,
|
||||
end: points.end,
|
||||
colorStops: processedColorStops,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function parseCSSLinearGradient(
|
||||
cssString: string,
|
||||
): $ReadOnlyArray<ParsedGradientValue> {
|
||||
const gradients = [];
|
||||
let match;
|
||||
const linearGradientRegex = /linear-gradient\s*\(((?:\([^)]*\)|[^())])*)\)/gi;
|
||||
|
||||
while ((match = linearGradientRegex.exec(cssString))) {
|
||||
const gradientContent = match[1];
|
||||
const parts = gradientContent.split(',');
|
||||
let points = TO_BOTTOM_START_END_POINTS;
|
||||
const trimmedDirection = parts[0].trim().toLowerCase();
|
||||
const colorStopRegex =
|
||||
/\s*((?:(?:rgba?|hsla?)\s*\([^)]+\))|#[0-9a-fA-F]+|[a-zA-Z]+)(?:\s+([0-9.]+%?))?\s*/gi;
|
||||
|
||||
if (ANGLE_UNIT_REGEX.test(trimmedDirection)) {
|
||||
const angle = parseAngle(trimmedDirection);
|
||||
if (angle != null) {
|
||||
points = calculateStartEndPointsFromAngle(angle);
|
||||
parts.shift();
|
||||
} else {
|
||||
// If an angle is invalid, return an empty array and do not apply any gradient. Same as web.
|
||||
return [];
|
||||
}
|
||||
} else if (DIRECTION_REGEX.test(trimmedDirection)) {
|
||||
const parsedPoints =
|
||||
calculateStartEndPointsFromDirection(trimmedDirection);
|
||||
if (parsedPoints != null) {
|
||||
points = parsedPoints;
|
||||
parts.shift();
|
||||
} else {
|
||||
// If a direction is invalid, return an empty array and do not apply any gradient. Same as web.
|
||||
return [];
|
||||
}
|
||||
} else if (!colorStopRegex.test(trimmedDirection)) {
|
||||
// If first part is not an angle/direction or a color stop, return an empty array and do not apply any gradient. Same as web.
|
||||
return [];
|
||||
}
|
||||
colorStopRegex.lastIndex = 0;
|
||||
|
||||
const colorStops = [];
|
||||
const fullColorStopsStr = parts.join(',');
|
||||
let colorStopMatch;
|
||||
while ((colorStopMatch = colorStopRegex.exec(fullColorStopsStr))) {
|
||||
const [, color, position] = colorStopMatch;
|
||||
const processedColor = processColor(color.trim().toLowerCase());
|
||||
if (
|
||||
processedColor != null &&
|
||||
(typeof position === 'undefined' || position.endsWith('%'))
|
||||
) {
|
||||
colorStops.push({
|
||||
color: processedColor,
|
||||
position: position ? parseFloat(position) / 100 : null,
|
||||
});
|
||||
} else {
|
||||
// If a color or position is invalid, return an empty array and do not apply any gradient. Same as web.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
gradients.push({
|
||||
type: 'linearGradient',
|
||||
start: points.start,
|
||||
end: points.end,
|
||||
colorStops: colorStops.map((stop, index, array) => ({
|
||||
color: stop.color,
|
||||
position:
|
||||
stop.position ??
|
||||
(array.length === 1 ? 1 : index / (array.length - 1)),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
return gradients;
|
||||
}
|
||||
|
||||
function calculateStartEndPointsFromDirection(direction: string): ?{
|
||||
start: {x: number, y: number},
|
||||
end: {x: number, y: number},
|
||||
} {
|
||||
// Remove extra whitespace
|
||||
const normalizedDirection = direction.replace(/\s+/g, ' ');
|
||||
|
||||
switch (normalizedDirection) {
|
||||
case 'to right':
|
||||
return {
|
||||
start: {x: 0, y: 0.5},
|
||||
end: {x: 1, y: 0.5},
|
||||
};
|
||||
case 'to left':
|
||||
return {
|
||||
start: {x: 1, y: 0.5},
|
||||
end: {x: 0, y: 0.5},
|
||||
};
|
||||
case 'to bottom':
|
||||
return TO_BOTTOM_START_END_POINTS;
|
||||
case 'to top':
|
||||
return {
|
||||
start: {x: 0.5, y: 1},
|
||||
end: {x: 0.5, y: 0},
|
||||
};
|
||||
case 'to bottom right':
|
||||
case 'to right bottom':
|
||||
return {
|
||||
start: {x: 0, y: 0},
|
||||
end: {x: 1, y: 1},
|
||||
};
|
||||
case 'to top left':
|
||||
case 'to left top':
|
||||
return {
|
||||
start: {x: 1, y: 1},
|
||||
end: {x: 0, y: 0},
|
||||
};
|
||||
case 'to bottom left':
|
||||
case 'to left bottom':
|
||||
return {
|
||||
start: {x: 1, y: 0},
|
||||
end: {x: 0, y: 1},
|
||||
};
|
||||
case 'to top right':
|
||||
case 'to right top':
|
||||
return {
|
||||
start: {x: 0, y: 1},
|
||||
end: {x: 1, y: 0},
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateStartEndPointsFromAngle(angleRadians: number): {
|
||||
start: {x: number, y: number},
|
||||
end: {x: number, y: number},
|
||||
} {
|
||||
// Normalize angle to be between 0 and 2π
|
||||
let angleRadiansNormalized = angleRadians % (2 * Math.PI);
|
||||
if (angleRadiansNormalized < 0) {
|
||||
angleRadiansNormalized += 2 * Math.PI;
|
||||
}
|
||||
|
||||
const endX = 0.5 + 0.5 * Math.sin(angleRadiansNormalized);
|
||||
const endY = 0.5 - 0.5 * Math.cos(angleRadiansNormalized);
|
||||
|
||||
const startX = 1 - endX;
|
||||
const startY = 1 - endY;
|
||||
|
||||
return {
|
||||
start: {x: startX, y: startY},
|
||||
end: {x: endX, y: endY},
|
||||
};
|
||||
}
|
||||
|
||||
function parseAngle(angle: string): ?number {
|
||||
const match = angle.match(ANGLE_UNIT_REGEX);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [, value, unit] = match;
|
||||
|
||||
const numericValue = parseFloat(value);
|
||||
switch (unit) {
|
||||
case 'deg':
|
||||
return (numericValue * Math.PI) / 180;
|
||||
case 'grad':
|
||||
return (numericValue * Math.PI) / 200;
|
||||
case 'rad':
|
||||
return numericValue;
|
||||
case 'turn':
|
||||
return numericValue * 2 * Math.PI;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -8271,6 +8271,14 @@ export type DropShadowPrimitive = {
|
||||
standardDeviation?: number | string,
|
||||
color?: ____ColorValue_Internal,
|
||||
};
|
||||
export type GradientValue = {
|
||||
type: \\"linearGradient\\",
|
||||
direction?: string,
|
||||
colorStops: $ReadOnlyArray<{
|
||||
color: ____ColorValue_Internal,
|
||||
position?: string,
|
||||
}>,
|
||||
};
|
||||
export type BoxShadowPrimitive = {
|
||||
offsetX: number | string,
|
||||
offsetY: number | string,
|
||||
@@ -8341,6 +8349,7 @@ export type ____ViewStyle_InternalCore = $ReadOnly<{
|
||||
experimental_boxShadow?: $ReadOnlyArray<BoxShadowPrimitive> | string,
|
||||
experimental_filter?: $ReadOnlyArray<FilterFunction> | string,
|
||||
experimental_mixBlendMode?: ____BlendMode_Internal,
|
||||
experimental_backgroundImage?: $ReadOnlyArray<GradientValue> | string,
|
||||
}>;
|
||||
export type ____ViewStyle_Internal = $ReadOnly<{
|
||||
...____ViewStyle_InternalCore,
|
||||
@@ -8582,6 +8591,22 @@ declare module.exports: processAspectRatio;
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`public API should not change unintentionally Libraries/StyleSheet/processBackgroundImage.js 1`] = `
|
||||
"type ParsedGradientValue = {
|
||||
type: \\"linearGradient\\",
|
||||
start: { x: number, y: number },
|
||||
end: { x: number, y: number },
|
||||
colorStops: $ReadOnlyArray<{
|
||||
color: ProcessedColorValue,
|
||||
position: number,
|
||||
}>,
|
||||
};
|
||||
declare export default function processBackgroundImage(
|
||||
backgroundImage: ?($ReadOnlyArray<GradientValue> | string)
|
||||
): $ReadOnlyArray<ParsedGradientValue>;
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`public API should not change unintentionally Libraries/StyleSheet/processBoxShadow.js 1`] = `
|
||||
"export type ParsedBoxShadow = {
|
||||
offsetX: number,
|
||||
|
||||
@@ -5498,6 +5498,7 @@ public final class com/facebook/react/uimanager/ViewProps {
|
||||
public static final field ASPECT_RATIO Ljava/lang/String;
|
||||
public static final field AUTO Ljava/lang/String;
|
||||
public static final field BACKGROUND_COLOR Ljava/lang/String;
|
||||
public static final field BACKGROUND_IMAGE Ljava/lang/String;
|
||||
public static final field BORDER_BLOCK_COLOR Ljava/lang/String;
|
||||
public static final field BORDER_BLOCK_END_COLOR Ljava/lang/String;
|
||||
public static final field BORDER_BLOCK_START_COLOR Ljava/lang/String;
|
||||
@@ -6127,6 +6128,11 @@ public final class com/facebook/react/uimanager/style/ComputedBorderRadiusProp :
|
||||
public static fun values ()[Lcom/facebook/react/uimanager/style/ComputedBorderRadiusProp;
|
||||
}
|
||||
|
||||
public final class com/facebook/react/uimanager/style/Gradient {
|
||||
public fun <init> (Lcom/facebook/react/bridge/ReadableMap;)V
|
||||
public final fun getShader (Landroid/graphics/Rect;)Landroid/graphics/Shader;
|
||||
}
|
||||
|
||||
public abstract class com/facebook/react/uimanager/style/LogicalEdge : java/lang/Enum {
|
||||
public static final field ALL Lcom/facebook/react/uimanager/style/LogicalEdge;
|
||||
public static final field BLOCK Lcom/facebook/react/uimanager/style/LogicalEdge;
|
||||
@@ -8244,6 +8250,7 @@ public class com/facebook/react/views/view/ReactViewManager : com/facebook/react
|
||||
public fun setBackfaceVisibility (Lcom/facebook/react/views/view/ReactViewGroup;Ljava/lang/String;)V
|
||||
public synthetic fun setBackgroundColor (Landroid/view/View;I)V
|
||||
public fun setBackgroundColor (Lcom/facebook/react/views/view/ReactViewGroup;I)V
|
||||
public fun setBackgroundImage (Lcom/facebook/react/views/view/ReactViewGroup;Lcom/facebook/react/bridge/ReadableArray;)V
|
||||
public fun setBorderColor (Lcom/facebook/react/views/view/ReactViewGroup;ILjava/lang/Integer;)V
|
||||
public fun setBorderRadius (Lcom/facebook/react/views/view/ReactViewGroup;IF)V
|
||||
public fun setBorderRadius (Lcom/facebook/react/views/view/ReactViewGroup;ILcom/facebook/react/bridge/Dynamic;)V
|
||||
|
||||
+1
@@ -74,6 +74,7 @@ public object ViewProps {
|
||||
// Props that affect more than just layout
|
||||
public const val ENABLED: String = "enabled"
|
||||
public const val BACKGROUND_COLOR: String = "backgroundColor"
|
||||
public const val BACKGROUND_IMAGE: String = "experimental_backgroundImage"
|
||||
public const val FOREGROUND_COLOR: String = "foregroundColor"
|
||||
public const val COLOR: String = "color"
|
||||
public const val FONT_SIZE: String = "fontSize"
|
||||
|
||||
+46
-2
@@ -12,15 +12,18 @@ import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.ComposeShader;
|
||||
import android.graphics.DashPathEffect;
|
||||
import android.graphics.Outline;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PathEffect;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Region;
|
||||
import android.graphics.Shader;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.view.View;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -38,6 +41,7 @@ import com.facebook.react.uimanager.style.BorderRadiusProp;
|
||||
import com.facebook.react.uimanager.style.BorderRadiusStyle;
|
||||
import com.facebook.react.uimanager.style.BorderStyle;
|
||||
import com.facebook.react.uimanager.style.ComputedBorderRadius;
|
||||
import com.facebook.react.uimanager.style.Gradient;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -109,6 +113,7 @@ public class CSSBackgroundDrawable extends Drawable {
|
||||
/* Used by all types of background and for drawing borders */
|
||||
private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private int mColor = Color.TRANSPARENT;
|
||||
private @Nullable Gradient[] mGradients = null;
|
||||
private int mAlpha = 255;
|
||||
|
||||
// There is a small gap between the edges of adjacent paths
|
||||
@@ -328,6 +333,11 @@ public class CSSBackgroundDrawable extends Drawable {
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
public void setGradients(Gradient[] gradients) {
|
||||
mGradients = gradients;
|
||||
invalidateSelf();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public int getColor() {
|
||||
return mColor;
|
||||
@@ -377,12 +387,19 @@ public class CSSBackgroundDrawable extends Drawable {
|
||||
|
||||
// Draws the View without its border first (with background color fill)
|
||||
int useColor = ColorUtils.setAlphaComponent(mColor, getOpacity());
|
||||
if (Color.alpha(useColor) != 0) { // color is not transparent
|
||||
if (Color.alpha(useColor) != 0) {
|
||||
mPaint.setColor(useColor);
|
||||
mPaint.setStyle(Paint.Style.FILL);
|
||||
canvas.drawPath(Preconditions.checkNotNull(mBackgroundColorRenderPath), mPaint);
|
||||
}
|
||||
|
||||
if (mGradients != null && mGradients.length > 0) {
|
||||
mPaint.setShader(getGradientShader());
|
||||
mPaint.setStyle(Paint.Style.FILL);
|
||||
canvas.drawPath(Preconditions.checkNotNull(mBackgroundColorRenderPath), mPaint);
|
||||
mPaint.setShader(null);
|
||||
}
|
||||
|
||||
final RectF borderWidth = getDirectionAwareBorderInsets();
|
||||
int colorLeft = getBorderColor(Spacing.LEFT);
|
||||
int colorTop = getBorderColor(Spacing.TOP);
|
||||
@@ -1105,11 +1122,17 @@ public class CSSBackgroundDrawable extends Drawable {
|
||||
mPaint.setStyle(Paint.Style.FILL);
|
||||
|
||||
int useColor = multiplyColorAlpha(mColor, mAlpha);
|
||||
if (Color.alpha(useColor) != 0) { // color is not transparent
|
||||
if (Color.alpha(useColor) != 0) {
|
||||
mPaint.setColor(useColor);
|
||||
canvas.drawRect(getBounds(), mPaint);
|
||||
}
|
||||
|
||||
if (mGradients != null && mGradients.length > 0) {
|
||||
mPaint.setShader(getGradientShader());
|
||||
canvas.drawRect(getBounds(), mPaint);
|
||||
mPaint.setShader(null);
|
||||
}
|
||||
|
||||
final RectF borderWidth = getDirectionAwareBorderInsets();
|
||||
|
||||
final int borderLeft = Math.round(borderWidth.left);
|
||||
@@ -1401,6 +1424,27 @@ public class CSSBackgroundDrawable extends Drawable {
|
||||
return new RectF(borderLeftWidth, borderTopWidth, borderRightWidth, borderBottomWidth);
|
||||
}
|
||||
|
||||
private @Nullable Shader getGradientShader() {
|
||||
if (mGradients == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Shader compositeShader = null;
|
||||
for (Gradient gradient : mGradients) {
|
||||
Shader currentShader = gradient.getShader(getBounds());
|
||||
if (currentShader == null) {
|
||||
continue;
|
||||
}
|
||||
if (compositeShader == null) {
|
||||
compositeShader = currentShader;
|
||||
} else {
|
||||
compositeShader =
|
||||
new ComposeShader(currentShader, compositeShader, PorterDuff.Mode.SRC_OVER);
|
||||
}
|
||||
}
|
||||
return compositeShader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Multiplies the color with the given alpha.
|
||||
*
|
||||
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.graphics.LinearGradient
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Shader
|
||||
import com.facebook.react.bridge.ReadableMap
|
||||
|
||||
public class Gradient(gradient: ReadableMap?) {
|
||||
private enum class GradientType {
|
||||
LINEAR_GRADIENT
|
||||
}
|
||||
|
||||
private val type: GradientType
|
||||
private var startX: Float = 0f
|
||||
private var startY: Float = 0f
|
||||
private var endX: Float = 0f
|
||||
private var endY: Float = 0f
|
||||
private val colors: IntArray
|
||||
private val positions: FloatArray
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
gradient.getMap("start")?.let { start ->
|
||||
startX = start.getDouble("x").toFloat()
|
||||
startY = start.getDouble("y").toFloat()
|
||||
}
|
||||
|
||||
gradient.getMap("end")?.let { end ->
|
||||
endX = end.getDouble("x").toFloat()
|
||||
endY = end.getDouble("y").toFloat()
|
||||
}
|
||||
|
||||
val colorStops =
|
||||
gradient.getArray("colorStops")
|
||||
?: throw IllegalArgumentException("Invalid colorStops array")
|
||||
|
||||
val size = colorStops.size()
|
||||
colors = IntArray(size)
|
||||
positions = FloatArray(size)
|
||||
|
||||
for (i in 0 until size) {
|
||||
val colorStop = colorStops.getMap(i)
|
||||
colors[i] = colorStop.getInt("color")
|
||||
positions[i] = colorStop.getDouble("position").toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
public fun getShader(bounds: Rect): Shader? {
|
||||
return when (type) {
|
||||
GradientType.LINEAR_GRADIENT ->
|
||||
LinearGradient(
|
||||
startX * bounds.width(),
|
||||
startY * bounds.height(),
|
||||
endX * bounds.width(),
|
||||
endY * bounds.height(),
|
||||
colors,
|
||||
positions,
|
||||
Shader.TileMode.CLAMP)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -31,6 +31,7 @@ import com.facebook.react.bridge.ReactContext;
|
||||
import com.facebook.react.bridge.ReactNoCrashSoftException;
|
||||
import com.facebook.react.bridge.ReactSoftExceptionLogger;
|
||||
import com.facebook.react.bridge.UiThreadUtil;
|
||||
import com.facebook.react.common.annotations.UnstableReactNativeAPI;
|
||||
import com.facebook.react.common.annotations.VisibleForTesting;
|
||||
import com.facebook.react.internal.featureflags.ReactNativeFeatureFlags;
|
||||
import com.facebook.react.modules.i18nmanager.I18nUtil;
|
||||
@@ -59,6 +60,7 @@ import com.facebook.react.uimanager.drawable.CSSBackgroundDrawable;
|
||||
import com.facebook.react.uimanager.style.BorderRadiusProp;
|
||||
import com.facebook.react.uimanager.style.BorderStyle;
|
||||
import com.facebook.react.uimanager.style.ComputedBorderRadius;
|
||||
import com.facebook.react.uimanager.style.Gradient;
|
||||
import com.facebook.react.uimanager.style.LogicalEdge;
|
||||
import com.facebook.react.uimanager.style.Overflow;
|
||||
|
||||
@@ -233,6 +235,11 @@ public class ReactViewGroup extends ViewGroup
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableReactNativeAPI
|
||||
/*package*/ void setGradients(@Nullable Gradient[] gradient) {
|
||||
getOrCreateReactViewBackground().setGradients(gradient);
|
||||
}
|
||||
|
||||
@Deprecated(since = "0.66.0", forRemoval = true)
|
||||
public void setTranslucentBackgroundDrawable(@Nullable Drawable background) {
|
||||
if (ReactNativeFeatureFlags.enableBackgroundStyleApplicator()) {
|
||||
|
||||
+21
@@ -12,6 +12,7 @@ import android.view.View;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.common.logging.FLog;
|
||||
import com.facebook.infer.annotation.Nullsafe;
|
||||
import com.facebook.react.bridge.Dynamic;
|
||||
import com.facebook.react.bridge.DynamicFromObject;
|
||||
import com.facebook.react.bridge.JSApplicationIllegalArgumentException;
|
||||
@@ -33,14 +34,18 @@ import com.facebook.react.uimanager.UIManagerHelper;
|
||||
import com.facebook.react.uimanager.ViewProps;
|
||||
import com.facebook.react.uimanager.annotations.ReactProp;
|
||||
import com.facebook.react.uimanager.annotations.ReactPropGroup;
|
||||
import com.facebook.react.uimanager.common.UIManagerType;
|
||||
import com.facebook.react.uimanager.common.ViewUtil;
|
||||
import com.facebook.react.uimanager.events.EventDispatcher;
|
||||
import com.facebook.react.uimanager.style.BorderRadiusProp;
|
||||
import com.facebook.react.uimanager.style.BorderStyle;
|
||||
import com.facebook.react.uimanager.style.Gradient;
|
||||
import com.facebook.react.uimanager.style.LogicalEdge;
|
||||
import java.util.Map;
|
||||
|
||||
/** View manager for AndroidViews (plain React Views). */
|
||||
@ReactModule(name = ReactViewManager.REACT_CLASS)
|
||||
@Nullsafe(Nullsafe.Mode.LOCAL)
|
||||
public class ReactViewManager extends ReactClippingViewManager<ReactViewGroup> {
|
||||
|
||||
@VisibleForTesting public static final String REACT_CLASS = ViewProps.VIEW_CLASS_NAME;
|
||||
@@ -92,6 +97,22 @@ public class ReactViewManager extends ReactClippingViewManager<ReactViewGroup> {
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = ViewProps.BACKGROUND_IMAGE, customType = "BackgroundImage")
|
||||
public void setBackgroundImage(ReactViewGroup view, @Nullable ReadableArray backgroundImage) {
|
||||
if (ViewUtil.getUIManagerType(view) == UIManagerType.FABRIC) {
|
||||
if (backgroundImage != null && backgroundImage.size() > 0) {
|
||||
Gradient[] gradients = new Gradient[backgroundImage.size()];
|
||||
for (int i = 0; i < backgroundImage.size(); i++) {
|
||||
ReadableMap gradientMap = backgroundImage.getMap(i);
|
||||
gradients[i] = new Gradient(gradientMap);
|
||||
}
|
||||
view.setGradients(gradients);
|
||||
} else {
|
||||
view.setGradients(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ReactProp(name = "nextFocusDown", defaultInt = View.NO_ID)
|
||||
public void nextFocusDown(ReactViewGroup view, int viewId) {
|
||||
view.setNextFocusDownId(viewId);
|
||||
|
||||
@@ -166,6 +166,15 @@ BaseViewProps::BaseViewProps(
|
||||
"experimental_filter",
|
||||
sourceProps.filter,
|
||||
{})),
|
||||
backgroundImage(
|
||||
CoreFeatures::enablePropIteratorSetter
|
||||
? sourceProps.backgroundImage
|
||||
: convertRawProp(
|
||||
context,
|
||||
rawProps,
|
||||
"experimental_backgroundImage",
|
||||
sourceProps.backgroundImage,
|
||||
{})),
|
||||
mixBlendMode(
|
||||
CoreFeatures::enablePropIteratorSetter
|
||||
? sourceProps.mixBlendMode
|
||||
@@ -311,6 +320,7 @@ void BaseViewProps::setProp(
|
||||
switch (hash) {
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(opacity);
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(backgroundColor);
|
||||
RAW_SET_PROP_SWITCH_CASE(backgroundImage, "experimental_backgroundImage");
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowColor);
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOffset);
|
||||
RAW_SET_PROP_SWITCH_CASE_BASIC(shadowOpacity);
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <react/renderer/core/LayoutMetrics.h>
|
||||
#include <react/renderer/core/Props.h>
|
||||
#include <react/renderer/core/PropsParserContext.h>
|
||||
#include <react/renderer/graphics/BackgroundImage.h>
|
||||
#include <react/renderer/graphics/BlendMode.h>
|
||||
#include <react/renderer/graphics/BoxShadow.h>
|
||||
#include <react/renderer/graphics/Color.h>
|
||||
@@ -63,6 +64,9 @@ class BaseViewProps : public YogaStylableProps, public AccessibilityProps {
|
||||
// Filter
|
||||
std::vector<FilterFunction> filter{};
|
||||
|
||||
// Gradient
|
||||
std::vector<GradientValue> backgroundImage{};
|
||||
|
||||
// MixBlendMode
|
||||
BlendMode mixBlendMode;
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ void ViewShadowNode::initialize() noexcept {
|
||||
bool formsView = formsStackingContext ||
|
||||
isColorMeaningful(viewProps.backgroundColor) || hasBorder() ||
|
||||
!viewProps.testId.empty() || !viewProps.boxShadow.empty() ||
|
||||
!viewProps.backgroundImage.empty() ||
|
||||
HostPlatformViewTraitsInitializer::formsView(viewProps);
|
||||
|
||||
if (formsView) {
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#include <react/renderer/core/LayoutMetrics.h>
|
||||
#include <react/renderer/core/PropsParserContext.h>
|
||||
#include <react/renderer/core/RawProps.h>
|
||||
#include <react/renderer/core/graphicsConversions.h>
|
||||
#include <react/renderer/graphics/BackgroundImage.h>
|
||||
#include <react/renderer/graphics/BlendMode.h>
|
||||
#include <react/renderer/graphics/BoxShadow.h>
|
||||
#include <react/renderer/graphics/Filter.h>
|
||||
@@ -1091,6 +1093,97 @@ inline void fromRawValue(
|
||||
result = blendMode.value();
|
||||
}
|
||||
|
||||
inline void fromRawValue(
|
||||
const PropsParserContext& context,
|
||||
const RawValue& value,
|
||||
std::vector<GradientValue>& result) {
|
||||
react_native_expect(value.hasType<std::vector<RawValue>>());
|
||||
if (!value.hasType<std::vector<RawValue>>()) {
|
||||
result = {};
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<GradientValue> backgroundImage{};
|
||||
auto rawBackgroundImage = static_cast<std::vector<RawValue>>(value);
|
||||
for (const auto& rawGradientValue : rawBackgroundImage) {
|
||||
bool isMap =
|
||||
rawGradientValue.hasType<std::unordered_map<std::string, RawValue>>();
|
||||
react_native_expect(isMap);
|
||||
if (!isMap) {
|
||||
result = {};
|
||||
return;
|
||||
}
|
||||
|
||||
auto rawGradientValueMap =
|
||||
static_cast<std::unordered_map<std::string, RawValue>>(
|
||||
rawGradientValue);
|
||||
GradientValue gradientValue{};
|
||||
|
||||
auto typeIt = rawGradientValueMap.find("type");
|
||||
if (typeIt != rawGradientValueMap.end() &&
|
||||
typeIt->second.hasType<std::string>()) {
|
||||
gradientValue.type =
|
||||
gradientTypeFromString((std::string)(typeIt->second));
|
||||
}
|
||||
|
||||
auto startIt = rawGradientValueMap.find("start");
|
||||
if (startIt != rawGradientValueMap.end() &&
|
||||
startIt->second.hasType<std::unordered_map<std::string, RawValue>>()) {
|
||||
auto startPoints = static_cast<std::unordered_map<std::string, RawValue>>(
|
||||
startIt->second);
|
||||
auto xIt = startPoints.find("x");
|
||||
auto yIt = startPoints.find("y");
|
||||
if (xIt != startPoints.end() && yIt != startPoints.end() &&
|
||||
xIt->second.hasType<Float>() && yIt->second.hasType<Float>()) {
|
||||
gradientValue.startX = (Float)(xIt->second);
|
||||
gradientValue.startY = (Float)(yIt->second);
|
||||
}
|
||||
}
|
||||
|
||||
auto endIt = rawGradientValueMap.find("end");
|
||||
if (endIt != rawGradientValueMap.end() &&
|
||||
endIt->second.hasType<std::unordered_map<std::string, RawValue>>()) {
|
||||
auto endPoints =
|
||||
static_cast<std::unordered_map<std::string, RawValue>>(endIt->second);
|
||||
auto xIt = endPoints.find("x");
|
||||
auto yIt = endPoints.find("y");
|
||||
if (xIt != endPoints.end() && yIt != endPoints.end() &&
|
||||
xIt->second.hasType<Float>() && yIt->second.hasType<Float>()) {
|
||||
gradientValue.endX = (Float)(xIt->second);
|
||||
gradientValue.endY = (Float)(yIt->second);
|
||||
}
|
||||
}
|
||||
|
||||
auto colorStopsIt = rawGradientValueMap.find("colorStops");
|
||||
if (colorStopsIt != rawGradientValueMap.end() &&
|
||||
colorStopsIt->second.hasType<std::vector<RawValue>>()) {
|
||||
auto rawColorStops =
|
||||
static_cast<std::vector<RawValue>>(colorStopsIt->second);
|
||||
|
||||
for (const auto& stop : rawColorStops) {
|
||||
if (stop.hasType<std::unordered_map<std::string, RawValue>>()) {
|
||||
auto stopMap =
|
||||
static_cast<std::unordered_map<std::string, RawValue>>(stop);
|
||||
auto positionIt = stopMap.find("position");
|
||||
auto colorIt = stopMap.find("color");
|
||||
|
||||
if (positionIt != stopMap.end() && colorIt != stopMap.end() &&
|
||||
positionIt->second.hasType<Float>()) {
|
||||
ColorStop colorStop{};
|
||||
colorStop.position = (Float)(positionIt->second);
|
||||
fromRawValue(context, colorIt->second, colorStop.color);
|
||||
gradientValue.colorStops.push_back(colorStop);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
backgroundImage.push_back(gradientValue);
|
||||
}
|
||||
|
||||
result = backgroundImage;
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
inline std::string toString(const std::array<float, N> vec) {
|
||||
std::string s;
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <react/renderer/graphics/Color.h>
|
||||
#include <react/renderer/graphics/Float.h>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace facebook::react {
|
||||
|
||||
struct ColorStop {
|
||||
bool operator==(const ColorStop& other) const = default;
|
||||
SharedColor color;
|
||||
std::optional<Float> position;
|
||||
};
|
||||
|
||||
enum class GradientType {
|
||||
LinearGradient,
|
||||
};
|
||||
|
||||
struct GradientValue {
|
||||
bool operator==(const GradientValue& other) const = default;
|
||||
|
||||
GradientType type;
|
||||
Float startX;
|
||||
Float startY;
|
||||
Float endX;
|
||||
Float endY;
|
||||
std::vector<ColorStop> colorStops;
|
||||
};
|
||||
|
||||
inline GradientType gradientTypeFromString(const std::string& gradientType) {
|
||||
if (gradientType == "linearGradient") {
|
||||
return GradientType::LinearGradient;
|
||||
} else {
|
||||
throw std::invalid_argument(std::string(gradientType));
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace facebook::react
|
||||
@@ -33,6 +33,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
GradientValue,
|
||||
BlendMode,
|
||||
BoxShadowPrimitive,
|
||||
DimensionValue,
|
||||
@@ -155,5 +156,9 @@ declare module '.' {
|
||||
| undefined;
|
||||
experimental_filter?: ReadonlyArray<FilterFunction> | string | undefined;
|
||||
experimental_mixBlendMode?: BlendMode | undefined;
|
||||
experimental_backgroundImage?:
|
||||
| ReadonlyArray<GradientValue>
|
||||
| string
|
||||
| undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @format
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {ViewStyleProp} from 'react-native/Libraries/StyleSheet/StyleSheet';
|
||||
|
||||
import React from 'react';
|
||||
import {StyleSheet, Text, View} from 'react-native';
|
||||
|
||||
type Props = $ReadOnly<{
|
||||
style: ViewStyleProp,
|
||||
testID?: string,
|
||||
}>;
|
||||
|
||||
function GradientBox(props: Props): React.Node {
|
||||
return (
|
||||
<View style={[styles.box, props.style]} testID={props.testID}>
|
||||
<Text style={styles.text}>Linear Gradient</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
box: {
|
||||
width: 200,
|
||||
height: 100,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginVertical: 10,
|
||||
},
|
||||
text: {
|
||||
color: 'white',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
exports.title = 'Linear Gradient';
|
||||
exports.category = 'UI';
|
||||
exports.description = 'Examples of linear gradients applied to views.';
|
||||
exports.examples = [
|
||||
{
|
||||
title: 'Basic Linear Gradient',
|
||||
description: 'Linear gradient from top to bottom',
|
||||
render(): React.Node {
|
||||
return (
|
||||
<GradientBox
|
||||
style={{
|
||||
experimental_backgroundImage: 'linear-gradient(#e66465, #9198e5);',
|
||||
}}
|
||||
testID="linear-gradient-basic"
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Diagonal Gradient',
|
||||
description: 'Linear gradient from top-left to bottom-right',
|
||||
render(): React.Node {
|
||||
return (
|
||||
<GradientBox
|
||||
testID="linear-gradient-diagonal"
|
||||
style={{
|
||||
experimental_backgroundImage:
|
||||
'linear-gradient(to bottom right, yellow, green)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Gradient with angle',
|
||||
description: 'Linear gradient with angle',
|
||||
render(): React.Node {
|
||||
return (
|
||||
<GradientBox
|
||||
testID="linear-gradient-angle"
|
||||
style={{
|
||||
experimental_backgroundImage:
|
||||
'linear-gradient(135deg, gray, brown)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Multiple Color Stops',
|
||||
render(): React.Node {
|
||||
return (
|
||||
<GradientBox
|
||||
testID="linear-gradient-color-stops"
|
||||
style={{
|
||||
experimental_backgroundImage:
|
||||
'linear-gradient(to right, red 0%, yellow 30%, green 60%, blue 100%)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Linear gradient with object style syntax',
|
||||
render(): React.Node {
|
||||
return (
|
||||
<GradientBox
|
||||
testID="linear-gradient-object-style-syntax"
|
||||
style={{
|
||||
experimental_backgroundImage: [
|
||||
{
|
||||
type: 'linearGradient',
|
||||
direction: 'to bottom',
|
||||
colorStops: [
|
||||
{color: 'purple', position: '0%'},
|
||||
{color: 'orange', position: '100%'},
|
||||
],
|
||||
},
|
||||
],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -311,6 +311,11 @@ const APIs: Array<RNTesterModuleInfo> = ([
|
||||
category: 'UI',
|
||||
module: require('../examples/Filter/FilterExample'),
|
||||
},
|
||||
{
|
||||
key: 'LinearGradient',
|
||||
category: 'UI',
|
||||
module: require('../examples/LinearGradient/LinearGradientExample'),
|
||||
},
|
||||
{
|
||||
key: 'MixBlendModeExample',
|
||||
category: 'UI',
|
||||
|
||||
Reference in New Issue
Block a user