mirror of
https://github.com/status-im/react-native.git
synced 2025-02-25 15:45:32 +00:00
Implement CSS z-index for iOS
Summary: This diff implement the CSS z-index for React Native iOS views. We've had numerous pull request for this feature, but they've all attempted to use the `layer.zPosition` property, which is problematic for two reasons: 1. zPosition only affects rendering order, not event processing order. Views with a higher zPosition will appear in front of others in the hierarchy, but won't be the first to receive touch events, and may be blocked by views that are visually behind them. 2. when using a perspective transform matrix, views with a nonzero zPosition will be rendered in a different position due to parallax, which probably isn't desirable. See https://github.com/facebook/react-native/pull/7825 for further discussion of this problem. So instead of using `layer.zPosition`, I've implemented this by actually adjusting the order of the subviews within their parent based on the zIndex. This can't be done on the JS side because it would affect layout, which is order-dependent, so I'm doing it inside the view itself. It works as follows: 1. The `reactSubviews` array is set, whose order matches the order of the JS components and shadowView components, as specified by the UIManager. 2. `didUpdateReactSubviews` is called, which in turn calls `sortedSubviews` (which lazily generates a sorted array of `reactSubviews` by zIndex) and inserts the result into the view. 3. If a subview is added or removed, or the zIndex of any subview is changed, the previous `sortedSubviews` array is cleared and `didUpdateReactSubviews` is called again. To demonstrate it working, I've modified the UIExplorer example from https://github.com/facebook/react-native/pull/7825 Reviewed By: javache Differential Revision: D3365717 fbshipit-source-id: b34aa8bfad577bce023f8af5414f9b974aafd8aa
This commit is contained in:
parent
993a928833
commit
d64368b9e2
@ -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
|
* The examples provided by Facebook are for non-commercial testing and
|
||||||
* evaluation purposes only.
|
* evaluation purposes only.
|
||||||
*
|
*
|
||||||
@ -30,7 +37,13 @@ var styles = StyleSheet.create({
|
|||||||
backgroundColor: '#527FE4',
|
backgroundColor: '#527FE4',
|
||||||
borderColor: '#000033',
|
borderColor: '#000033',
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
}
|
},
|
||||||
|
zIndex: {
|
||||||
|
justifyContent: 'space-around',
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
marginTop: -10,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
var ViewBorderStyleExample = React.createClass({
|
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 (
|
||||||
|
<TouchableWithoutFeedback onPress={this._handlePress}>
|
||||||
|
<View>
|
||||||
|
<Text style={{paddingBottom: 10}}>Tap to flip sorting order</Text>
|
||||||
|
<View style={[
|
||||||
|
styles.zIndex,
|
||||||
|
{marginTop: 0, backgroundColor: '#E57373', zIndex: indices[0]}
|
||||||
|
]}>
|
||||||
|
<Text>ZIndex {indices[0]}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[
|
||||||
|
styles.zIndex,
|
||||||
|
{marginLeft: 50, backgroundColor: '#FFF176', zIndex: indices[1]}
|
||||||
|
]}>
|
||||||
|
<Text>ZIndex {indices[1]}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[
|
||||||
|
styles.zIndex,
|
||||||
|
{marginLeft: 100, backgroundColor: '#81C784', zIndex: indices[2]}
|
||||||
|
]}>
|
||||||
|
<Text>ZIndex {indices[2]}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[
|
||||||
|
styles.zIndex,
|
||||||
|
{marginLeft: 150, backgroundColor: '#64B5F6', zIndex: indices[3]}
|
||||||
|
]}>
|
||||||
|
<Text>ZIndex {indices[3]}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableWithoutFeedback>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_handlePress() {
|
||||||
|
this.setState({flipped: !this.state.flipped});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
exports.title = '<View>';
|
exports.title = '<View>';
|
||||||
exports.description = 'Basic building block of all UI, examples that ' +
|
exports.description = 'Basic building block of all UI, examples that ' +
|
||||||
'demonstrate some of the many styles available.';
|
'demonstrate some of the many styles available.';
|
||||||
@ -188,5 +248,10 @@ exports.examples = [
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
title: 'ZIndex',
|
||||||
|
render: function() {
|
||||||
|
return <ZIndexExample />;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -100,6 +100,9 @@ var LayoutPropTypes = {
|
|||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/CSS/flex
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/flex
|
||||||
flex: ReactPropTypes.number,
|
flex: ReactPropTypes.number,
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/CSS/z-index
|
||||||
|
zIndex: ReactPropTypes.number,
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = LayoutPropTypes;
|
module.exports = LayoutPropTypes;
|
||||||
|
@ -893,8 +893,6 @@ static void RCTSetChildren(NSNumber *containerTag,
|
|||||||
[container insertReactSubview:view atIndex:index++];
|
[container insertReactSubview:view atIndex:index++];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[container didUpdateReactSubviews];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag
|
RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag
|
||||||
@ -965,8 +963,6 @@ RCT_EXPORT_METHOD(manageChildren:(nonnull NSNumber *)containerTag
|
|||||||
[container insertReactSubview:destinationsToChildrenToAdd[reactIndex]
|
[container insertReactSubview:destinationsToChildrenToAdd[reactIndex]
|
||||||
atIndex:reactIndex.integerValue];
|
atIndex:reactIndex.integerValue];
|
||||||
}
|
}
|
||||||
|
|
||||||
[container didUpdateReactSubviews];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
|
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
|
||||||
|
@ -129,6 +129,11 @@ typedef void (^RCTApplierBlock)(NSDictionary<NSNumber *, UIView *> *viewRegistry
|
|||||||
@property (nonatomic, assign) css_wrap_type_t flexWrap;
|
@property (nonatomic, assign) css_wrap_type_t flexWrap;
|
||||||
@property (nonatomic, assign) CGFloat flex;
|
@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.
|
* Calculate property changes that need to be propagated to the view.
|
||||||
* The applierBlocks set contains RCTApplierBlock functions that must be applied
|
* The applierBlocks set contains RCTApplierBlock functions that must be applied
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
#import "RCTLog.h"
|
#import "RCTLog.h"
|
||||||
#import "RCTUtils.h"
|
#import "RCTUtils.h"
|
||||||
#import "UIView+React.h"
|
#import "UIView+React.h"
|
||||||
|
#import "UIView+Private.h"
|
||||||
|
|
||||||
typedef void (^RCTActionBlock)(RCTShadowView *shadowViewSelf, id value);
|
typedef void (^RCTActionBlock)(RCTShadowView *shadowViewSelf, id value);
|
||||||
typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf);
|
typedef void (^RCTResetActionBlock)(RCTShadowView *shadowViewSelf);
|
||||||
@ -39,6 +40,7 @@ typedef NS_ENUM(unsigned int, meta_prop_t) {
|
|||||||
BOOL _recomputePadding;
|
BOOL _recomputePadding;
|
||||||
BOOL _recomputeMargin;
|
BOOL _recomputeMargin;
|
||||||
BOOL _recomputeBorder;
|
BOOL _recomputeBorder;
|
||||||
|
BOOL _didUpdateSubviews;
|
||||||
float _paddingMetaProps[META_PROP_COUNT];
|
float _paddingMetaProps[META_PROP_COUNT];
|
||||||
float _marginMetaProps[META_PROP_COUNT];
|
float _marginMetaProps[META_PROP_COUNT];
|
||||||
float _borderMetaProps[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
|
// dirtied, but really we should track which properties have changed and
|
||||||
// only update those.
|
// only update those.
|
||||||
|
|
||||||
|
if (_didUpdateSubviews) {
|
||||||
|
_didUpdateSubviews = NO;
|
||||||
|
[self didUpdateReactSubviews];
|
||||||
|
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||||
|
UIView *view = viewRegistry[_reactTag];
|
||||||
|
[view clearSortedSubviews];
|
||||||
|
[view didUpdateReactSubviews];
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
if (!_backgroundColor) {
|
if (!_backgroundColor) {
|
||||||
UIColor *parentBackgroundColor = parentProperties[RCTBackgroundColorProp];
|
UIColor *parentBackgroundColor = parentProperties[RCTBackgroundColorProp];
|
||||||
if (parentBackgroundColor) {
|
if (parentBackgroundColor) {
|
||||||
@ -351,6 +363,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
|
|||||||
[_reactSubviews insertObject:subview atIndex:atIndex];
|
[_reactSubviews insertObject:subview atIndex:atIndex];
|
||||||
_cssNode->children_count = (int)_reactSubviews.count;
|
_cssNode->children_count = (int)_reactSubviews.count;
|
||||||
subview->_superview = self;
|
subview->_superview = self;
|
||||||
|
_didUpdateSubviews = YES;
|
||||||
[self dirtyText];
|
[self dirtyText];
|
||||||
[self dirtyLayout];
|
[self dirtyLayout];
|
||||||
[self dirtyPropagation];
|
[self dirtyPropagation];
|
||||||
@ -361,6 +374,7 @@ static void RCTProcessMetaProps(const float metaProps[META_PROP_COUNT], float st
|
|||||||
[subview dirtyText];
|
[subview dirtyText];
|
||||||
[subview dirtyLayout];
|
[subview dirtyLayout];
|
||||||
[subview dirtyPropagation];
|
[subview dirtyPropagation];
|
||||||
|
_didUpdateSubviews = YES;
|
||||||
subview->_superview = nil;
|
subview->_superview = nil;
|
||||||
[_reactSubviews removeObject:subview];
|
[_reactSubviews removeObject:subview];
|
||||||
_cssNode->children_count = (int)_reactSubviews.count;
|
_cssNode->children_count = (int)_reactSubviews.count;
|
||||||
@ -596,6 +610,16 @@ RCT_STYLE_PROPERTY(FlexWrap, flexWrap, flex_wrap, css_wrap_type_t)
|
|||||||
[self dirtyPropagation];
|
[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
|
- (void)didUpdateReactSubviews
|
||||||
{
|
{
|
||||||
// Does nothing by default
|
// Does nothing by default
|
||||||
|
@ -41,6 +41,13 @@
|
|||||||
*/
|
*/
|
||||||
+ (UIEdgeInsets)contentInsetsForView:(UIView *)curView;
|
+ (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
|
* This is an optimization used to improve performance
|
||||||
* for large scrolling views with many subviews, such as a
|
* for large scrolling views with many subviews, such as a
|
||||||
|
@ -99,6 +99,8 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
|
|||||||
UIColor *_backgroundColor;
|
UIColor *_backgroundColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@synthesize reactZIndex = _reactZIndex;
|
||||||
|
|
||||||
- (instancetype)initWithFrame:(CGRect)frame
|
- (instancetype)initWithFrame:(CGRect)frame
|
||||||
{
|
{
|
||||||
if ((self = [super initWithFrame:frame])) {
|
if ((self = [super initWithFrame:frame])) {
|
||||||
@ -274,7 +276,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
|||||||
- (void)react_remountAllSubviews
|
- (void)react_remountAllSubviews
|
||||||
{
|
{
|
||||||
if (_removeClippedSubviews) {
|
if (_removeClippedSubviews) {
|
||||||
for (UIView *view in self.reactSubviews) {
|
for (UIView *view in self.sortedReactSubviews) {
|
||||||
if (view.superview != self) {
|
if (view.superview != self) {
|
||||||
[self addSubview:view];
|
[self addSubview:view];
|
||||||
[view react_remountAllSubviews];
|
[view react_remountAllSubviews];
|
||||||
@ -313,7 +315,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
|||||||
clipView = self;
|
clipView = self;
|
||||||
|
|
||||||
// Mount / unmount views
|
// Mount / unmount views
|
||||||
for (UIView *view in self.reactSubviews) {
|
for (UIView *view in self.sortedReactSubviews) {
|
||||||
if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) {
|
if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) {
|
||||||
|
|
||||||
// View is at least partially visible, so remount it if unmounted
|
// View is at least partially visible, so remount it if unmounted
|
||||||
|
@ -246,6 +246,8 @@ RCT_VIEW_BORDER_RADIUS_PROPERTY(TopRight)
|
|||||||
RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomLeft)
|
RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomLeft)
|
||||||
RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomRight)
|
RCT_VIEW_BORDER_RADIUS_PROPERTY(BottomRight)
|
||||||
|
|
||||||
|
RCT_REMAP_VIEW_PROPERTY(zIndex, reactZIndex, double)
|
||||||
|
|
||||||
#pragma mark - ShadowView properties
|
#pragma mark - ShadowView properties
|
||||||
|
|
||||||
RCT_EXPORT_SHADOW_PROPERTY(backgroundColor, UIColor)
|
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(onLayout, RCTDirectEventBlock)
|
||||||
|
|
||||||
|
RCT_EXPORT_SHADOW_PROPERTY(zIndex, double)
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -9,10 +9,14 @@
|
|||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
@interface UIView (RCTViewUnmounting)
|
@interface UIView (Private)
|
||||||
|
|
||||||
|
// remove clipped subviews implementation
|
||||||
- (void)react_remountAllSubviews;
|
- (void)react_remountAllSubviews;
|
||||||
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView;
|
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView;
|
||||||
- (UIView *)react_findClipView;
|
- (UIView *)react_findClipView;
|
||||||
|
|
||||||
|
// zIndex sorting
|
||||||
|
- (void)clearSortedSubviews;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
@ -25,9 +25,20 @@
|
|||||||
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER;
|
- (void)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex NS_REQUIRES_SUPER;
|
||||||
- (void)removeReactSubview:(UIView *)subview 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<UIView *> *sortedReactSubviews;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the subviews array based on the reactSubviews. Default behavior is
|
* 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;
|
- (void)didUpdateReactSubviews;
|
||||||
|
|
||||||
|
@ -87,9 +87,51 @@
|
|||||||
[subview removeFromSuperview];
|
[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<UIView *> *)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
|
- (void)didUpdateReactSubviews
|
||||||
{
|
{
|
||||||
for (UIView *subview in self.reactSubviews) {
|
for (UIView *subview in self.sortedReactSubviews) {
|
||||||
[self addSubview:subview];
|
[self addSubview:subview];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user