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];
|
[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];
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue