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

Introduce gradient mask style prop. #13479

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions Examples/UIExplorer/js/ViewExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,5 +249,22 @@ exports.examples = [
render: function() {
return <ZIndexExample />;
},
}, {
title: 'Mask',
render: function() {
var fadeMask = {
colors: ['transparent', '#000f', '#000f', 'transparent'],
locations: [0, 0.1, 0.6, 1.0],
sideOrCorner: 'to bottom',
};

return (
<View style={{height: 200, mask: fadeMask}}>
<View style={{height: 200, justifyContent: 'center', backgroundColor: 'red'}}>
<Text style={{padding: 5, color: 'white'}}>This mask creates a fade effect along the top and bottom of the view</Text>
</View>
</View>
);
}
},
];
37 changes: 37 additions & 0 deletions Libraries/Components/View/MaskPropTypesIOS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule MaskPropTypesIOS
* @flow
*/
'use strict';

var ColorPropType = require('ColorPropType');
var ReactPropTypes = require('React').PropTypes;

var MaskPropTypesIOS = {
/**
* Sets the view's mask to a gradient created with the given colors and locations.
* A gradient can be specified using an object with the following properties:
*
* - `colors` - An array of colors with alpha channel using the standard color syntax. Only the alpha channel is used for the mask. (required)
* - `locations` - An array of percentages that correspond to each color in the `colors` array.
* - `sideOrCorner` - The direction of the gradient. It consists of two keywords: one indicates the horizontal side, "left" or "right", and the other the vertical side, "top" or "bottom". The order is not relevant and each is optional. If omitted, it defaults to "to bottom".
*
* More information on the syntax of `sideOrCorner` can be found in the documentation for [CSS `linear-gradient`](https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient).
*
* @platform ios
*/
mask: ReactPropTypes.shape({
colors: ReactPropTypes.arrayOf(ColorPropType).isRequired,
locations: ReactPropTypes.arrayOf(ReactPropTypes.number),
sideOrCorner: ReactPropTypes.string,
}),
};

module.exports = MaskPropTypesIOS;
2 changes: 2 additions & 0 deletions Libraries/Components/View/ReactNativeStyleAttributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var ViewStylePropTypes = require('ViewStylePropTypes');

var keyMirror = require('fbjs/lib/keyMirror');
var processColor = require('processColor');
var processMask = require('processMask');
var processTransform = require('processTransform');
var sizesDiffer = require('sizesDiffer');

Expand All @@ -27,6 +28,7 @@ var ReactNativeStyleAttributes = {
...keyMirror(ImageStylePropTypes),
};

ReactNativeStyleAttributes.mask = { process: processMask };
ReactNativeStyleAttributes.transform = { process: processTransform };
ReactNativeStyleAttributes.shadowOffset = { diff: sizesDiffer };

Expand Down
2 changes: 2 additions & 0 deletions Libraries/Components/View/ViewStylePropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
var LayoutPropTypes = require('LayoutPropTypes');
var ReactPropTypes = require('React').PropTypes;
var ColorPropType = require('ColorPropType');
var MaskPropTypesIOS = require('MaskPropTypesIOS');
var ShadowPropTypesIOS = require('ShadowPropTypesIOS');
var TransformPropTypes = require('TransformPropTypes');

Expand All @@ -22,6 +23,7 @@ var TransformPropTypes = require('TransformPropTypes');
*/
var ViewStylePropTypes = {
...LayoutPropTypes,
...MaskPropTypesIOS,
...ShadowPropTypesIOS,
...TransformPropTypes,
backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`processMask validation should require colors 1`] = `"The mask must have at least one color. Passed properties: {\\"colors\\":[]}"`;

exports[`processMask validation should validate colors and locations 1`] = `"The mask must have one location per color. Passed properties: {\\"colors\\":[\\"#0000\\",\\"white\\"],\\"locations\\":[0,0.5,1]}"`;

exports[`processMask validation should validate sideOrCorner 1`] = `"Mask \\"sideOrCorner\\" must start with \\"to \\": \\"bottom\\""`;

exports[`processMask validation should validate sideOrCorner values 1`] = `"Mask \\"sideOrCorner\\" must contain only \\"left\\", \\"right\\", \\"top\\", \\"bottom\\": \\"to left botttom\\""`;
82 changes: 82 additions & 0 deletions Libraries/StyleSheet/__tests__/processMask-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';

jest.disableAutomock();

const processMask = require('processMask');

describe('processMask', () => {

describe('color processing', () => {
it('should support standard color syntax', () => {
var maskJson = processMask({
colors: ['#0000', 'white', 'transparent', 'rgba(255,255,255,0.5)']
});
expect(maskJson.colors).toEqual([0, 0xffffffff, 0, 0x80ffffff]);
});
});

describe('sideOrCorner processing', () => {
it('should convert one side', () => {
var maskJson = processMask({
colors: ['#0000', 'white'],
sideOrCorner: 'to right'
});
expect(maskJson.start.x).toEqual(0);
expect(maskJson.start.y).toEqual(0.5);
expect(maskJson.end.x).toEqual(1.0);
expect(maskJson.end.y).toEqual(0.5);
});

it('should convert two sides to a corner', () => {
var maskJson = processMask({
colors: ['#0000', 'white'],
sideOrCorner: 'to right bottom'
});
expect(maskJson.start.x).toEqual(0);
expect(maskJson.start.y).toEqual(0);
expect(maskJson.end.x).toEqual(1.0);
expect(maskJson.end.y).toEqual(1.0);
});
});

describe('validation', () => {
it('should require colors', () => {
var mask = { colors: [] };
expect(() => processMask(mask)).toThrowErrorMatchingSnapshot();
});

it('should validate colors and locations', () => {
var mask = {
colors: ['#0000', 'white'],
locations: [0, 0.5, 1.0]
};
expect(() => processMask(mask)).toThrowErrorMatchingSnapshot();
});

it('should validate sideOrCorner', () => {
var mask = {
colors: ['#0000', 'white'],
locations: [0, 0.5],
sideOrCorner: 'bottom'
};
expect(() => processMask(mask)).toThrowErrorMatchingSnapshot();
});

it('should validate sideOrCorner values', () => {
var mask = {
colors: ['#0000', 'white'],
locations: [0, 0.5],
sideOrCorner: 'to left botttom'
};
expect(() => processMask(mask)).toThrowErrorMatchingSnapshot();
});
});
});
110 changes: 110 additions & 0 deletions Libraries/StyleSheet/processMask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule processMask
* @flow
*/
'use strict';

var processColor = require('processColor');

var invariant = require('fbjs/lib/invariant');
var stringifySafe = require('stringifySafe');

/**
* Generate a mask object from the given style info
*/
function processMask(maskStyle: Object): Object {
if (__DEV__) {
validateMask(maskStyle);
}

var position = convertSideOrCorner(maskStyle.sideOrCorner);

return {
colors: maskStyle.colors.map(processColor),
locations: maskStyle.locations,
start: position.start,
end: position.end
};
}

function convertSideOrCorner(sideOrCorner: string): Object {
// Default position is "to bottom"
var position = {
start: { x: 0.5, y: 0 },
end: { x: 0.5, y: 1.0 }
};

if (!sideOrCorner) {
return position;
}

var values = sideOrCorner.split(/\s+/);

if (values[1] === 'left') {
position.start = { x: 1.0, y: 0.5 };
position.end = { x: 0, y: 0.5 };
} else if (values[1] === 'right') {
position.start = { x: 0, y: 0.5 };
position.end = { x: 1.0, y: 0.5 };
} else if (values[1] === 'top') {
position.start = { x: 0.5, y: 1.0 };
position.end = { x: 0.5, y: 0 };
}

if (values[2] === 'left') {
position.start.x = 1.0;
position.end.x = 0;
} else if (values[2] === 'right') {
position.start.x = 0;
position.end.x = 1.0;
} else if (values[2] === 'top') {
position.start.y = 1.0;
position.end.y = 0;
} else if (values[2] === 'bottom') {
position.start.y = 0;
position.end.y = 1.0;
}
return position;
}

function validateMask(maskStyle: Object): void {
invariant(
maskStyle.colors && maskStyle.colors.length,
'The mask must have at least one color. Passed properties: %s',
stringifySafe(maskStyle)
);
if (maskStyle.locations) {
invariant(
maskStyle.locations.length === maskStyle.colors.length,
'The mask must have one location per color. Passed properties: %s',
stringifySafe(maskStyle)
);
}
if (maskStyle.sideOrCorner) {
validateSideOrCorner(maskStyle.sideOrCorner);
}
}

var legalSideOrCorner = ['left', 'right', 'top', 'bottom'];

function validateSideOrCorner(sideOrCorner: string): void {
invariant(
sideOrCorner.indexOf('to ') === 0,
'Mask "sideOrCorner" must start with "to ": %s',
stringifySafe(sideOrCorner)
);
invariant(
sideOrCorner.split(/\s+/).slice(1).every(val => legalSideOrCorner.includes(val)),
'Mask "sideOrCorner" must contain only "left", "right", "top", "bottom": %s',
stringifySafe(sideOrCorner)
);
}

module.exports = processMask;
2 changes: 2 additions & 0 deletions React/Base/RCTConvert.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ typedef BOOL css_backface_visibility_t;
+ (RCTBorderStyle)RCTBorderStyle:(id)json;
+ (RCTTextDecorationLineType)RCTTextDecorationLineType:(id)json;

+ (CAGradientLayer *)CAGradientLayer:(id)json;

@end

@interface RCTConvert (Deprecated)
Expand Down
23 changes: 23 additions & 0 deletions React/Base/RCTConvert.m
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,29 @@ + (NSPropertyList)NSPropertyList:(id)json
@"keyboard": @(RCTAnimationTypeKeyboard),
}), RCTAnimationTypeEaseInEaseOut, integerValue)

+ (CAGradientLayer *)CAGradientLayer:(id)json
{
if (!json || ![json isKindOfClass:[NSDictionary class]]) {
return nil;
}

CAGradientLayer *maskLayer = [CAGradientLayer layer];

maskLayer.colors = [RCTConvert CGColorArray:json[@"colors"]];
maskLayer.locations = [RCTConvert NSNumberArray:json[@"locations"]];

if (json[@"start"]) {
maskLayer.startPoint = [RCTConvert CGPoint:json[@"start"]];
}
if (json[@"end"]) {
maskLayer.endPoint = [RCTConvert CGPoint:json[@"end"]];
}

maskLayer.anchorPoint = CGPointZero;

return maskLayer;
}

@end

@interface RCTImageSource (Packager)
Expand Down
5 changes: 5 additions & 0 deletions React/Views/RCTView.h
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,9 @@
*/
@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;

/**
* Optional layer set by a "mask" style prop.
*/
@property (nonatomic, strong) CALayer *maskLayer;

@end
14 changes: 12 additions & 2 deletions React/Views/RCTView.m
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ @implementation RCTView
}

@synthesize reactZIndex = _reactZIndex;
@synthesize maskLayer = _maskLayer;

- (instancetype)initWithFrame:(CGRect)frame
{
Expand Down Expand Up @@ -479,6 +480,7 @@ - (void)displayLayer:(CALayer *)layer
}

RCTUpdateShadowPathForView(self);
RCTUpdateMaskBoundsForView(self);

const RCTCornerRadii cornerRadii = [self cornerRadii];
const UIEdgeInsets borderInsets = [self bordersAsInsets];
Expand Down Expand Up @@ -508,7 +510,7 @@ - (void)displayLayer:(CALayer *)layer
layer.backgroundColor = _backgroundColor.CGColor;
layer.contents = nil;
layer.needsDisplayOnBoundsChange = NO;
layer.mask = nil;
layer.mask = _maskLayer;
return;
}

Expand Down Expand Up @@ -601,9 +603,17 @@ static void RCTUpdateShadowPathForView(RCTView *view)
}
}

static void RCTUpdateMaskBoundsForView(RCTView *view)
{
// A gradient mask needs its bounds to be in sync with the view's layer
if (view.maskLayer) {
view.maskLayer.bounds = view.layer.bounds;
}
}

- (void)updateClippingForLayer:(CALayer *)layer
{
CALayer *mask = nil;
CALayer *mask = _maskLayer;
CGFloat cornerRadius = 0;

if (self.clipsToBounds) {
Expand Down
Loading