Skip to content

Commit e4c53c2

Browse files
nicklockwoodfacebook-github-bot-4
authored and
facebook-github-bot-4
committed
Improved shadow performance
Summary: public React Native currently exposes the iOS layer shadow properties more-or-less directly, however there are a number of problems with this: 1) Performance when using these properties is poor by default. That's because iOS calculates the shadow by getting the exact pixel mask of the view, including any tranlucent content, and all of its subviews, which is very CPU and GPU-intensive. 2) The iOS shadow properties do not match the syntax or semantics of the CSS box-shadow standard, and are unlikely to be possible to implement on Android. 3) We don't expose the `layer.shadowPath` property, which is crucial to getting good performance out of layer shadows. This diff solves problem number 1) by implementing a default `shadowPath` that matches the view border for views with an opaque background. This improves the performance of shadows by optimizing for the common usage case. I've also reinstated background color propagation for views which have shadow props - this should help ensure that this best-case scenario occurs more often. For views with an explicit transparent background, the shadow will continue to work as it did before ( `shadowPath` will be left unset, and the shadow will be derived exactly from the pixels of the view and its subviews). This is the worst-case path for performance, however, so you should avoid it unless absolutely necessary. **Support for this may be disabled by default in future, or dropped altogether.** For translucent images, it is suggested that you bake the shadow into the image itself, or use another mechanism to pre-generate the shadow. For text shadows, you should use the textShadow properties, which work cross-platform and have much better performance. Problem number 2) will be solved in a future diff, possibly by renaming the iOS shadowXXX properties to boxShadowXXX, and changing the syntax and semantics to match the CSS standards. Problem number 3) is now mostly moot, since we generate the shadowPath automatically. In future, we may provide an iOS-specific prop to set the path explicitly if there's a demand for more precise control of the shadow. Reviewed By: weicool Differential Revision: D2827581 fb-gh-sync-id: 853aa018e1d61d5f88304c6fc1b78f9d7e739804
1 parent 4074b79 commit e4c53c2

File tree

8 files changed

+183
-16
lines changed

8 files changed

+183
-16
lines changed
+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* The examples provided by Facebook are for non-commercial testing and
3+
* evaluation purposes only.
4+
*
5+
* Facebook reserves all rights not expressly granted.
6+
*
7+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
8+
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
9+
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
10+
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
11+
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
12+
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
13+
*/
14+
'use strict';
15+
16+
var React = require('react-native');
17+
var {
18+
Image,
19+
StyleSheet,
20+
View
21+
} = React;
22+
23+
var styles = StyleSheet.create({
24+
box: {
25+
width: 100,
26+
height: 100,
27+
borderWidth: 2,
28+
},
29+
shadow1: {
30+
shadowOpacity: 0.5,
31+
shadowRadius: 3,
32+
shadowOffset: {width: 2, height: 2},
33+
},
34+
shadow2: {
35+
shadowOpacity: 1.0,
36+
shadowColor: 'red',
37+
shadowRadius: 0,
38+
shadowOffset: {width: 3, height: 3},
39+
},
40+
});
41+
42+
exports.title = 'Box Shadow';
43+
exports.description = 'Demonstrates some of the shadow styles available to Views.';
44+
exports.examples = [
45+
{
46+
title: 'Basic shadow',
47+
description: 'shadowOpacity: 0.5, shadowOffset: {2, 2}',
48+
render() {
49+
return <View style={[styles.box, styles.shadow1]} />;
50+
}
51+
},
52+
{
53+
title: 'Colored shadow',
54+
description: 'shadowColor: \'red\', shadowRadius: 0',
55+
render() {
56+
return <View style={[styles.box, styles.shadow2]} />;
57+
}
58+
},
59+
{
60+
title: 'Shaped shadow',
61+
description: 'borderRadius: 50',
62+
render() {
63+
return <View style={[styles.box, styles.shadow1, {borderRadius: 50}]} />;
64+
}
65+
},
66+
{
67+
title: 'Image shadow',
68+
description: 'Image shadows are derived exactly from the pixels.',
69+
render() {
70+
return <Image
71+
source={require('./hawk.png')}
72+
style={[styles.box, styles.shadow1, {borderWidth: 0, overflow: 'visible'}]}
73+
/>;
74+
}
75+
},
76+
{
77+
title: 'Child shadow',
78+
description: 'For views without an opaque background color, shadow will be derived from the subviews.',
79+
render() {
80+
return <View style={[styles.box, styles.shadow1, {backgroundColor: 'transparent'}]}>
81+
<View style={[styles.box, {width: 80, height: 80, borderRadius: 40, margin: 8, backgroundColor: 'red'}]}/>
82+
</View>;
83+
}
84+
},
85+
];

Examples/UIExplorer/UIExplorerList.ios.js

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ var APIS = [
6666
require('./AppStateIOSExample'),
6767
require('./AsyncStorageExample'),
6868
require('./BorderExample'),
69+
require('./BoxShadowExample'),
6970
require('./CameraRollExample'),
7071
require('./ClipboardExample'),
7172
require('./GeolocationExample'),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree. An additional grant
7+
* of patent rights can be found in the PATENTS file in the same directory.
8+
*
9+
* @providesModule ShadowPropTypesIOS
10+
* @flow
11+
*/
12+
'use strict';
13+
14+
var ColorPropType = require('ColorPropType');
15+
var ReactPropTypes = require('ReactPropTypes');
16+
17+
var ShadowPropTypesIOS = {
18+
/**
19+
* Sets the drop shadow color
20+
* @platform ios
21+
*/
22+
shadowColor: ColorPropType,
23+
/**
24+
* Sets the drop shadow offset
25+
* @platform ios
26+
*/
27+
shadowOffset: ReactPropTypes.shape(
28+
{width: ReactPropTypes.number, height: ReactPropTypes.number}
29+
),
30+
/**
31+
* Sets the drop shadow opacity (multiplied by the color's alpha component)
32+
* @platform ios
33+
*/
34+
shadowOpacity: ReactPropTypes.number,
35+
/**
36+
* Sets the drop shadow blur radius
37+
* @platform ios
38+
*/
39+
shadowRadius: ReactPropTypes.number,
40+
};
41+
42+
module.exports = ShadowPropTypesIOS;

Libraries/Components/View/ViewStylePropTypes.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
var LayoutPropTypes = require('LayoutPropTypes');
1515
var ReactPropTypes = require('ReactPropTypes');
1616
var ColorPropType = require('ColorPropType');
17+
var ShadowPropTypesIOS = require('ShadowPropTypesIOS');
1718
var TransformPropTypes = require('TransformPropTypes');
1819

1920
/**
2021
* Warning: Some of these properties may not be supported in all releases.
2122
*/
2223
var ViewStylePropTypes = {
2324
...LayoutPropTypes,
25+
...ShadowPropTypesIOS,
2426
...TransformPropTypes,
2527
backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']),
2628
backgroundColor: ColorPropType,
@@ -42,12 +44,6 @@ var ViewStylePropTypes = {
4244
borderLeftWidth: ReactPropTypes.number,
4345
opacity: ReactPropTypes.number,
4446
overflow: ReactPropTypes.oneOf(['visible', 'hidden']),
45-
shadowColor: ColorPropType,
46-
shadowOffset: ReactPropTypes.shape(
47-
{width: ReactPropTypes.number, height: ReactPropTypes.number}
48-
),
49-
shadowOpacity: ReactPropTypes.number,
50-
shadowRadius: ReactPropTypes.number,
5147
/**
5248
* (Android-only) Sets the elevation of a view, using Android's underlying
5349
* [elevation API](https://developer.android.com/training/material/shadows-clipping.html#Elevation).

Libraries/Image/ImageStylePropTypes.js

+2
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ var ImageResizeMode = require('ImageResizeMode');
1515
var LayoutPropTypes = require('LayoutPropTypes');
1616
var ReactPropTypes = require('ReactPropTypes');
1717
var ColorPropType = require('ColorPropType');
18+
var ShadowPropTypesIOS = require('ShadowPropTypesIOS');
1819
var TransformPropTypes = require('TransformPropTypes');
1920

2021
var ImageStylePropTypes = {
2122
...LayoutPropTypes,
23+
...ShadowPropTypesIOS,
2224
...TransformPropTypes,
2325
resizeMode: ReactPropTypes.oneOf(Object.keys(ImageResizeMode)),
2426
backfaceVisibility: ReactPropTypes.oneOf(['visible', 'hidden']),

Libraries/Text/RCTShadowText.m

-1
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,6 @@ - (void)set##setProp:(type)value; \
373373
RCT_TEXT_PROPERTY(LetterSpacing, _letterSpacing, CGFloat)
374374
RCT_TEXT_PROPERTY(LineHeight, _lineHeight, CGFloat)
375375
RCT_TEXT_PROPERTY(NumberOfLines, _numberOfLines, NSUInteger)
376-
RCT_TEXT_PROPERTY(ShadowOffset, _shadowOffset, CGSize)
377376
RCT_TEXT_PROPERTY(TextAlign, _textAlign, NSTextAlignment)
378377
RCT_TEXT_PROPERTY(TextDecorationColor, _textDecorationColor, UIColor *);
379378
RCT_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, RCTTextDecorationLineType);

Libraries/Text/RCTTextManager.m

-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ - (RCTShadowView *)shadowView
5151
RCT_EXPORT_SHADOW_PROPERTY(letterSpacing, CGFloat)
5252
RCT_EXPORT_SHADOW_PROPERTY(lineHeight, CGFloat)
5353
RCT_EXPORT_SHADOW_PROPERTY(numberOfLines, NSUInteger)
54-
RCT_EXPORT_SHADOW_PROPERTY(shadowOffset, CGSize)
5554
RCT_EXPORT_SHADOW_PROPERTY(textAlign, NSTextAlignment)
5655
RCT_EXPORT_SHADOW_PROPERTY(textDecorationStyle, NSUnderlineStyle)
5756
RCT_EXPORT_SHADOW_PROPERTY(textDecorationColor, UIColor)

React/Views/RCTView.m

+51-8
Original file line numberDiff line numberDiff line change
@@ -515,8 +515,11 @@ - (void)reactSetFrame:(CGRect)frame
515515
// If frame is zero, or below the threshold where the border radii can
516516
// be rendered as a stretchable image, we'll need to re-render.
517517
// TODO: detect up-front if re-rendering is necessary
518+
CGSize oldSize = self.bounds.size;
518519
[super reactSetFrame:frame];
519-
[self.layer setNeedsDisplay];
520+
if (!CGSizeEqualToSize(self.bounds.size, oldSize)) {
521+
[self.layer setNeedsDisplay];
522+
}
520523
}
521524

522525
- (void)displayLayer:(CALayer *)layer
@@ -525,6 +528,8 @@ - (void)displayLayer:(CALayer *)layer
525528
return;
526529
}
527530

531+
RCTUpdateShadowPathForView(self);
532+
528533
const RCTCornerRadii cornerRadii = [self cornerRadii];
529534
const UIEdgeInsets borderInsets = [self bordersAsInsets];
530535
const RCTBorderColors borderColors = [self borderColors];
@@ -608,6 +613,44 @@ - (void)displayLayer:(CALayer *)layer
608613
[self updateClippingForLayer:layer];
609614
}
610615

616+
static BOOL RCTLayerHasShadow(CALayer *layer)
617+
{
618+
return layer.shadowOpacity * CGColorGetAlpha(layer.shadowColor) > 0;
619+
}
620+
621+
- (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor
622+
{
623+
// Inherit background color if a shadow has been set, as an optimization
624+
if (RCTLayerHasShadow(self.layer)) {
625+
self.backgroundColor = inheritedBackgroundColor;
626+
}
627+
}
628+
629+
static void RCTUpdateShadowPathForView(RCTView *view)
630+
{
631+
if (RCTLayerHasShadow(view.layer)) {
632+
if (CGColorGetAlpha(view.backgroundColor.CGColor) > 0.999) {
633+
634+
// If view has a solid background color, calculate shadow path from border
635+
const RCTCornerRadii cornerRadii = [view cornerRadii];
636+
const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero);
637+
CGPathRef shadowPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL);
638+
view.layer.shadowPath = shadowPath;
639+
CGPathRelease(shadowPath);
640+
641+
} else {
642+
643+
// Can't accurately calculate box shadow, so fall back to pixel-based shadow
644+
view.layer.shadowPath = nil;
645+
646+
RCTLogWarn(@"View #%@ of type %@ has a shadow set but cannot calculate "
647+
"shadow efficiently. Consider setting a background color to "
648+
"fix this, or apply the shadow to a more specific component.",
649+
view.reactTag, [view class]);
650+
}
651+
}
652+
}
653+
611654
- (void)updateClippingForLayer:(CALayer *)layer
612655
{
613656
CALayer *mask = nil;
@@ -691,14 +734,14 @@ - (void)setBorder##side##Radius:(CGFloat)radius \
691734

692735
#pragma mark - Border Style
693736

694-
#define setBorderStyle(side) \
737+
#define setBorderStyle(side) \
695738
- (void)setBorder##side##Style:(RCTBorderStyle)style \
696-
{ \
697-
if (_border##side##Style == style) { \
698-
return; \
699-
} \
700-
_border##side##Style = style; \
701-
[self.layer setNeedsDisplay]; \
739+
{ \
740+
if (_border##side##Style == style) { \
741+
return; \
742+
} \
743+
_border##side##Style = style; \
744+
[self.layer setNeedsDisplay]; \
702745
}
703746

704747
setBorderStyle()

0 commit comments

Comments
 (0)