Improve z-index implementation on iOS

Summary:
This avoids reordering views because it created some bugs when the native hierarchy is different from the shadow views. This leverages `layer.zPosition` and takes z-index in consideration when we check what view should be the target of a touch.

**Test plan**
Tested that this fixes some layout issues that occurred when using sticky headers in the Expo home screen.
Closes https://github.com/facebook/react-native/pull/14011

Differential Revision: D5108437

Pulled By: shergin

fbshipit-source-id: 0abfe85666e9d236a190e6f54cdd5453cacfbcac
This commit is contained in:
Janic Duplessis 2017-05-28 21:35:38 -07:00 committed by Facebook Github Bot
parent 7807247905
commit 1658f36630
6 changed files with 29 additions and 50 deletions

View File

@ -232,7 +232,6 @@ static void RCTProcessMetaPropsBorder(const YGValue metaProps[META_PROP_COUNT],
[self didUpdateReactSubviews]; [self didUpdateReactSubviews];
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) { [applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
UIView *view = viewRegistry[self->_reactTag]; UIView *view = viewRegistry[self->_reactTag];
[view clearSortedSubviews];
[view didUpdateReactSubviews]; [view didUpdateReactSubviews];
}]; }];
} }

View File

@ -47,13 +47,6 @@
*/ */
@property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection; @property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection;
/**
* 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) NSInteger 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

View File

@ -102,8 +102,6 @@ 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])) {
@ -169,13 +167,16 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
BOOL isPointInside = [self pointInside:point withEvent:event]; BOOL isPointInside = [self pointInside:point withEvent:event];
BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly); BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly);
if (needsHitSubview && (![self clipsToBounds] || isPointInside)) { if (needsHitSubview && (![self clipsToBounds] || isPointInside)) {
// Take z-index into account when calculating the touch target.
NSArray<UIView *> *sortedSubviews = [self reactZIndexSortedSubviews];
// The default behaviour of UIKit is that if a view does not contain a point, // The default behaviour of UIKit is that if a view does not contain a point,
// then no subviews will be returned from hit testing, even if they contain // then no subviews will be returned from hit testing, even if they contain
// the hit point. By doing hit testing directly on the subviews, we bypass // the hit point. By doing hit testing directly on the subviews, we bypass
// the strict containment policy (i.e., UIKit guarantees that every ancestor // the strict containment policy (i.e., UIKit guarantees that every ancestor
// of the hit view will return YES from -pointInside:withEvent:). See: // of the hit view will return YES from -pointInside:withEvent:). See:
// - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html // - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
for (UIView *subview in [self.subviews reverseObjectEnumerator]) { for (UIView *subview in [sortedSubviews reverseObjectEnumerator]) {
CGPoint convertedPoint = [subview convertPoint:point fromView:self]; CGPoint convertedPoint = [subview convertPoint:point fromView:self];
hitSubview = [subview hitTest:convertedPoint withEvent:event]; hitSubview = [subview hitTest:convertedPoint withEvent:event];
if (hitSubview != nil) { if (hitSubview != nil) {
@ -291,7 +292,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
- (void)react_remountAllSubviews - (void)react_remountAllSubviews
{ {
if (_removeClippedSubviews) { if (_removeClippedSubviews) {
for (UIView *view in self.sortedReactSubviews) { for (UIView *view in self.reactSubviews) {
if (view.superview != self) { if (view.superview != self) {
[self addSubview:view]; [self addSubview:view];
[view react_remountAllSubviews]; [view react_remountAllSubviews];
@ -330,7 +331,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
clipView = self; clipView = self;
// Mount / unmount views // Mount / unmount views
for (UIView *view in self.sortedReactSubviews) { for (UIView *view in self.reactSubviews) {
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

View File

@ -16,7 +16,4 @@
- (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

View File

@ -31,15 +31,15 @@
@property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection; @property (nonatomic, assign) UIUserInterfaceLayoutDirection reactLayoutDirection;
/** /**
* z-index, used to override sibling order in didUpdateReactSubviews. * The z-index of the view.
*/ */
@property (nonatomic, assign) NSInteger reactZIndex; @property (nonatomic, assign) NSInteger reactZIndex;
/** /**
* The reactSubviews array, sorted by zIndex. This value is cached and * Subviews sorted by z-index. Note that this method doesn't do any caching (yet)
* automatically recalculated if views are added or removed. * and sorts all the views each call.
*/ */
@property (nonatomic, copy, readonly) NSArray<UIView *> *sortedReactSubviews; - (NSArray<UIView *> *)reactZIndexSortedSubviews;
/** /**
* Updates the subviews array based on the reactSubviews. Default behavior is * Updates the subviews array based on the reactSubviews. Default behavior is

View File

@ -110,49 +110,38 @@
- (NSInteger)reactZIndex - (NSInteger)reactZIndex
{ {
return [objc_getAssociatedObject(self, _cmd) integerValue]; return self.layer.zPosition;
} }
- (void)setReactZIndex:(NSInteger)reactZIndex - (void)setReactZIndex:(NSInteger)reactZIndex
{ {
objc_setAssociatedObject(self, @selector(reactZIndex), @(reactZIndex), OBJC_ASSOCIATION_RETAIN_NONATOMIC); self.layer.zPosition = reactZIndex;
} }
- (NSArray<UIView *> *)sortedReactSubviews - (NSArray<UIView *> *)reactZIndexSortedSubviews
{ {
NSArray *subviews = objc_getAssociatedObject(self, _cmd); // Check if sorting is required - in most cases it won't be.
if (!subviews) { BOOL sortingRequired = NO;
// Check if sorting is required - in most cases it won't be for (UIView *subview in self.subviews) {
BOOL sortingRequired = NO; if (subview.reactZIndex != 0) {
for (UIView *subview in self.reactSubviews) { sortingRequired = YES;
if (subview.reactZIndex != 0) { break;
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; return sortingRequired ? [self.reactSubviews sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
} if (a.reactZIndex > b.reactZIndex) {
return NSOrderedDescending;
// private method, used to reset sort } else {
- (void)clearSortedSubviews // Ensure sorting is stable by treating equal zIndex as ascending so
{ // that original order is preserved.
objc_setAssociatedObject(self, @selector(sortedReactSubviews), nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return NSOrderedAscending;
}
}] : self.subviews;
} }
- (void)didUpdateReactSubviews - (void)didUpdateReactSubviews
{ {
for (UIView *subview in self.sortedReactSubviews) { for (UIView *subview in self.reactSubviews) {
[self addSubview:subview]; [self addSubview:subview];
} }
} }