diff --git a/Examples/UIExplorer/ViewExample.js b/Examples/UIExplorer/ViewExample.js index c5cb7cae7..7dc42925b 100644 --- a/Examples/UIExplorer/ViewExample.js +++ b/Examples/UIExplorer/ViewExample.js @@ -1,4 +1,11 @@ /** + * Copyright (c) 2013-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. + * * The examples provided by Facebook are for non-commercial testing and * evaluation purposes only. * @@ -30,7 +37,13 @@ var styles = StyleSheet.create({ backgroundColor: '#527FE4', borderColor: '#000033', borderWidth: 1, - } + }, + zIndex: { + justifyContent: 'space-around', + width: 100, + height: 50, + marginTop: -10, + }, }); var ViewBorderStyleExample = React.createClass({ @@ -74,6 +87,53 @@ var ViewBorderStyleExample = React.createClass({ } }); +var ZIndexExample = React.createClass({ + getInitialState() { + return { + flipped: false + }; + }, + + render() { + const indices = this.state.flipped ? [-1, 0, 1, 2] : [2, 1, 0, -1]; + return ( + + + Tap to flip sorting order + + ZIndex {indices[0]} + + + ZIndex {indices[1]} + + + ZIndex {indices[2]} + + + ZIndex {indices[3]} + + + + ); + }, + + _handlePress() { + this.setState({flipped: !this.state.flipped}); + } +}); + exports.title = ''; exports.description = 'Basic building block of all UI, examples that ' + 'demonstrate some of the many styles available.'; @@ -188,5 +248,10 @@ exports.examples = [ ); }, + }, { + title: 'ZIndex', + render: function() { + return ; + }, }, ]; diff --git a/Libraries/StyleSheet/LayoutPropTypes.js b/Libraries/StyleSheet/LayoutPropTypes.js index 95414b339..a07e16fe2 100644 --- a/Libraries/StyleSheet/LayoutPropTypes.js +++ b/Libraries/StyleSheet/LayoutPropTypes.js @@ -100,6 +100,9 @@ var LayoutPropTypes = { // https://developer.mozilla.org/en-US/docs/Web/CSS/flex flex: ReactPropTypes.number, + + // https://developer.mozilla.org/en-US/docs/Web/CSS/z-index + zIndex: ReactPropTypes.number, }; module.exports = LayoutPropTypes; diff --git a/React/Modules/RCTUIManager.m b/React/Modules/RCTUIManager.m index 4ab30f991..62539be9d 100644 --- a/React/Modules/RCTUIManager.m +++ b/React/Modules/RCTUIManager.m @@ -893,8 +893,6 @@ static void RCTSetChildren(NSNumber *containerTag, [container insertReactSubview:view atIndex:index++]; } } - - [container didUpdateReactSubviews]; } RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag @@ -965,8 +963,6 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag [container insertReactSubview:destinationsToChildrenToAdd[reactIndex] atIndex:reactIndex.integerValue]; } - - [container didUpdateReactSubviews]; } RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag diff --git a/React/Views/RCTShadowView.h b/React/Views/RCTShadowView.h index ed3e42988..d32ba1365 100644 --- a/React/Views/RCTShadowView.h +++ b/React/Views/RCTShadowView.h @@ -129,6 +129,11 @@ typedef void (^RCTApplierBlock)(NSDictionary *viewRegistry @property (nonatomic, assign) css_wrap_type_t flexWrap; @property (nonatomic, assign) CGFloat flex; +/** + * z-index, used to override sibling order in the view + */ +@property (nonatomic, assign) double zIndex; + /** * Calculate property changes that need to be propagated to the view. * The applierBlocks set contains RCTApplierBlock functions that must be applied diff --git a/React/Views/RCTShadowView.m b/React/Views/RCTShadowView.m index d257e9659..b518929dd 100644 --- a/React/Views/RCTShadowView.m +++ b/React/Views/RCTShadowView.m @@ -13,6 +13,7 @@ #import "RCTLog.h" #import "RCTUtils.h" #import "UIView+React.h" +#import "UIView+Private.h" typedef void (^RCTActionBlock)(RCTShadowView *shadowViewSelf, id value); typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf); @@ -39,6 +40,7 @@ typedef NS_ENUM(unsigned int, meta_prop_t) { BOOL _recomputePadding; BOOL _recomputeMargin; BOOL _recomputeBorder; + BOOL _didUpdateSubviews; float _paddingMetaProps[META_PROP_COUNT]; float _marginMetaProps[META_PROP_COUNT]; float _borderMetaProps[META_PROP_COUNT]; @@ -179,6 +181,16 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st // dirtied, but really we should track which properties have changed and // only update those. + if (_didUpdateSubviews) { + _didUpdateSubviews = NO; + [self didUpdateReactSubviews]; + [applierBlocks addObject:^(NSDictionary *viewRegistry) { + UIView *view = viewRegistry[_reactTag]; + [view clearSortedSubviews]; + [view didUpdateReactSubviews]; + }]; + } + if (!_backgroundColor) { UIColor *parentBackgroundColor = parentProperties[RCTBackgroundColorProp]; if (parentBackgroundColor) { @@ -351,6 +363,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st [_reactSubviews insertObject:subview atIndex:atIndex]; _cssNode->children_count = (int)_reactSubviews.count; subview->_superview = self; + _didUpdateSubviews = YES; [self dirtyText]; [self dirtyLayout]; [self dirtyPropagation]; @@ -361,6 +374,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st [subview dirtyText]; [subview dirtyLayout]; [subview dirtyPropagation]; + _didUpdateSubviews = YES; subview->_superview = nil; [_reactSubviews removeObject:subview]; _cssNode->children_count = (int)_reactSubviews.count; @@ -596,6 +610,16 @@ RCT_STYLE_PROPERTY(FlexWrap, flexWrap, flex_wrap, css_wrap_type_t) [self dirtyPropagation]; } +- (void)setZIndex:(double)zIndex +{ + _zIndex = zIndex; + if (_superview) { + // Changing zIndex means the subview order of the parent needs updating + _superview->_didUpdateSubviews = YES; + [_superview dirtyPropagation]; + } +} + - (void)didUpdateReactSubviews { // Does nothing by default diff --git a/React/Views/RCTView.h b/React/Views/RCTView.h index 7dc5bf318..ea17f568a 100644 --- a/React/Views/RCTView.h +++ b/React/Views/RCTView.h @@ -41,6 +41,13 @@ */ + (UIEdgeInsets)contentInsetsForView:(UIView *)curView; +/** + * z-index, used to override sibling order in didUpdateReactSubviews. This is + * inherited from UIView+React, but we override it here to reduce the boxing + * and associated object overheads. + */ +@property (nonatomic, assign) double reactZIndex; + /** * This is an optimization used to improve performance * for large scrolling views with many subviews, such as a diff --git a/React/Views/RCTView.m b/React/Views/RCTView.m index 4d1b362ae..ac8718eed 100644 --- a/React/Views/RCTView.m +++ b/React/Views/RCTView.m @@ -99,6 +99,8 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view) UIColor *_backgroundColor; } +@synthesize reactZIndex = _reactZIndex; + - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { @@ -274,7 +276,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) - (void)react_remountAllSubviews { if (_removeClippedSubviews) { - for (UIView *view in self.reactSubviews) { + for (UIView *view in self.sortedReactSubviews) { if (view.superview != self) { [self addSubview:view]; [view react_remountAllSubviews]; @@ -313,7 +315,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) clipView = self; // Mount / unmount views - for (UIView *view in self.reactSubviews) { + for (UIView *view in self.sortedReactSubviews) { if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) { // View is at least partially visible, so remount it if unmounted diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index 899b3f031..c54e726c3 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -246,6 +246,8 @@ RCT_VIEW_BORDER_RADIUS_PROPERTY(TopRight) RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomLeft) RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomRight) +RCT_REMAP_VIEW_PROPERTY(zIndex, reactZIndex, double) + #pragma mark - ShadowView properties RCT_EXPORT_SHADOW_PROPERTY(backgroundColor, UIColor) @@ -290,4 +292,6 @@ RCT_EXPORT_SHADOW_PROPERTY(position, css_position_type_t) RCT_EXPORT_SHADOW_PROPERTY(onLayout, RCTDirectEventBlock) +RCT_EXPORT_SHADOW_PROPERTY(zIndex, double) + @end diff --git a/React/Views/UIView+Private.h b/React/Views/UIView+Private.h index 14e6fcd2b..057c46293 100644 --- a/React/Views/UIView+Private.h +++ b/React/Views/UIView+Private.h @@ -9,10 +9,14 @@ #import -@interface UIView (RCTViewUnmounting) +@interface UIView (Private) +// remove clipped subviews implementation - (void)react_remountAllSubviews; - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView; - (UIView *)react_findClipView; +// zIndex sorting +- (void)clearSortedSubviews; + @end diff --git a/React/Views/UIView+React.h b/React/Views/UIView+React.h index fa779e520..61484740b 100644 --- a/React/Views/UIView+React.h +++ b/React/Views/UIView+React.h @@ -25,9 +25,20 @@ - (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER; - (void)removeReactSubview:(UIView *)subview NS_REQUIRES_SUPER; +/** + * z-index, used to override sibling order in didUpdateReactSubviews. + */ +@property (nonatomic, assign) double reactZIndex; + +/** + * The reactSubviews array, sorted by zIndex. This value is cached and + * automatically recalculated if views are added or removed. + */ +@property (nonatomic, copy, readonly) NSArray *sortedReactSubviews; + /** * Updates the subviews array based on the reactSubviews. Default behavior is - * to insert the reactSubviews into the UIView. + * to insert the sortedReactSubviews into the UIView. */ - (void)didUpdateReactSubviews; diff --git a/React/Views/UIView+React.m b/React/Views/UIView+React.m index f25c71644..7004493d8 100644 --- a/React/Views/UIView+React.m +++ b/React/Views/UIView+React.m @@ -87,9 +87,51 @@ [subview removeFromSuperview]; } +- (double)reactZIndex +{ + return [objc_getAssociatedObject(self, _cmd) doubleValue]; +} + +- (void)setReactZIndex:(double)reactZIndex +{ + objc_setAssociatedObject(self, @selector(reactZIndex), @(reactZIndex), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSArray *)sortedReactSubviews +{ + NSArray *subviews = objc_getAssociatedObject(self, _cmd); + if (!subviews) { + // Check if sorting is required - in most cases it won't be + BOOL sortingRequired = NO; + for (UIView *subview in self.reactSubviews) { + if (subview.reactZIndex != 0) { + sortingRequired = YES; + break; + } + } + subviews = sortingRequired ? [self.reactSubviews sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) { + if (a.reactZIndex > b.reactZIndex) { + return NSOrderedDescending; + } else { + // ensure sorting is stable by treating equal zIndex as ascending so + // that original order is preserved + return NSOrderedAscending; + } + }] : self.reactSubviews; + objc_setAssociatedObject(self, _cmd, subviews, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } + return subviews; +} + +// private method, used to reset sort +- (void)clearSortedSubviews +{ + objc_setAssociatedObject(self, @selector(sortedReactSubviews), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + - (void)didUpdateReactSubviews { - for (UIView *subview in self.reactSubviews) { + for (UIView *subview in self.sortedReactSubviews) { [self addSubview:subview]; } }