mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
JS plumbing to get filters into native (#44458)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/44458 This is the JS plumbing to get it so that views can now use filters. The typing looks like `filter: [{brightness: 1.5}, {hueRotate: '90deg'}]` which is different than web which would look like `filter: brightness(1.5) hue-rotate(90deg)`. I feel like the web version is overly complicated and not very *react native-y*. Transform uses the array based approach (albeit they also accept a string). Open to changing this but really feel like the web format is silly and bad since it would just involve parsing some arbitrary string. The diff includes: * Style sheet changes so typing is valid * Process function to turn filter format into {name: string, amount: string} * Test for process function * View config changes on Android, iOS and ReactNativeStyleAttributes Changelog: [Internal] Reviewed By: NickGerleman Differential Revision: D56845572 fbshipit-source-id: 5029b5adac29bb863c89f6c699d5693c58cad711
This commit is contained in:
committed by
Facebook GitHub Bot
parent
c27f2ab747
commit
0dceac9f02
@@ -12,6 +12,7 @@ import type {AnyAttributeType} from '../../Renderer/shims/ReactNativeTypes';
|
||||
|
||||
import processAspectRatio from '../../StyleSheet/processAspectRatio';
|
||||
import processColor from '../../StyleSheet/processColor';
|
||||
import processFilter from '../../StyleSheet/processFilter';
|
||||
import processFontVariant from '../../StyleSheet/processFontVariant';
|
||||
import processTransform from '../../StyleSheet/processTransform';
|
||||
import processTransformOrigin from '../../StyleSheet/processTransformOrigin';
|
||||
@@ -114,6 +115,11 @@ const ReactNativeStyleAttributes: {[string]: AnyAttributeType, ...} = {
|
||||
transform: {process: processTransform},
|
||||
transformOrigin: {process: processTransformOrigin},
|
||||
|
||||
/**
|
||||
* Filter
|
||||
*/
|
||||
experimental_filter: {process: processFilter},
|
||||
|
||||
/**
|
||||
* View
|
||||
*/
|
||||
|
||||
@@ -166,6 +166,9 @@ const validAttributesForNonEventProps = {
|
||||
backgroundColor: {process: require('../StyleSheet/processColor').default},
|
||||
transform: true,
|
||||
transformOrigin: true,
|
||||
experimental_filter: {
|
||||
process: require('../StyleSheet/processFilter').default,
|
||||
},
|
||||
opacity: true,
|
||||
elevation: true,
|
||||
shadowColor: {process: require('../StyleSheet/processColor').default},
|
||||
|
||||
@@ -220,6 +220,9 @@ const validAttributesForNonEventProps = {
|
||||
hitSlop: {diff: require('../Utilities/differ/insetsDiffer')},
|
||||
collapsable: true,
|
||||
collapsableChildren: true,
|
||||
experimental_filter: {
|
||||
process: require('../StyleSheet/processFilter').default,
|
||||
},
|
||||
|
||||
borderTopWidth: true,
|
||||
borderTopColor: {process: require('../StyleSheet/processColor').default},
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
'use strict';
|
||||
|
||||
import type AnimatedNode from '../Animated/nodes/AnimatedNode';
|
||||
import type {FilterPrimitive} from '../StyleSheet/processFilter';
|
||||
import type {
|
||||
____DangerouslyImpreciseStyle_InternalOverrides,
|
||||
____ImageStyle_InternalOverrides,
|
||||
@@ -690,10 +691,15 @@ export type ____ShadowStyle_Internal = $ReadOnly<{
|
||||
...____ShadowStyle_InternalOverrides,
|
||||
}>;
|
||||
|
||||
type ____FilterStyle_Internal = $ReadOnly<{
|
||||
experimental_filter?: $ReadOnlyArray<FilterPrimitive>,
|
||||
}>;
|
||||
|
||||
export type ____ViewStyle_InternalCore = $ReadOnly<{
|
||||
...$Exact<____LayoutStyle_Internal>,
|
||||
...$Exact<____ShadowStyle_Internal>,
|
||||
...$Exact<____TransformStyle_Internal>,
|
||||
...____FilterStyle_Internal,
|
||||
backfaceVisibility?: 'visible' | 'hidden',
|
||||
backgroundColor?: ____ColorValue_Internal,
|
||||
borderColor?: ____ColorValue_Internal,
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 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
|
||||
* @flow strict-local
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {FilterPrimitive} from '../processFilter';
|
||||
|
||||
const processFilter = require('../processFilter').default;
|
||||
|
||||
// js1 test processFilter
|
||||
describe('processFilter', () => {
|
||||
testStandardFilter('brightness');
|
||||
testStandardFilter('opacity');
|
||||
testStandardFilter('contrast');
|
||||
testStandardFilter('saturate');
|
||||
testStandardFilter('grayscale');
|
||||
testStandardFilter('sepia');
|
||||
testStandardFilter('invert');
|
||||
|
||||
testNumericFilter('blur', 5, [
|
||||
{
|
||||
blur: 5,
|
||||
},
|
||||
]);
|
||||
testNumericFilter('blur', -5, []);
|
||||
testUnitFilter('blur', 5, '%', []);
|
||||
testUnitFilter('blur', 5, 'px', [
|
||||
{
|
||||
blur: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
testNumericFilter('hueRotate', 0, [{hueRotate: 0}]);
|
||||
testUnitFilter('hueRotate', 90, 'deg', [{hueRotate: 90}]);
|
||||
testUnitFilter('hueRotate', 1.5708, 'rad', [
|
||||
{hueRotate: (180 * 1.5708) / Math.PI},
|
||||
]);
|
||||
testUnitFilter('hueRotate', -90, 'deg', [{hueRotate: -90}]);
|
||||
testUnitFilter('hueRotate', 1.5, 'grad', []);
|
||||
testNumericFilter('hueRotate', 90, []);
|
||||
testUnitFilter('hueRotate', 50, '%', []);
|
||||
|
||||
it('multiple filters', () => {
|
||||
expect(
|
||||
processFilter([
|
||||
{brightness: 0.5},
|
||||
{opacity: 0.5},
|
||||
{blur: 5},
|
||||
{hueRotate: '90deg'},
|
||||
]),
|
||||
).toEqual([{brightness: 0.5}, {opacity: 0.5}, {blur: 5}, {hueRotate: 90}]);
|
||||
});
|
||||
it('multiple filters one invalid', () => {
|
||||
expect(
|
||||
processFilter([
|
||||
{brightness: 0.5},
|
||||
{opacity: 0.5},
|
||||
{blur: 5},
|
||||
{hueRotate: '90foo'},
|
||||
]),
|
||||
).toEqual([]);
|
||||
});
|
||||
it('multiple same filters', () => {
|
||||
expect(
|
||||
processFilter([
|
||||
{brightness: 0.5},
|
||||
{brightness: 0.5},
|
||||
{brightness: 0.5},
|
||||
{brightness: 0.5},
|
||||
]),
|
||||
).toEqual([
|
||||
{brightness: 0.5},
|
||||
{brightness: 0.5},
|
||||
{brightness: 0.5},
|
||||
{brightness: 0.5},
|
||||
]);
|
||||
});
|
||||
it('empty', () => {
|
||||
expect(processFilter([])).toEqual([]);
|
||||
});
|
||||
it('Non filter', () => {
|
||||
// $FlowExpectedError[incompatible-call]
|
||||
expect(processFilter([{foo: 5}])).toEqual([]);
|
||||
});
|
||||
it('Invalid amount type', () => {
|
||||
// $FlowExpectedError[incompatible-call]
|
||||
expect(processFilter([{brightness: {}}])).toEqual([]);
|
||||
});
|
||||
it('string multiple filters', () => {
|
||||
expect(
|
||||
processFilter('brightness(0.5) opacity(0.5) blur(5) hueRotate(90deg)'),
|
||||
).toEqual([{brightness: 0.5}, {opacity: 0.5}, {blur: 5}, {hueRotate: 90}]);
|
||||
});
|
||||
it('string multiple filters one invalid', () => {
|
||||
expect(
|
||||
processFilter('brightness(0.5) opacity(0.5) blur(5) hueRotate(90foo)'),
|
||||
).toEqual([]);
|
||||
});
|
||||
it('string multiple same filters', () => {
|
||||
expect(
|
||||
processFilter(
|
||||
'brightness(0.5) brightness(0.5) brightness(0.5) brightness(0.5)',
|
||||
),
|
||||
).toEqual([
|
||||
{brightness: 0.5},
|
||||
{brightness: 0.5},
|
||||
{brightness: 0.5},
|
||||
{brightness: 0.5},
|
||||
]);
|
||||
});
|
||||
it('string empty', () => {
|
||||
expect(processFilter('')).toEqual([]);
|
||||
});
|
||||
it('string non filter', () => {
|
||||
// $FlowExpectedError[incompatible-call]
|
||||
expect(processFilter('foo: 5')).toEqual([]);
|
||||
});
|
||||
it('string invalid amount type', () => {
|
||||
// $FlowExpectedError[incompatible-call]
|
||||
expect(processFilter('brightness: {}')).toEqual([]);
|
||||
});
|
||||
it('string brightness(.5)', () => {
|
||||
// $FlowExpectedError[incompatible-call]
|
||||
expect(processFilter('brightness(.5)')).toEqual([{brightness: 0.5}]);
|
||||
});
|
||||
});
|
||||
|
||||
function testStandardFilter(filter: string): void {
|
||||
const value = 0.5;
|
||||
const expected = createFilterPrimitive(filter, value);
|
||||
const percentExpected = createFilterPrimitive(filter, value / 100);
|
||||
|
||||
testNumericFilter(filter, value, [expected]);
|
||||
testNumericFilter(filter, -value, []);
|
||||
testUnitFilter(filter, value, 'px', [expected]);
|
||||
testUnitFilter(filter, value, '%', [percentExpected]);
|
||||
}
|
||||
|
||||
function testNumericFilter(
|
||||
filter: string,
|
||||
value: number,
|
||||
expected: Array<FilterPrimitive>,
|
||||
): void {
|
||||
const filterObject = createFilterPrimitive(filter, value);
|
||||
const filterString = filter + '(' + value.toString() + ')';
|
||||
|
||||
it(filterString, () => {
|
||||
expect(processFilter([filterObject])).toEqual(expected);
|
||||
});
|
||||
it('string ' + filterString, () => {
|
||||
expect(processFilter(filterString)).toEqual(expected);
|
||||
});
|
||||
}
|
||||
|
||||
function testUnitFilter(
|
||||
filter: string,
|
||||
value: number,
|
||||
unit: string,
|
||||
expected: Array<FilterPrimitive>,
|
||||
): void {
|
||||
const unitAmount = value + unit;
|
||||
const filterObject = createFilterPrimitive(filter, unitAmount);
|
||||
const filterString = filter + '(' + unitAmount + ')';
|
||||
|
||||
it(filterString, () => {
|
||||
expect(processFilter([filterObject])).toEqual(expected);
|
||||
});
|
||||
it('string ' + filterString, () => {
|
||||
expect(processFilter(filterString)).toEqual(expected);
|
||||
});
|
||||
}
|
||||
|
||||
function createFilterPrimitive(
|
||||
filter: string,
|
||||
value: number | string,
|
||||
): FilterPrimitive {
|
||||
switch (filter) {
|
||||
case 'brightness':
|
||||
return {brightness: value};
|
||||
case 'blur':
|
||||
return {blur: value};
|
||||
case 'contrast':
|
||||
return {contrast: value};
|
||||
case 'grayscale':
|
||||
return {grayscale: value};
|
||||
case 'hueRotate':
|
||||
return {hueRotate: value};
|
||||
case 'invert':
|
||||
return {invert: value};
|
||||
case 'opacity':
|
||||
return {opacity: value};
|
||||
case 'saturate':
|
||||
return {saturate: value};
|
||||
case 'sepia':
|
||||
return {sepia: value};
|
||||
default:
|
||||
throw new Error('Invalid filter: ' + filter);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
export type FilterPrimitive =
|
||||
| {brightness: number | string}
|
||||
| {blur: number | string}
|
||||
| {contrast: number | string}
|
||||
| {grayscale: number | string}
|
||||
| {hueRotate: number | string}
|
||||
| {invert: number | string}
|
||||
| {opacity: number | string}
|
||||
| {saturate: number | string}
|
||||
| {sepia: number | string};
|
||||
|
||||
export default function processFilter(
|
||||
filter: $ReadOnlyArray<FilterPrimitive> | string,
|
||||
): $ReadOnlyArray<FilterPrimitive> {
|
||||
let result: Array<FilterPrimitive> = [];
|
||||
if (typeof filter === 'string') {
|
||||
// matches on functions with args like "brightness(1.5)"
|
||||
const regex = new RegExp(/(\w+)\(([^)]+)\)/g);
|
||||
let matches;
|
||||
|
||||
while ((matches = regex.exec(filter))) {
|
||||
const amount = _getFilterAmount(matches[1], matches[2]);
|
||||
|
||||
if (amount != null) {
|
||||
const filterPrimitive = {};
|
||||
// $FlowFixMe The key will be the correct one but flow can't see that.
|
||||
filterPrimitive[matches[1]] = amount;
|
||||
// $FlowFixMe The key will be the correct one but flow can't see that.
|
||||
result.push(filterPrimitive);
|
||||
} else {
|
||||
// If any primitive is invalid then apply none of the filters. This is how
|
||||
// web works and makes it clear that something is wrong becuase no
|
||||
// graphical effects are happening.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const filterPrimitive of filter) {
|
||||
const [filterName, filterValue] = Object.entries(filterPrimitive)[0];
|
||||
const amount = _getFilterAmount(filterName, filterValue);
|
||||
|
||||
if (amount != null) {
|
||||
const resultObject = {};
|
||||
// $FlowFixMe
|
||||
resultObject[filterName] = amount;
|
||||
// $FlowFixMe
|
||||
result.push(resultObject);
|
||||
} else {
|
||||
// If any primitive is invalid then apply none of the filters. This is how
|
||||
// web works and makes it clear that something is wrong becuase no
|
||||
// graphical effects are happening.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function _getFilterAmount(filterName: string, filterArgs: mixed): ?number {
|
||||
let filterArgAsNumber: number;
|
||||
let unit: string;
|
||||
if (typeof filterArgs === 'string') {
|
||||
// matches on args with units like "1.5 5% -80deg"
|
||||
const argsWithUnitsRegex = new RegExp(/([+-]?\d*(\.\d+)?)([a-zA-Z%]+)?/g);
|
||||
const match = argsWithUnitsRegex.exec(filterArgs);
|
||||
|
||||
if (!match || isNaN(Number(match[1]))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
filterArgAsNumber = Number(match[1]);
|
||||
unit = match[3];
|
||||
} else if (typeof filterArgs === 'number') {
|
||||
filterArgAsNumber = filterArgs;
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
switch (filterName) {
|
||||
// Hue rotate takes some angle that can have a unit and can be
|
||||
// negative. Additionally, 0 with no unit is allowed.
|
||||
case 'hueRotate':
|
||||
if (filterArgAsNumber === 0) {
|
||||
return 0;
|
||||
}
|
||||
if (unit !== 'deg' && unit !== 'rad') {
|
||||
return undefined;
|
||||
}
|
||||
return unit === 'rad'
|
||||
? (180 * filterArgAsNumber) / Math.PI
|
||||
: filterArgAsNumber;
|
||||
// blur takes any positive CSS length that is not a percent. In RN
|
||||
// we currently only have DIPs, so we are not parsing units here.
|
||||
case 'blur':
|
||||
if ((unit && unit !== 'px') || filterArgAsNumber < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return filterArgAsNumber;
|
||||
// All other filters except take a non negative number or percentage. There
|
||||
// are no units associated with this value and percentage numbers map 1-to-1
|
||||
// to a non-percentage number (e.g. 50% == 0.5).
|
||||
case 'brightness':
|
||||
case 'contrast':
|
||||
case 'grayscale':
|
||||
case 'invert':
|
||||
case 'opacity':
|
||||
case 'saturate':
|
||||
case 'sepia':
|
||||
if ((unit && unit !== '%' && unit !== 'px') || filterArgAsNumber < 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (unit === '%') {
|
||||
filterArgAsNumber /= 100;
|
||||
}
|
||||
return filterArgAsNumber;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -7639,10 +7639,14 @@ export type ____ShadowStyle_Internal = $ReadOnly<{
|
||||
...____ShadowStyle_InternalCore,
|
||||
...____ShadowStyle_InternalOverrides,
|
||||
}>;
|
||||
type ____FilterStyle_Internal = $ReadOnly<{
|
||||
experimental_filter?: $ReadOnlyArray<FilterPrimitive>,
|
||||
}>;
|
||||
export type ____ViewStyle_InternalCore = $ReadOnly<{
|
||||
...$Exact<____LayoutStyle_Internal>,
|
||||
...$Exact<____ShadowStyle_Internal>,
|
||||
...$Exact<____TransformStyle_Internal>,
|
||||
...____FilterStyle_Internal,
|
||||
backfaceVisibility?: \\"visible\\" | \\"hidden\\",
|
||||
backgroundColor?: ____ColorValue_Internal,
|
||||
borderColor?: ____ColorValue_Internal,
|
||||
@@ -7939,6 +7943,23 @@ declare module.exports: processColorArray;
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`public API should not change unintentionally Libraries/StyleSheet/processFilter.js 1`] = `
|
||||
"export type FilterPrimitive =
|
||||
| { brightness: number | string }
|
||||
| { blur: number | string }
|
||||
| { contrast: number | string }
|
||||
| { grayscale: number | string }
|
||||
| { hueRotate: number | string }
|
||||
| { invert: number | string }
|
||||
| { opacity: number | string }
|
||||
| { saturate: number | string }
|
||||
| { sepia: number | string };
|
||||
declare export default function processFilter(
|
||||
filter: $ReadOnlyArray<FilterPrimitive> | string
|
||||
): $ReadOnlyArray<FilterPrimitive>;
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`public API should not change unintentionally Libraries/StyleSheet/processFontVariant.js 1`] = `
|
||||
"declare function processFontVariant(
|
||||
fontVariant: ____FontVariantArray_Internal | string
|
||||
|
||||
Reference in New Issue
Block a user