123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970 |
- /*
- * 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 "RCTView.h"
- #import "RCTAutoInsetsProtocol.h"
- #import "RCTBorderDrawing.h"
- #import "RCTConvert.h"
- #import "RCTI18nUtil.h"
- #import "RCTLog.h"
- #import "RCTUtils.h"
- #import "UIView+React.h"
- UIAccessibilityTraits const SwitchAccessibilityTrait = 0x20000000000001;
- @implementation UIView (RCTViewUnmounting)
- - (void)react_remountAllSubviews
- {
- // Normal views don't support unmounting, so all
- // this does is forward message to our subviews,
- // in case any of those do support it
- for (UIView *subview in self.subviews) {
- [subview react_remountAllSubviews];
- }
- }
- - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
- {
- // Even though we don't support subview unmounting
- // we do support clipsToBounds, so if that's enabled
- // we'll update the clipping
- if (self.clipsToBounds && self.subviews.count > 0) {
- clipRect = [clipView convertRect:clipRect toView:self];
- clipRect = CGRectIntersection(clipRect, self.bounds);
- clipView = self;
- }
- // Normal views don't support unmounting, so all
- // this does is forward message to our subviews,
- // in case any of those do support it
- for (UIView *subview in self.subviews) {
- [subview react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
- }
- }
- - (UIView *)react_findClipView
- {
- UIView *testView = self;
- UIView *clipView = nil;
- CGRect clipRect = self.bounds;
- // We will only look for a clipping view up the view hierarchy until we hit the root view.
- while (testView) {
- if (testView.clipsToBounds) {
- if (clipView) {
- CGRect testRect = [clipView convertRect:clipRect toView:testView];
- if (!CGRectContainsRect(testView.bounds, testRect)) {
- clipView = testView;
- clipRect = CGRectIntersection(testView.bounds, testRect);
- }
- } else {
- clipView = testView;
- clipRect = [self convertRect:self.bounds toView:clipView];
- }
- }
- if ([testView isReactRootView]) {
- break;
- }
- testView = testView.superview;
- }
- return clipView ?: self.window;
- }
- @end
- static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
- {
- NSMutableString *str = [NSMutableString stringWithString:@""];
- for (UIView *subview in view.subviews) {
- NSString *label = subview.accessibilityLabel;
- if (!label) {
- label = RCTRecursiveAccessibilityLabel(subview);
- }
- if (label && label.length > 0) {
- if (str.length > 0) {
- [str appendString:@" "];
- }
- [str appendString:label];
- }
- }
- return str.length == 0 ? nil : str;
- }
- @implementation RCTView {
- UIColor *_backgroundColor;
- NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsNameMap;
- NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsLabelMap;
- }
- - (instancetype)initWithFrame:(CGRect)frame
- {
- if ((self = [super initWithFrame:frame])) {
- _borderWidth = -1;
- _borderTopWidth = -1;
- _borderRightWidth = -1;
- _borderBottomWidth = -1;
- _borderLeftWidth = -1;
- _borderStartWidth = -1;
- _borderEndWidth = -1;
- _borderTopLeftRadius = -1;
- _borderTopRightRadius = -1;
- _borderTopStartRadius = -1;
- _borderTopEndRadius = -1;
- _borderBottomLeftRadius = -1;
- _borderBottomRightRadius = -1;
- _borderBottomStartRadius = -1;
- _borderBottomEndRadius = -1;
- _borderStyle = RCTBorderStyleSolid;
- _hitTestEdgeInsets = UIEdgeInsetsZero;
- _backgroundColor = super.backgroundColor;
- }
- return self;
- }
- RCT_NOT_IMPLEMENTED(-(instancetype)initWithCoder : unused)
- - (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
- {
- if (_reactLayoutDirection != layoutDirection) {
- _reactLayoutDirection = layoutDirection;
- [self.layer setNeedsDisplay];
- }
- if ([self respondsToSelector:@selector(setSemanticContentAttribute:)]) {
- self.semanticContentAttribute = layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight
- ? UISemanticContentAttributeForceLeftToRight
- : UISemanticContentAttributeForceRightToLeft;
- }
- }
- - (NSString *)accessibilityLabel
- {
- NSString *label = super.accessibilityLabel;
- if (label) {
- return label;
- }
- return RCTRecursiveAccessibilityLabel(self);
- }
- - (NSArray<UIAccessibilityCustomAction *> *)accessibilityCustomActions
- {
- if (!self.accessibilityActions.count) {
- return nil;
- }
- accessibilityActionsNameMap = [[NSMutableDictionary alloc] init];
- accessibilityActionsLabelMap = [[NSMutableDictionary alloc] init];
- NSMutableArray *actions = [NSMutableArray array];
- for (NSDictionary *action in self.accessibilityActions) {
- if (action[@"name"]) {
- accessibilityActionsNameMap[action[@"name"]] = action;
- }
- if (action[@"label"]) {
- accessibilityActionsLabelMap[action[@"label"]] = action;
- [actions addObject:[[UIAccessibilityCustomAction alloc]
- initWithName:action[@"label"]
- target:self
- selector:@selector(didActivateAccessibilityCustomAction:)]];
- }
- }
- return [actions copy];
- }
- - (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action
- {
- if (!_onAccessibilityAction || !accessibilityActionsLabelMap) {
- return NO;
- }
- // iOS defines the name as the localized label, so use our map to convert this back to the non-localized action namne
- // when passing to JS. This allows for standard action names across platforms.
- NSDictionary *actionObject = accessibilityActionsLabelMap[action.name];
- if (actionObject) {
- _onAccessibilityAction(@{@"actionName" : actionObject[@"name"], @"actionTarget" : self.reactTag});
- }
- return YES;
- }
- - (NSString *)accessibilityValue
- {
- static dispatch_once_t onceToken;
- static NSDictionary<NSString *, NSString *> *rolesAndStatesDescription = nil;
- dispatch_once(&onceToken, ^{
- NSString *bundlePath = [[NSBundle mainBundle] pathForResource:@"AccessibilityResources" ofType:@"bundle"];
- NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];
- if (bundle) {
- NSURL *url = [bundle URLForResource:@"Localizable" withExtension:@"strings"];
- if (@available(iOS 11.0, *)) {
- rolesAndStatesDescription = [NSDictionary dictionaryWithContentsOfURL:url error:nil];
- } else {
- // Fallback on earlier versions
- rolesAndStatesDescription = [NSDictionary dictionaryWithContentsOfURL:url];
- }
- }
- if (rolesAndStatesDescription == nil) {
- NSLog(@"Cannot load localized accessibility strings.");
- rolesAndStatesDescription = @{
- @"alert" : @"alert",
- @"checkbox" : @"checkbox",
- @"combobox" : @"combo box",
- @"menu" : @"menu",
- @"menubar" : @"menu bar",
- @"menuitem" : @"menu item",
- @"progressbar" : @"progress bar",
- @"radio" : @"radio button",
- @"radiogroup" : @"radio group",
- @"scrollbar" : @"scroll bar",
- @"spinbutton" : @"spin button",
- @"switch" : @"switch",
- @"tab" : @"tab",
- @"tablist" : @"tab list",
- @"timer" : @"timer",
- @"toolbar" : @"tool bar",
- @"checked" : @"checked",
- @"unchecked" : @"not checked",
- @"busy" : @"busy",
- @"expanded" : @"expanded",
- @"collapsed" : @"collapsed",
- @"mixed" : @"mixed",
- };
- }
- });
- if ((self.accessibilityTraits & SwitchAccessibilityTrait) == SwitchAccessibilityTrait) {
- for (NSString *state in self.accessibilityState) {
- id val = self.accessibilityState[state];
- if (!val) {
- continue;
- }
- if ([state isEqualToString:@"checked"] && [val isKindOfClass:[NSNumber class]]) {
- return [val boolValue] ? @"1" : @"0";
- }
- }
- }
- NSMutableArray *valueComponents = [NSMutableArray new];
- NSString *roleDescription = self.accessibilityRole ? rolesAndStatesDescription[self.accessibilityRole] : nil;
- if (roleDescription) {
- [valueComponents addObject:roleDescription];
- }
- for (NSString *state in self.accessibilityState) {
- id val = self.accessibilityState[state];
- if (!val) {
- continue;
- }
- if ([state isEqualToString:@"checked"]) {
- if ([val isKindOfClass:[NSNumber class]]) {
- [valueComponents addObject:rolesAndStatesDescription[[val boolValue] ? @"checked" : @"unchecked"]];
- } else if ([val isKindOfClass:[NSString class]] && [val isEqualToString:@"mixed"]) {
- [valueComponents addObject:rolesAndStatesDescription[@"mixed"]];
- }
- }
- if ([state isEqualToString:@"expanded"] && [val isKindOfClass:[NSNumber class]]) {
- [valueComponents addObject:rolesAndStatesDescription[[val boolValue] ? @"expanded" : @"collapsed"]];
- }
- if ([state isEqualToString:@"busy"] && [val isKindOfClass:[NSNumber class]] && [val boolValue]) {
- [valueComponents addObject:rolesAndStatesDescription[@"busy"]];
- }
- }
- // handle accessibilityValue
- if (self.accessibilityValueInternal) {
- id min = self.accessibilityValueInternal[@"min"];
- id now = self.accessibilityValueInternal[@"now"];
- id max = self.accessibilityValueInternal[@"max"];
- id text = self.accessibilityValueInternal[@"text"];
- if (text && [text isKindOfClass:[NSString class]]) {
- [valueComponents addObject:text];
- } else if (
- [min isKindOfClass:[NSNumber class]] && [now isKindOfClass:[NSNumber class]] &&
- [max isKindOfClass:[NSNumber class]] && ([min intValue] < [max intValue]) &&
- ([min intValue] <= [now intValue] && [now intValue] <= [max intValue])) {
- int val = ([now intValue] * 100) / ([max intValue] - [min intValue]);
- [valueComponents addObject:[NSString stringWithFormat:@"%d percent", val]];
- }
- }
- if (valueComponents.count > 0) {
- return [valueComponents componentsJoinedByString:@", "];
- }
- return nil;
- }
- - (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)) {
- // 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 [sortedSubviews 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 %lld on %@", (long long)_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);
- }
- - (UIView *)reactAccessibilityElement
- {
- return self;
- }
- - (BOOL)isAccessibilityElement
- {
- if (self.reactAccessibilityElement == self) {
- return [super isAccessibilityElement];
- }
- return NO;
- }
- - (BOOL)performAccessibilityAction:(NSString *)name
- {
- if (_onAccessibilityAction && accessibilityActionsNameMap[name]) {
- _onAccessibilityAction(@{@"actionName" : name, @"actionTarget" : self.reactTag});
- return YES;
- }
- return NO;
- }
- - (BOOL)accessibilityActivate
- {
- if ([self performAccessibilityAction:@"activate"]) {
- return YES;
- } else if (_onAccessibilityTap) {
- _onAccessibilityTap(nil);
- return YES;
- } else {
- return NO;
- }
- }
- - (BOOL)accessibilityPerformMagicTap
- {
- if ([self performAccessibilityAction:@"magicTap"]) {
- return YES;
- } else if (_onMagicTap) {
- _onMagicTap(nil);
- return YES;
- } else {
- return NO;
- }
- }
- - (BOOL)accessibilityPerformEscape
- {
- if ([self performAccessibilityAction:@"escape"]) {
- return YES;
- } else if (_onAccessibilityEscape) {
- _onAccessibilityEscape(nil);
- return YES;
- } else {
- return NO;
- }
- }
- - (void)accessibilityIncrement
- {
- [self performAccessibilityAction:@"increment"];
- }
- - (void)accessibilityDecrement
- {
- [self performAccessibilityAction:@"decrement"];
- }
- - (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<RCTAutoInsetsProtocol> *)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 - View unmounting
- - (void)react_remountAllSubviews
- {
- if (_removeClippedSubviews) {
- for (UIView *view in self.reactSubviews) {
- if (view.superview != self) {
- [self addSubview:view];
- [view react_remountAllSubviews];
- }
- }
- } else {
- // If _removeClippedSubviews is false, we must already be showing all subviews
- [super react_remountAllSubviews];
- }
- }
- - (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
- {
- // TODO (#5906496): for scrollviews (the primary use-case) we could
- // optimize this by only doing a range check along the scroll axis,
- // instead of comparing the whole frame
- if (!_removeClippedSubviews) {
- // Use default behavior if unmounting is disabled
- return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
- }
- if (self.reactSubviews.count == 0) {
- // Do nothing if we have no subviews
- return;
- }
- if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
- // Do nothing if layout hasn't happened yet
- return;
- }
- // Convert clipping rect to local coordinates
- clipRect = [clipView convertRect:clipRect toView:self];
- clipRect = CGRectIntersection(clipRect, self.bounds);
- clipView = self;
- // Mount / unmount views
- for (UIView *view in self.reactSubviews) {
- if (!CGSizeEqualToSize(CGRectIntersection(clipRect, view.frame).size, CGSizeZero)) {
- // View is at least partially visible, so remount it if unmounted
- [self addSubview:view];
- // Then test its subviews
- if (CGRectContainsRect(clipRect, view.frame)) {
- // View is fully visible, so remount all subviews
- [view react_remountAllSubviews];
- } else {
- // View is partially visible, so update clipped subviews
- [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
- }
- } else if (view.superview) {
- // View is completely outside the clipRect, so unmount it
- [view removeFromSuperview];
- }
- }
- }
- - (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews
- {
- if (!removeClippedSubviews && _removeClippedSubviews) {
- [self react_remountAllSubviews];
- }
- _removeClippedSubviews = removeClippedSubviews;
- }
- - (void)didUpdateReactSubviews
- {
- if (_removeClippedSubviews) {
- [self updateClippedSubviews];
- } else {
- [super didUpdateReactSubviews];
- }
- }
- - (void)updateClippedSubviews
- {
- // Find a suitable view to use for clipping
- UIView *clipView = [self react_findClipView];
- if (clipView) {
- [self react_updateClippedSubviewsWithClipRect:clipView.bounds relativeToView:clipView];
- }
- }
- - (void)layoutSubviews
- {
- // TODO (#5906496): this a nasty performance drain, but necessary
- // to prevent gaps appearing when the loading spinner disappears.
- // We might be able to fix this another way by triggering a call
- // to updateClippedSubviews manually after loading
- [super layoutSubviews];
- if (_removeClippedSubviews) {
- [self updateClippedSubviews];
- }
- }
- - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
- {
- [super traitCollectionDidChange:previousTraitCollection];
- #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
- if (@available(iOS 13.0, *)) {
- if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) {
- [self.layer setNeedsDisplay];
- }
- }
- #endif
- }
- #pragma mark - Borders
- - (UIColor *)backgroundColor
- {
- return _backgroundColor;
- }
- - (void)setBackgroundColor:(UIColor *)backgroundColor
- {
- if ([_backgroundColor isEqual:backgroundColor]) {
- return;
- }
- _backgroundColor = backgroundColor;
- [self.layer setNeedsDisplay];
- }
- static CGFloat RCTDefaultIfNegativeTo(CGFloat defaultValue, CGFloat x)
- {
- return x >= 0 ? x : defaultValue;
- };
- - (UIEdgeInsets)bordersAsInsets
- {
- const CGFloat borderWidth = MAX(0, _borderWidth);
- const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
- if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) {
- const CGFloat borderStartWidth = RCTDefaultIfNegativeTo(_borderLeftWidth, _borderStartWidth);
- const CGFloat borderEndWidth = RCTDefaultIfNegativeTo(_borderRightWidth, _borderEndWidth);
- const CGFloat directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth;
- const CGFloat directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth;
- return (UIEdgeInsets){
- RCTDefaultIfNegativeTo(borderWidth, _borderTopWidth),
- RCTDefaultIfNegativeTo(borderWidth, directionAwareBorderLeftWidth),
- RCTDefaultIfNegativeTo(borderWidth, _borderBottomWidth),
- RCTDefaultIfNegativeTo(borderWidth, directionAwareBorderRightWidth),
- };
- }
- const CGFloat directionAwareBorderLeftWidth = isRTL ? _borderEndWidth : _borderStartWidth;
- const CGFloat directionAwareBorderRightWidth = isRTL ? _borderStartWidth : _borderEndWidth;
- return (UIEdgeInsets){
- RCTDefaultIfNegativeTo(borderWidth, _borderTopWidth),
- RCTDefaultIfNegativeTo(borderWidth, RCTDefaultIfNegativeTo(_borderLeftWidth, directionAwareBorderLeftWidth)),
- RCTDefaultIfNegativeTo(borderWidth, _borderBottomWidth),
- RCTDefaultIfNegativeTo(borderWidth, RCTDefaultIfNegativeTo(_borderRightWidth, directionAwareBorderRightWidth)),
- };
- }
- - (RCTCornerRadii)cornerRadii
- {
- const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
- const CGFloat radius = MAX(0, _borderRadius);
- CGFloat topLeftRadius;
- CGFloat topRightRadius;
- CGFloat bottomLeftRadius;
- CGFloat bottomRightRadius;
- if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) {
- const CGFloat topStartRadius = RCTDefaultIfNegativeTo(_borderTopLeftRadius, _borderTopStartRadius);
- const CGFloat topEndRadius = RCTDefaultIfNegativeTo(_borderTopRightRadius, _borderTopEndRadius);
- const CGFloat bottomStartRadius = RCTDefaultIfNegativeTo(_borderBottomLeftRadius, _borderBottomStartRadius);
- const CGFloat bottomEndRadius = RCTDefaultIfNegativeTo(_borderBottomRightRadius, _borderBottomEndRadius);
- const CGFloat directionAwareTopLeftRadius = isRTL ? topEndRadius : topStartRadius;
- const CGFloat directionAwareTopRightRadius = isRTL ? topStartRadius : topEndRadius;
- const CGFloat directionAwareBottomLeftRadius = isRTL ? bottomEndRadius : bottomStartRadius;
- const CGFloat directionAwareBottomRightRadius = isRTL ? bottomStartRadius : bottomEndRadius;
- topLeftRadius = RCTDefaultIfNegativeTo(radius, directionAwareTopLeftRadius);
- topRightRadius = RCTDefaultIfNegativeTo(radius, directionAwareTopRightRadius);
- bottomLeftRadius = RCTDefaultIfNegativeTo(radius, directionAwareBottomLeftRadius);
- bottomRightRadius = RCTDefaultIfNegativeTo(radius, directionAwareBottomRightRadius);
- } else {
- const CGFloat directionAwareTopLeftRadius = isRTL ? _borderTopEndRadius : _borderTopStartRadius;
- const CGFloat directionAwareTopRightRadius = isRTL ? _borderTopStartRadius : _borderTopEndRadius;
- const CGFloat directionAwareBottomLeftRadius = isRTL ? _borderBottomEndRadius : _borderBottomStartRadius;
- const CGFloat directionAwareBottomRightRadius = isRTL ? _borderBottomStartRadius : _borderBottomEndRadius;
- topLeftRadius =
- RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderTopLeftRadius, directionAwareTopLeftRadius));
- topRightRadius =
- RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderTopRightRadius, directionAwareTopRightRadius));
- bottomLeftRadius =
- RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderBottomLeftRadius, directionAwareBottomLeftRadius));
- bottomRightRadius = RCTDefaultIfNegativeTo(
- radius, RCTDefaultIfNegativeTo(_borderBottomRightRadius, directionAwareBottomRightRadius));
- }
- // 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
- {
- const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
- if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) {
- const CGColorRef borderStartColor = _borderStartColor ?: _borderLeftColor;
- const CGColorRef borderEndColor = _borderEndColor ?: _borderRightColor;
- const CGColorRef directionAwareBorderLeftColor = isRTL ? borderEndColor : borderStartColor;
- const CGColorRef directionAwareBorderRightColor = isRTL ? borderStartColor : borderEndColor;
- return (RCTBorderColors){
- _borderTopColor ?: _borderColor,
- directionAwareBorderLeftColor ?: _borderColor,
- _borderBottomColor ?: _borderColor,
- directionAwareBorderRightColor ?: _borderColor,
- };
- }
- const CGColorRef directionAwareBorderLeftColor = isRTL ? _borderEndColor : _borderStartColor;
- const CGColorRef directionAwareBorderRightColor = isRTL ? _borderStartColor : _borderEndColor;
- return (RCTBorderColors){
- _borderTopColor ?: _borderColor,
- directionAwareBorderLeftColor ?: _borderLeftColor ?: _borderColor,
- _borderBottomColor ?: _borderColor,
- directionAwareBorderRightColor ?: _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];
- 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 = 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.
- CGColorRef backgroundColor;
- #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
- if (@available(iOS 13.0, *)) {
- backgroundColor = [_backgroundColor resolvedColorWithTraitCollection:self.traitCollection].CGColor;
- } else {
- backgroundColor = _backgroundColor.CGColor;
- }
- #else
- backgroundColor = _backgroundColor.CGColor;
- #endif
- if (useIOSBorderRendering) {
- layer.cornerRadius = cornerRadii.topLeft;
- layer.borderColor = borderColors.left;
- layer.borderWidth = borderInsets.left;
- layer.backgroundColor = backgroundColor;
- layer.contents = nil;
- layer.needsDisplayOnBoundsChange = NO;
- layer.mask = nil;
- return;
- }
- UIImage *image = RCTGetBorderImage(
- _borderStyle, layer.bounds.size, cornerRadii, borderInsets, borderColors, backgroundColor, 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, (CGFloat)1.0 / size.width, (CGFloat)1.0 / size.height);
- });
- 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;
- }
- 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;
- RCTLogAdvice(
- @"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)
- setBorderColor(Start) setBorderColor(End)
- #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)
- setBorderWidth(Start) setBorderWidth(End)
- #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(TopStart)
- setBorderRadius(TopEnd) setBorderRadius(BottomLeft) setBorderRadius(BottomRight)
- setBorderRadius(BottomStart) setBorderRadius(BottomEnd)
- #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);
- CGColorRelease(_borderStartColor);
- CGColorRelease(_borderEndColor);
- }
- @end
|