diff --git a/Examples/UIExplorer/BoxShadowExample.js b/Examples/UIExplorer/BoxShadowExample.js new file mode 100644 index 000000000..12568c1b6 --- /dev/null +++ b/Examples/UIExplorer/BoxShadowExample.js @@ -0,0 +1,85 @@ +/** + * The examples provided by Facebook are for non-commercial testing and + * evaluation purposes only. + * + * Facebook reserves all rights not expressly granted. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL + * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +'use strict'; + +var React = require('react-native'); +var { + Image, + StyleSheet, + View +} = React; + +var styles = StyleSheet.create({ + box: { + width: 100, + height: 100, + borderWidth: 2, + }, + shadow1: { + shadowOpacity: 0.5, + shadowRadius: 3, + shadowOffset: {width: 2, height: 2}, + }, + shadow2: { + shadowOpacity: 1.0, + shadowColor: 'red', + shadowRadius: 0, + shadowOffset: {width: 3, height: 3}, + }, +}); + +exports.title = 'Box Shadow'; +exports.description = 'Demonstrates some of the shadow styles available to Views.'; +exports.examples = [ + { + title: 'Basic shadow', + description: 'shadowOpacity: 0.5, shadowOffset: {2, 2}', + render() { + return ; + } + }, + { + title: 'Colored shadow', + description: 'shadowColor: \'red\', shadowRadius: 0', + render() { + return ; + } + }, + { + title: 'Shaped shadow', + description: 'borderRadius: 50', + render() { + return ; + } + }, + { + title: 'Image shadow', + description: 'Image shadows are derived exactly from the pixels.', + render() { + return ; + } + }, + { + title: 'Child shadow', + description: 'For views without an opaque background color, shadow will be derived from the subviews.', + render() { + return + + ; + } + }, +]; diff --git a/Examples/UIExplorer/UIExplorerList.ios.js b/Examples/UIExplorer/UIExplorerList.ios.js index 0c1e9e2e7..92f0f0bad 100644 --- a/Examples/UIExplorer/UIExplorerList.ios.js +++ b/Examples/UIExplorer/UIExplorerList.ios.js @@ -66,6 +66,7 @@ var APIS = [ require('./AppStateIOSExample'), require('./AsyncStorageExample'), require('./BorderExample'), + require('./BoxShadowExample'), require('./CameraRollExample'), require('./ClipboardExample'), require('./GeolocationExample'), diff --git a/Libraries/Components/View/ShadowPropTypesIOS.js b/Libraries/Components/View/ShadowPropTypesIOS.js new file mode 100644 index 000000000..c770ca9ed --- /dev/null +++ b/Libraries/Components/View/ShadowPropTypesIOS.js @@ -0,0 +1,42 @@ +/** + * 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 ShadowPropTypesIOS + * @flow + */ +'use strict'; + +var ColorPropType = require('ColorPropType'); +var ReactPropTypes = require('ReactPropTypes'); + +var ShadowPropTypesIOS = { + /** + * Sets the drop shadow color + * @platform ios + */ + shadowColor: ColorPropType, + /** + * Sets the drop shadow offset + * @platform ios + */ + shadowOffset: ReactPropTypes.shape( + {width: ReactPropTypes.number, height: ReactPropTypes.number} + ), + /** + * Sets the drop shadow opacity (multiplied by the color's alpha component) + * @platform ios + */ + shadowOpacity: ReactPropTypes.number, + /** + * Sets the drop shadow blur radius + * @platform ios + */ + shadowRadius: ReactPropTypes.number, +}; + +module.exports = ShadowPropTypesIOS; diff --git a/Libraries/Components/View/ViewStylePropTypes.js b/Libraries/Components/View/ViewStylePropTypes.js index cb193be1e..619df34c8 100644 --- a/Libraries/Components/View/ViewStylePropTypes.js +++ b/Libraries/Components/View/ViewStylePropTypes.js @@ -14,6 +14,7 @@ var LayoutPropTypes = require('LayoutPropTypes'); var ReactPropTypes = require('ReactPropTypes'); var ColorPropType = require('ColorPropType'); +var ShadowPropTypesIOS = require('ShadowPropTypesIOS'); var TransformPropTypes = require('TransformPropTypes'); /** @@ -21,6 +22,7 @@ var TransformPropTypes = require('TransformPropTypes'); */ var ViewStylePropTypes = { ...LayoutPropTypes, + ...ShadowPropTypesIOS, ...TransformPropTypes, backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']), backgroundColor: ColorPropType, @@ -42,12 +44,6 @@ var ViewStylePropTypes = { borderLeftWidth: ReactPropTypes.number, opacity: ReactPropTypes.number, overflow: ReactPropTypes.oneOf(['visible', 'hidden']), - shadowColor: ColorPropType, - shadowOffset: ReactPropTypes.shape( - {width: ReactPropTypes.number, height: ReactPropTypes.number} - ), - shadowOpacity: ReactPropTypes.number, - shadowRadius: ReactPropTypes.number, /** * (Android-only) Sets the elevation of a view, using Android's underlying * [elevation API](https://developer.android.com/training/material/shadows-clipping.html#Elevation). diff --git a/Libraries/Image/ImageStylePropTypes.js b/Libraries/Image/ImageStylePropTypes.js index ec806b1b8..5e99a54b0 100644 --- a/Libraries/Image/ImageStylePropTypes.js +++ b/Libraries/Image/ImageStylePropTypes.js @@ -15,10 +15,12 @@ var ImageResizeMode = require('ImageResizeMode'); var LayoutPropTypes = require('LayoutPropTypes'); var ReactPropTypes = require('ReactPropTypes'); var ColorPropType = require('ColorPropType'); +var ShadowPropTypesIOS = require('ShadowPropTypesIOS'); var TransformPropTypes = require('TransformPropTypes'); var ImageStylePropTypes = { ...LayoutPropTypes, + ...ShadowPropTypesIOS, ...TransformPropTypes, resizeMode: ReactPropTypes.oneOf(Object.keys(ImageResizeMode)), backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']), diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m index 4d4380d42..eebbc9c32 100644 --- a/Libraries/Text/RCTShadowText.m +++ b/Libraries/Text/RCTShadowText.m @@ -373,7 +373,6 @@ RCT_TEXT_PROPERTY(IsHighlighted, _isHighlighted, BOOL) RCT_TEXT_PROPERTY(LetterSpacing, _letterSpacing, CGFloat) RCT_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat) RCT_TEXT_PROPERTY(NumberOfLines, _numberOfLines, NSUInteger) -RCT_TEXT_PROPERTY(ShadowOffset, _shadowOffset, CGSize) RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment) RCT_TEXT_PROPERTY(TextDecorationColor, _textDecorationColor, UIColor *); RCT_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, RCTTextDecorationLineType); diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m index 7a28ab928..2a7e4d98b 100644 --- a/Libraries/Text/RCTTextManager.m +++ b/Libraries/Text/RCTTextManager.m @@ -51,7 +51,6 @@ RCT_EXPORT_SHADOW_PROPERTY(isHighlighted, BOOL) RCT_EXPORT_SHADOW_PROPERTY(letterSpacing, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(lineHeight, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(numberOfLines, NSUInteger) -RCT_EXPORT_SHADOW_PROPERTY(shadowOffset, CGSize) RCT_EXPORT_SHADOW_PROPERTY(textAlign, NSTextAlignment) RCT_EXPORT_SHADOW_PROPERTY(textDecorationStyle, NSUnderlineStyle) RCT_EXPORT_SHADOW_PROPERTY(textDecorationColor, UIColor) diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 7c773691e..010c62c49 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -515,8 +515,11 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) // If frame is zero, or below the threshold where the border radii can // be rendered as a stretchable image, we'll need to re-render. // TODO: detect up-front if re-rendering is necessary + CGSize oldSize = self.bounds.size; [super reactSetFrame:frame]; - [self.layer setNeedsDisplay]; + if (!CGSizeEqualToSize(self.bounds.size, oldSize)) { + [self.layer setNeedsDisplay]; + } } - (void)displayLayer:(CALayer *)layer @@ -525,6 +528,8 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) return; } + RCTUpdateShadowPathForView(self); + const RCTCornerRadii cornerRadii = [self cornerRadii]; const UIEdgeInsets borderInsets = [self bordersAsInsets]; const RCTBorderColors borderColors = [self borderColors]; @@ -608,6 +613,44 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) [self updateClippingForLayer:layer]; } +static BOOL RCTLayerHasShadow(CALayer *layer) +{ + return layer.shadowOpacity * CGColorGetAlpha(layer.shadowColor) > 0; +} + +- (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor +{ + // Inherit background color if a shadow has been set, as an optimization + if (RCTLayerHasShadow(self.layer)) { + self.backgroundColor = inheritedBackgroundColor; + } +} + +static void RCTUpdateShadowPathForView(RCTView *view) +{ + if (RCTLayerHasShadow(view.layer)) { + if (CGColorGetAlpha(view.backgroundColor.CGColor) > 0.999) { + + // If view has a solid background color, calculate shadow path from border + const RCTCornerRadii cornerRadii = [view cornerRadii]; + const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero); + CGPathRef shadowPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL); + view.layer.shadowPath = shadowPath; + CGPathRelease(shadowPath); + + } else { + + // Can't accurately calculate box shadow, so fall back to pixel-based shadow + view.layer.shadowPath = nil; + + RCTLogWarn(@"View #%@ of type %@ has a shadow set but cannot calculate " + "shadow efficiently. Consider setting a background color to " + "fix this, or apply the shadow to a more specific component.", + view.reactTag, [view class]); + } + } +} + - (void)updateClippingForLayer:(CALayer *)layer { CALayer *mask = nil; @@ -691,14 +734,14 @@ setBorderRadius(BottomRight) #pragma mark - Border Style -#define setBorderStyle(side) \ +#define setBorderStyle(side) \ - (void)setBorder##side##Style:(RCTBorderStyle)style \ - { \ - if (_border##side##Style == style) { \ - return; \ - } \ - _border##side##Style = style; \ - [self.layer setNeedsDisplay]; \ + { \ + if (_border##side##Style == style) { \ + return; \ + } \ + _border##side##Style = style; \ + [self.layer setNeedsDisplay]; \ } setBorderStyle()