/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #import "RCTViewComponentView.h" #import #import #import #import #import #import #import "RCTConversions.h" using namespace facebook::react; @implementation RCTViewComponentView { UIColor *_backgroundColor; CALayer *_borderLayer; } - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { static const auto defaultProps = std::make_shared(); _props = defaultProps; } return self; } - (void)setContentView:(UIView *)contentView { if (_contentView) { [_contentView removeFromSuperview]; } _contentView = contentView; if (_contentView) { [self addSubview:_contentView]; } } - (void)layoutSubviews { [super layoutSubviews]; if (_borderLayer) { _borderLayer.frame = self.layer.bounds; } if (_contentView) { _contentView.frame = RCTCGRectFromRect(_layoutMetrics.getContentFrame()); } } - (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); } - (UIColor *)backgroundColor { return _backgroundColor; } - (void)setBackgroundColor:(UIColor *)backgroundColor { _backgroundColor = backgroundColor; } #pragma mark - RCTComponentViewProtocol + (ComponentHandle)componentHandle { RCTAssert( self == [RCTViewComponentView class], @"`+[RCTComponentViewProtocol componentHandle]` must be implemented for all subclasses (and `%@` particularly).", NSStringFromClass([self class])); return ViewShadowNode::Handle(); } - (void)updateProps:(SharedProps)props oldProps:(SharedProps)oldProps { #ifndef NS_BLOCK_ASSERTIONS auto propsRawPtr = _props.get(); RCTAssert( propsRawPtr && ( [self class] == [RCTViewComponentView class] || typeid(*propsRawPtr).hash_code() != typeid(const ViewProps).hash_code() ), @"`RCTViewComponentView` subclasses (and `%@` particularly) must setup `_props`" " instance variable with a default value in the constructor.", NSStringFromClass([self class]) ); #endif const auto &oldViewProps = *std::static_pointer_cast(oldProps ?: _props); const auto &newViewProps = *std::static_pointer_cast(props); _props = std::static_pointer_cast(props); BOOL needsInvalidateLayer = NO; // `opacity` if (oldViewProps.opacity != newViewProps.opacity) { self.layer.opacity = (CGFloat)newViewProps.opacity; needsInvalidateLayer = YES; } // `backgroundColor` if (oldViewProps.backgroundColor != newViewProps.backgroundColor) { _backgroundColor = RCTUIColorFromSharedColor(newViewProps.backgroundColor); needsInvalidateLayer = YES; } // `foregroundColor` if (oldViewProps.foregroundColor != newViewProps.foregroundColor) { self.foregroundColor = RCTUIColorFromSharedColor(newViewProps.foregroundColor); } // `shadowColor` if (oldViewProps.shadowColor != newViewProps.shadowColor) { CGColorRef shadowColor = RCTCGColorRefFromSharedColor(newViewProps.shadowColor); self.layer.shadowColor = shadowColor; CGColorRelease(shadowColor); needsInvalidateLayer = YES; } // `shadowOffset` if (oldViewProps.shadowOffset != newViewProps.shadowOffset) { self.layer.shadowOffset = RCTCGSizeFromSize(newViewProps.shadowOffset); needsInvalidateLayer = YES; } // `shadowOpacity` if (oldViewProps.shadowOpacity != newViewProps.shadowOpacity) { self.layer.shadowOpacity = (CGFloat)newViewProps.shadowOpacity; needsInvalidateLayer = YES; } // `shadowRadius` if (oldViewProps.shadowRadius != newViewProps.shadowRadius) { self.layer.shadowRadius = (CGFloat)newViewProps.shadowRadius; needsInvalidateLayer = YES; } // `backfaceVisibility` if (oldViewProps.backfaceVisibility != newViewProps.backfaceVisibility) { self.layer.doubleSided = newViewProps.backfaceVisibility; } // `shouldRasterize` if (oldViewProps.shouldRasterize != newViewProps.shouldRasterize) { self.layer.shouldRasterize = newViewProps.shouldRasterize; self.layer.rasterizationScale = newViewProps.shouldRasterize ? [UIScreen mainScreen].scale : 1.0; } // `pointerEvents` if (oldViewProps.pointerEvents != newViewProps.pointerEvents) { self.userInteractionEnabled = newViewProps.pointerEvents != PointerEventsMode::None; } // `transform` if (oldViewProps.transform != newViewProps.transform) { self.layer.transform = RCTCATransform3DFromTransformMatrix(newViewProps.transform); self.layer.allowsEdgeAntialiasing = newViewProps.transform != Transform::Identity(); } // `hitSlop` if (oldViewProps.hitSlop != newViewProps.hitSlop) { self.hitTestEdgeInsets = RCTUIEdgeInsetsFromEdgeInsets(newViewProps.hitSlop); } // `overflow` if (oldViewProps.yogaStyle.overflow != newViewProps.yogaStyle.overflow) { self.clipsToBounds = newViewProps.yogaStyle.overflow != YGOverflowVisible; needsInvalidateLayer = YES; } // `zIndex` if (oldViewProps.zIndex != newViewProps.zIndex) { self.layer.zPosition = (CGFloat)newViewProps.zIndex; } // `border` if ( oldViewProps.borderStyles != newViewProps.borderStyles || oldViewProps.borderRadii != newViewProps.borderRadii || oldViewProps.borderColors != newViewProps.borderColors ) { needsInvalidateLayer = YES; } // `nativeId` if (oldViewProps.nativeId != newViewProps.nativeId) { self.nativeId = RCTNSStringFromStringNilIfEmpty(newViewProps.nativeId); } // `accessible` if (oldViewProps.accessible != newViewProps.accessible) { self.accessibilityElement.isAccessibilityElement = newViewProps.accessible; } // `accessibilityLabel` if (oldViewProps.accessibilityLabel != newViewProps.accessibilityLabel) { self.accessibilityElement.accessibilityLabel = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityLabel); } // `accessibilityHint` if (oldViewProps.accessibilityHint != newViewProps.accessibilityHint) { self.accessibilityElement.accessibilityHint = RCTNSStringFromStringNilIfEmpty(newViewProps.accessibilityHint); } // `accessibilityTraits` if (oldViewProps.accessibilityTraits != newViewProps.accessibilityTraits) { self.accessibilityElement.accessibilityTraits = RCTUIAccessibilityTraitsFromAccessibilityTraits(newViewProps.accessibilityTraits); } // `accessibilityViewIsModal` if (oldViewProps.accessibilityViewIsModal != newViewProps.accessibilityViewIsModal) { self.accessibilityElement.accessibilityViewIsModal = newViewProps.accessibilityViewIsModal; } // `accessibilityElementsHidden` if (oldViewProps.accessibilityElementsHidden != newViewProps.accessibilityElementsHidden) { self.accessibilityElement.accessibilityElementsHidden = newViewProps.accessibilityElementsHidden; } // `accessibilityIgnoresInvertColors` if (oldViewProps.accessibilityIgnoresInvertColors != newViewProps.accessibilityIgnoresInvertColors) { #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000 /* __IPHONE_11_0 */ if (@available(iOS 11.0, *)) { self.accessibilityIgnoresInvertColors = newViewProps.accessibilityIgnoresInvertColors; } #endif } if (needsInvalidateLayer) { [self invalidateLayer]; } } - (void)updateEventEmitter:(SharedEventEmitter)eventEmitter { assert(std::dynamic_pointer_cast(eventEmitter)); _eventEmitter = std::static_pointer_cast(eventEmitter); } - (void)updateLayoutMetrics:(LayoutMetrics)layoutMetrics oldLayoutMetrics:(LayoutMetrics)oldLayoutMetrics { _layoutMetrics = layoutMetrics; [super updateLayoutMetrics:layoutMetrics oldLayoutMetrics:oldLayoutMetrics]; [self invalidateLayer]; } - (void)prepareForRecycle { [super prepareForRecycle]; _eventEmitter.reset(); } - (UIView *)betterHitTest:(CGPoint)point withEvent:(UIEvent *)event { // This is a classic textbook implementation of `hitTest:` with a couple of improvements: // * It takes layers' `zIndex` property into an account; // * It does not stop algorithm if some touch is outside the view // which does not have `clipToBounds` enabled. if (!self.userInteractionEnabled || self.hidden || self.alpha < 0.01) { return nil; } BOOL isPointInside = [self pointInside:point withEvent:event]; if (self.clipsToBounds && !isPointInside) { return nil; } NSArray<__kindof UIView *> *sortedSubviews = [self.subviews sortedArrayUsingComparator:^NSComparisonResult(UIView *a, UIView *b) { // Ensure sorting is stable by treating equal `zIndex` as ascending so // that original order is preserved. return a.layer.zPosition > b.layer.zPosition ? NSOrderedDescending : NSOrderedAscending; }]; for (UIView *subview in [sortedSubviews reverseObjectEnumerator]) { UIView *hitView = [subview hitTest:[subview convertPoint:point fromView:self] withEvent:event]; if (hitView) { return hitView; } } return isPointInside ? self : nil; } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { switch (_props->pointerEvents) { case PointerEventsMode::Auto: return [self betterHitTest:point withEvent:event]; case PointerEventsMode::None: return nil; case PointerEventsMode::BoxOnly: return [self pointInside:point withEvent:event] ? self : nil; case PointerEventsMode::BoxNone: UIView *view = [self betterHitTest:point withEvent:event]; return view != self ? view : nil; } } static RCTCornerRadii RCTCornerRadiiFromBorderRadii(BorderRadii borderRadii) { return RCTCornerRadii { .topLeft = (CGFloat)borderRadii.topLeft, .topRight = (CGFloat)borderRadii.topRight, .bottomLeft = (CGFloat)borderRadii.bottomLeft, .bottomRight = (CGFloat)borderRadii.bottomRight }; } static RCTBorderColors RCTBorderColorsFromBorderColors(BorderColors borderColors) { return RCTBorderColors { .left = RCTCGColorRefFromSharedColor(borderColors.left), .top = RCTCGColorRefFromSharedColor(borderColors.top), .bottom = RCTCGColorRefFromSharedColor(borderColors.bottom), .right = RCTCGColorRefFromSharedColor(borderColors.right) }; } static UIEdgeInsets UIEdgeInsetsFromBorderInsets(EdgeInsets edgeInsets) { return UIEdgeInsets { .left = (CGFloat)edgeInsets.left, .top = (CGFloat)edgeInsets.top, .bottom = (CGFloat)edgeInsets.bottom, .right = (CGFloat)edgeInsets.right }; } static RCTBorderStyle RCTBorderStyleFromBorderStyle(BorderStyle borderStyle) { switch (borderStyle) { case BorderStyle::Solid: return RCTBorderStyleSolid; case BorderStyle::Dotted: return RCTBorderStyleDotted; case BorderStyle::Dashed: return RCTBorderStyleDashed; } } - (void)invalidateLayer { const auto borderMetrics = _props->resolveBorderMetrics(_layoutMetrics.layoutDirection == LayoutDirection::RightToLeft); CALayer *layer = self.layer; // Stage 1. Shadow Path BOOL layerHasShadow = layer.shadowOpacity > 0 && CGColorGetAlpha(layer.shadowColor) > 0; if (layerHasShadow) { if (CGColorGetAlpha(_backgroundColor.CGColor) > 0.999) { // If view has a solid background color, calculate shadow path from border. const RCTCornerInsets cornerInsets = RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero); CGPathRef shadowPath = RCTPathCreateWithRoundedRect(self.bounds, cornerInsets, nil); layer.shadowPath = shadowPath; CGPathRelease(shadowPath); } else { // Can't accurately calculate box shadow, so fall back to pixel-based shadow. layer.shadowPath = nil; } } else { layer.shadowPath = nil; } // Stage 2. Border Rendering const bool useCoreAnimationBorderRendering = borderMetrics.borderColors.isUniform() && borderMetrics.borderWidths.isUniform() && borderMetrics.borderStyles.isUniform() && borderMetrics.borderRadii.isUniform() && borderMetrics.borderStyles.left == BorderStyle::Solid && ( // 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. borderMetrics.borderWidths.left == 0 || colorComponentsFromColor(borderMetrics.borderColors.left).alpha == 0 || self.clipsToBounds ); if (useCoreAnimationBorderRendering) { if (_borderLayer) { [_borderLayer removeFromSuperlayer]; _borderLayer = nil; } layer.borderWidth = (CGFloat)borderMetrics.borderWidths.left; layer.borderColor = RCTCGColorRefFromSharedColor(borderMetrics.borderColors.left); layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft; layer.backgroundColor = _backgroundColor.CGColor; _contentView.layer.cornerRadius = (CGFloat)borderMetrics.borderRadii.topLeft; _contentView.layer.masksToBounds = YES; } else { if (!_borderLayer) { _borderLayer = [[CALayer alloc] init]; _borderLayer.zPosition = -1024.0f; _borderLayer.frame = layer.bounds; _borderLayer.magnificationFilter = kCAFilterNearest; [layer addSublayer:_borderLayer]; } layer.backgroundColor = nil; layer.borderWidth = 0; layer.borderColor = nil; layer.cornerRadius = 0; _contentView.layer.cornerRadius = 0; _contentView.layer.masksToBounds = NO; UIImage *image = RCTGetBorderImage( RCTBorderStyleFromBorderStyle(borderMetrics.borderStyles.left), layer.bounds.size, RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsFromBorderInsets(borderMetrics.borderWidths), RCTBorderColorsFromBorderColors(borderMetrics.borderColors), _backgroundColor.CGColor, self.clipsToBounds ); if (image == nil) { _borderLayer.contents = nil; } else { CGSize imageSize = image.size; UIEdgeInsets imageCapInsets = image.capInsets; CGRect contentsCenter = CGRect { CGPoint {imageCapInsets.left / imageSize.width, imageCapInsets.top / imageSize.height}, CGSize {(CGFloat)1.0 / imageSize.width, (CGFloat)1.0 / imageSize.height} }; _borderLayer.contents = (id)image.CGImage; _borderLayer.contentsScale = image.scale; const BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); if (isResizable) { _borderLayer.contentsCenter = contentsCenter; } else { _borderLayer.contentsCenter = CGRect { CGPoint {0.0, 0.0}, CGSize {1.0, 1.0}}; } } // Stage 2.5. Custom Clipping Mask CAShapeLayer *maskLayer = nil; CGFloat cornerRadius = 0; if (self.clipsToBounds) { if (borderMetrics.borderRadii.isUniform()) { // In this case we can simply use `cornerRadius` exclusivly. cornerRadius = borderMetrics.borderRadii.topLeft; } else { // In this case we have to generate masking layer manually. CGPathRef path = RCTPathCreateWithRoundedRect( self.bounds, RCTGetCornerInsets(RCTCornerRadiiFromBorderRadii(borderMetrics.borderRadii), UIEdgeInsetsZero), nil ); maskLayer = [CAShapeLayer layer]; maskLayer.path = path; CGPathRelease(path); } } layer.cornerRadius = cornerRadius; layer.mask = maskLayer; } } #pragma mark - Accessibility - (NSObject *)accessibilityElement { return self; } static NSString *RCTRecursiveAccessibilityLabel(UIView *view) { NSMutableString *result = [NSMutableString stringWithString:@""]; for (UIView *subview in view.subviews) { NSString *label = subview.accessibilityLabel; if (!label) { label = RCTRecursiveAccessibilityLabel(subview); } if (label && label.length > 0) { if (result.length > 0) { [result appendString:@" "]; } [result appendString:label]; } } return result; } - (NSString *)accessibilityLabel { NSString *label = super.accessibilityLabel; if (label) { return label; } return RCTRecursiveAccessibilityLabel(self); } #pragma mark - Accessibility Events - (NSArray *)accessibilityCustomActions { const auto &accessibilityActions = _props->accessibilityActions; if (accessibilityActions.size() == 0) { return nil; } NSMutableArray *customActions = [NSMutableArray array]; for (const auto &accessibilityAction : accessibilityActions) { [customActions addObject:[[UIAccessibilityCustomAction alloc] initWithName:RCTNSStringFromString(accessibilityAction) target:self selector:@selector(didActivateAccessibilityCustomAction:)]]; } return [customActions copy]; } - (BOOL)accessibilityActivate { _eventEmitter->onAccessibilityTap(); return YES; } - (BOOL)accessibilityPerformMagicTap { _eventEmitter->onAccessibilityMagicTap(); return YES; } - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action { _eventEmitter->onAccessibilityAction(RCTStringFromNSString(action.name)); return YES; } - (SharedTouchEventEmitter)touchEventEmitterAtPoint:(CGPoint)point { return _eventEmitter; } @end