/** * Copyright (c) 2015-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. */ #import "RCTView.h" #import "RCTAutoInsetsProtocol.h" #import "RCTBorderDrawing.h" #import "RCTConvert.h" #import "RCTLog.h" #import "RCTUtils.h" #import "UIView+React.h" #import "UIView+Private.h" static NSString *RCTRecursiveAccessibilityLabel(UIView *view) { NSMutableString *str = [NSMutableString stringWithString:@""]; for (UIView *subview in view.subviews) { NSString *label = subview.accessibilityLabel; if (label) { [str appendString:@" "]; [str appendString:label]; } else { [str appendString:RCTRecursiveAccessibilityLabel(subview)]; } } return str; } @implementation RCTView { UIColor *_backgroundColor; } @synthesize reactZIndex = _reactZIndex; - (instancetype)initWithFrame:(CGRect)frame { if ((self = [super initWithFrame:frame])) { _borderWidth = -1; _borderTopWidth = -1; _borderRightWidth = -1; _borderBottomWidth = -1; _borderLeftWidth = -1; _borderTopLeftRadius = -1; _borderTopRightRadius = -1; _borderBottomLeftRadius = -1; _borderBottomRightRadius = -1; _borderStyle = RCTBorderStyleSolid; _hitTestEdgeInsets = UIEdgeInsetsZero; _backgroundColor = super.backgroundColor; } return self; } RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused) - (NSString *)accessibilityLabel { if (super.accessibilityLabel) { return super.accessibilityLabel; } return RCTRecursiveAccessibilityLabel(self); } - (void)setPointerEvents:(RCTPointerEvents)pointerEvents { _pointerEvents = pointerEvents; self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone); if (pointerEvents == RCTPointerEventsBoxNone) { self.accessibilityViewIsModal = NO; } } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]); if(!canReceiveTouchEvents) { return nil; } // `hitSubview` is the topmost subview which was hit. The hit point can // be outside the bounds of `view` (e.g., if -clipsToBounds is NO). UIView *hitSubview = nil; BOOL isPointInside = [self pointInside:point withEvent:event]; BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly); if (needsHitSubview && (![self clipsToBounds] || isPointInside)) { // 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]) { CGPoint convertedPoint = [subview convertPoint:point fromView:self]; hitSubview = [subview hitTest:convertedPoint withEvent:event]; if (hitSubview != nil) { break; } } } UIView *hitView = (isPointInside ? self : nil); switch (_pointerEvents) { case RCTPointerEventsNone: return nil; case RCTPointerEventsUnspecified: return hitSubview ?: hitView; case RCTPointerEventsBoxOnly: return hitView; case RCTPointerEventsBoxNone: return hitSubview; default: RCTLogError(@"Invalid pointer-events specified %zd on %@", _pointerEvents, self); return hitSubview ?: hitView; } } - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) { return [super pointInside:point withEvent:event]; } CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets); return CGRectContainsPoint(hitFrame, point); } - (BOOL)accessibilityActivate { if (_onAccessibilityTap) { _onAccessibilityTap(nil); return YES; } else { return NO; } } - (BOOL)accessibilityPerformMagicTap { if (_onMagicTap) { _onMagicTap(nil); return YES; } else { return NO; } } - (void)didUpdateReactSubviews { if (!self.rct_nextClippingView && !self.rct_removesClippedSubviews) { [super didUpdateReactSubviews]; return; } UIView *rct_nextClippingViewForSubviews = self.rct_removesClippedSubviews ? self : self.rct_nextClippingView; [self rct_updateSubviewsWithNextClippingView:rct_nextClippingViewForSubviews]; CGRect clippingRect = [self rct_activeClippingRect]; if (!CGRectIsNull(clippingRect)) { [self rct_clipSubviewsWithAncestralClipRect:clippingRect]; } } - (NSString *)description { NSString *superDescription = super.description; NSRange semicolonRange = [superDescription rangeOfString:@";"]; NSString *replacement = [NSString stringWithFormat:@"; reactTag: %@;", self.reactTag]; return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement]; } #pragma mark - Statics for dealing with layoutGuides + (void)autoAdjustInsetsForView:(UIView *)parentView withScrollView:(UIScrollView *)scrollView updateOffset:(BOOL)updateOffset { UIEdgeInsets baseInset = parentView.contentInset; CGFloat previousInsetTop = scrollView.contentInset.top; CGPoint contentOffset = scrollView.contentOffset; if (parentView.automaticallyAdjustContentInsets) { UIEdgeInsets autoInset = [self contentInsetsForView:parentView]; baseInset.top += autoInset.top; baseInset.bottom += autoInset.bottom; baseInset.left += autoInset.left; baseInset.right += autoInset.right; } scrollView.contentInset = baseInset; scrollView.scrollIndicatorInsets = baseInset; if (updateOffset) { // If we're adjusting the top inset, then let's also adjust the contentOffset so that the view // elements above the top guide do not cover the content. // This is generally only needed when your views are initially laid out, for // manual changes to contentOffset, you can optionally disable this step CGFloat currentInsetTop = scrollView.contentInset.top; if (currentInsetTop != previousInsetTop) { contentOffset.y -= (currentInsetTop - previousInsetTop); scrollView.contentOffset = contentOffset; } } } + (UIEdgeInsets)contentInsetsForView:(UIView *)view { while (view) { UIViewController *controller = view.reactViewController; if (controller) { return (UIEdgeInsets){ controller.topLayoutGuide.length, 0, controller.bottomLayoutGuide.length, 0 }; } view = view.superview; } return UIEdgeInsetsZero; } #pragma mark - Borders - (UIColor *)backgroundColor { return _backgroundColor; } - (void)setBackgroundColor:(UIColor *)backgroundColor { if ([_backgroundColor isEqual:backgroundColor]) { return; } _backgroundColor = backgroundColor; [self.layer setNeedsDisplay]; } - (UIEdgeInsets)bordersAsInsets { const CGFloat borderWidth = MAX(0, _borderWidth); return (UIEdgeInsets) { _borderTopWidth >= 0 ? _borderTopWidth : borderWidth, _borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth, _borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth, _borderRightWidth >= 0 ? _borderRightWidth : borderWidth, }; } - (RCTCornerRadii)cornerRadii { // Get corner radii const CGFloat radius = MAX(0, _borderRadius); const CGFloat topLeftRadius = _borderTopLeftRadius >= 0 ? _borderTopLeftRadius : radius; const CGFloat topRightRadius = _borderTopRightRadius >= 0 ? _borderTopRightRadius : radius; const CGFloat bottomLeftRadius = _borderBottomLeftRadius >= 0 ? _borderBottomLeftRadius : radius; const CGFloat bottomRightRadius = _borderBottomRightRadius >= 0 ? _borderBottomRightRadius : radius; // Get scale factors required to prevent radii from overlapping const CGSize size = self.bounds.size; const CGFloat topScaleFactor = RCTZeroIfNaN(MIN(1, size.width / (topLeftRadius + topRightRadius))); const CGFloat bottomScaleFactor = RCTZeroIfNaN(MIN(1, size.width / (bottomLeftRadius + bottomRightRadius))); const CGFloat rightScaleFactor = RCTZeroIfNaN(MIN(1, size.height / (topRightRadius + bottomRightRadius))); const CGFloat leftScaleFactor = RCTZeroIfNaN(MIN(1, size.height / (topLeftRadius + bottomLeftRadius))); // Return scaled radii return (RCTCornerRadii){ topLeftRadius * MIN(topScaleFactor, leftScaleFactor), topRightRadius * MIN(topScaleFactor, rightScaleFactor), bottomLeftRadius * MIN(bottomScaleFactor, leftScaleFactor), bottomRightRadius * MIN(bottomScaleFactor, rightScaleFactor), }; } - (RCTBorderColors)borderColors { return (RCTBorderColors){ _borderTopColor ?: _borderColor, _borderLeftColor ?: _borderColor, _borderBottomColor ?: _borderColor, _borderRightColor ?: _borderColor, }; } - (void)reactSetFrame:(CGRect)frame { // If frame is zero, or below the threshold where the border radii can // be rendered as a stretchable image, we'll need to re-render. // TODO: detect up-front if re-rendering is necessary CGSize oldSize = self.bounds.size; [super reactSetFrame:frame]; // When the frame changes, our view needs to reclip itself with its parent, // and also clip any of its own subviews if `rct_removesClippedSubviews` is turned on. [self rct_reclip]; if (!CGSizeEqualToSize(self.bounds.size, oldSize)) { [self.layer setNeedsDisplay]; } } - (void)displayLayer:(CALayer *)layer { if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) { return; } RCTUpdateShadowPathForView(self); const RCTCornerRadii cornerRadii = [self cornerRadii]; const UIEdgeInsets borderInsets = [self bordersAsInsets]; const RCTBorderColors borderColors = [self borderColors]; BOOL useIOSBorderRendering = !RCTRunningInTestEnvironment() && RCTCornerRadiiAreEqual(cornerRadii) && RCTBorderInsetsAreEqual(borderInsets) && RCTBorderColorsAreEqual(borderColors) && _borderStyle == RCTBorderStyleSolid && // iOS draws borders in front of the content whereas CSS draws them behind // the content. For this reason, only use iOS border drawing when clipping // or when the border is hidden. (borderInsets.top == 0 || (borderColors.top && CGColorGetAlpha(borderColors.top) == 0) || self.clipsToBounds); // iOS clips to the outside of the border, but CSS clips to the inside. To // solve this, we'll need to add a container view inside the main view to // correctly clip the subviews. if (useIOSBorderRendering) { layer.cornerRadius = cornerRadii.topLeft; layer.borderColor = borderColors.left; layer.borderWidth = borderInsets.left; layer.backgroundColor = _backgroundColor.CGColor; layer.contents = nil; layer.needsDisplayOnBoundsChange = NO; layer.mask = nil; return; } UIImage *image = RCTGetBorderImage(_borderStyle, layer.bounds.size, cornerRadii, borderInsets, borderColors, _backgroundColor.CGColor, self.clipsToBounds); layer.backgroundColor = NULL; if (image == nil) { layer.contents = nil; layer.needsDisplayOnBoundsChange = NO; return; } CGRect contentsCenter = ({ CGSize size = image.size; UIEdgeInsets insets = image.capInsets; CGRectMake( insets.left / size.width, insets.top / size.height, 1.0 / size.width, 1.0 / size.height ); }); if (RCTRunningInTestEnvironment()) { const CGSize size = self.bounds.size; UIGraphicsBeginImageContextWithOptions(size, NO, image.scale); [image drawInRect:(CGRect){CGPointZero, size}]; image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); contentsCenter = CGRectMake(0, 0, 1, 1); } layer.contents = (id)image.CGImage; layer.contentsScale = image.scale; layer.needsDisplayOnBoundsChange = YES; layer.magnificationFilter = kCAFilterNearest; const BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); if (isResizable) { layer.contentsCenter = contentsCenter; } else { layer.contentsCenter = CGRectMake(0.0, 0.0, 1.0, 1.0); } [self updateClippingForLayer:layer]; } static BOOL RCTLayerHasShadow(CALayer *layer) { return layer.shadowOpacity * CGColorGetAlpha(layer.shadowColor) > 0; } - (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor { // Inherit background color if a shadow has been set, as an optimization if (RCTLayerHasShadow(self.layer)) { self.backgroundColor = inheritedBackgroundColor; } } static void RCTUpdateShadowPathForView(RCTView *view) { if (RCTLayerHasShadow(view.layer)) { if (CGColorGetAlpha(view.backgroundColor.CGColor) > 0.999) { // If view has a solid background color, calculate shadow path from border const RCTCornerRadii cornerRadii = [view cornerRadii]; const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero); CGPathRef shadowPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL); view.layer.shadowPath = shadowPath; CGPathRelease(shadowPath); } else { // Can't accurately calculate box shadow, so fall back to pixel-based shadow view.layer.shadowPath = nil; RCTLogWarn(@"View #%@ of type %@ has a shadow set but cannot calculate " "shadow efficiently. Consider setting a background color to " "fix this, or apply the shadow to a more specific component.", view.reactTag, [view class]); } } } - (void)updateClippingForLayer:(CALayer *)layer { CALayer *mask = nil; CGFloat cornerRadius = 0; if (self.clipsToBounds) { const RCTCornerRadii cornerRadii = [self cornerRadii]; if (RCTCornerRadiiAreEqual(cornerRadii)) { cornerRadius = cornerRadii.topLeft; } else { CAShapeLayer *shapeLayer = [CAShapeLayer layer]; CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL); shapeLayer.path = path; CGPathRelease(path); mask = shapeLayer; } } layer.cornerRadius = cornerRadius; layer.mask = mask; } #pragma mark Border Color #define setBorderColor(side) \ - (void)setBorder##side##Color:(CGColorRef)color \ { \ if (CGColorEqualToColor(_border##side##Color, color)) { \ return; \ } \ CGColorRelease(_border##side##Color); \ _border##side##Color = CGColorRetain(color); \ [self.layer setNeedsDisplay]; \ } setBorderColor() setBorderColor(Top) setBorderColor(Right) setBorderColor(Bottom) setBorderColor(Left) #pragma mark - Border Width #define setBorderWidth(side) \ - (void)setBorder##side##Width:(CGFloat)width \ { \ if (_border##side##Width == width) { \ return; \ } \ _border##side##Width = width; \ [self.layer setNeedsDisplay]; \ } setBorderWidth() setBorderWidth(Top) setBorderWidth(Right) setBorderWidth(Bottom) setBorderWidth(Left) #pragma mark - Border Radius #define setBorderRadius(side) \ - (void)setBorder##side##Radius:(CGFloat)radius \ { \ if (_border##side##Radius == radius) { \ return; \ } \ _border##side##Radius = radius; \ [self.layer setNeedsDisplay]; \ } setBorderRadius() setBorderRadius(TopLeft) setBorderRadius(TopRight) setBorderRadius(BottomLeft) setBorderRadius(BottomRight) #pragma mark - Border Style #define setBorderStyle(side) \ - (void)setBorder##side##Style:(RCTBorderStyle)style \ { \ if (_border##side##Style == style) { \ return; \ } \ _border##side##Style = style; \ [self.layer setNeedsDisplay]; \ } setBorderStyle() - (void)dealloc { CGColorRelease(_borderColor); CGColorRelease(_borderTopColor); CGColorRelease(_borderRightColor); CGColorRelease(_borderBottomColor); CGColorRelease(_borderLeftColor); } @end