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:
parent
7807247905
commit
1658f36630
|
@ -232,7 +232,6 @@ static void RCTProcessMetaPropsBorder(const YGValue metaProps[META_PROP_COUNT],
|
|||
[self didUpdateReactSubviews];
|
||||
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
UIView *view = viewRegistry[self->_reactTag];
|
||||
[view clearSortedSubviews];
|
||||
[view didUpdateReactSubviews];
|
||||
}];
|
||||
}
|
||||
|
|
|
@ -47,13 +47,6 @@
|
|||
*/
|
||||
@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
|
||||
* for large scrolling views with many subviews, such as a
|
||||
|
|
|
@ -102,8 +102,6 @@ static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
|
|||
UIColor *_backgroundColor;
|
||||
}
|
||||
|
||||
@synthesize reactZIndex = _reactZIndex;
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
|
@ -169,13 +167,16 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
|||
BOOL isPointInside = [self pointInside:point withEvent:event];
|
||||
BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly);
|
||||
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,
|
||||
// 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 strict containment policy (i.e., UIKit guarantees that every ancestor
|
||||
// of the hit view will return YES from -pointInside:withEvent:). See:
|
||||
// - 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];
|
||||
hitSubview = [subview hitTest:convertedPoint withEvent:event];
|
||||
if (hitSubview != nil) {
|
||||
|
@ -291,7 +292,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
|||
- (void)react_remountAllSubviews
|
||||
{
|
||||
if (_removeClippedSubviews) {
|
||||
for (UIView *view in self.sortedReactSubviews) {
|
||||
for (UIView *view in self.reactSubviews) {
|
||||
if (view.superview != self) {
|
||||
[self addSubview:view];
|
||||
[view react_remountAllSubviews];
|
||||
|
@ -330,7 +331,7 @@ RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
|||
clipView = self;
|
||||
|
||||
// Mount / unmount views
|
||||
for (UIView *view in self.sortedReactSubviews) {
|
||||
for (UIView *view in self.reactSubviews) {
|
||||
if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) {
|
||||
|
||||
// View is at least partially visible, so remount it if unmounted
|
||||
|
|
|
@ -16,7 +16,4 @@
|
|||
- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView;
|
||||
- (UIView *)react_findClipView;
|
||||
|
||||
// zIndex sorting
|
||||
- (void)clearSortedSubviews;
|
||||
|
||||
@end
|
||||
|
|
|
@ -31,15 +31,15 @@
|
|||
@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;
|
||||
|
||||
/**
|
||||
* The reactSubviews array, sorted by zIndex. This value is cached and
|
||||
* automatically recalculated if views are added or removed.
|
||||
* Subviews sorted by z-index. Note that this method doesn't do any caching (yet)
|
||||
* 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
|
||||
|
|
|
@ -110,49 +110,38 @@
|
|||
|
||||
- (NSInteger)reactZIndex
|
||||
{
|
||||
return [objc_getAssociatedObject(self, _cmd) integerValue];
|
||||
return self.layer.zPosition;
|
||||
}
|
||||
|
||||
- (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);
|
||||
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;
|
||||
}
|
||||
// Check if sorting is required - in most cases it won't be.
|
||||
BOOL sortingRequired = NO;
|
||||
for (UIView *subview in self.subviews) {
|
||||
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);
|
||||
return 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.subviews;
|
||||
}
|
||||
|
||||
- (void)didUpdateReactSubviews
|
||||
{
|
||||
for (UIView *subview in self.sortedReactSubviews) {
|
||||
for (UIView *subview in self.reactSubviews) {
|
||||
[self addSubview:subview];
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue