UNPKG

32.9 kBPlain TextView Raw
1/*
2 * Copyright (c) Facebook, Inc. and its affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8#import "RCTView.h"
9
10#import "RCTAutoInsetsProtocol.h"
11#import "RCTBorderDrawing.h"
12#import "RCTConvert.h"
13#import "RCTLog.h"
14#import "RCTUtils.h"
15#import "UIView+React.h"
16#import "RCTI18nUtil.h"
17
18UIAccessibilityTraits const SwitchAccessibilityTrait = 0x20000000000001;
19
20@implementation UIView (RCTViewUnmounting)
21
22- (void)react_remountAllSubviews
23{
24 // Normal views don't support unmounting, so all
25 // this does is forward message to our subviews,
26 // in case any of those do support it
27
28 for (UIView *subview in self.subviews) {
29 [subview react_remountAllSubviews];
30 }
31}
32
33- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
34{
35 // Even though we don't support subview unmounting
36 // we do support clipsToBounds, so if that's enabled
37 // we'll update the clipping
38
39 if (self.clipsToBounds && self.subviews.count > 0) {
40 clipRect = [clipView convertRect:clipRect toView:self];
41 clipRect = CGRectIntersection(clipRect, self.bounds);
42 clipView = self;
43 }
44
45 // Normal views don't support unmounting, so all
46 // this does is forward message to our subviews,
47 // in case any of those do support it
48
49 for (UIView *subview in self.subviews) {
50 [subview react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
51 }
52}
53
54- (UIView *)react_findClipView
55{
56 UIView *testView = self;
57 UIView *clipView = nil;
58 CGRect clipRect = self.bounds;
59 // We will only look for a clipping view up the view hierarchy until we hit the root view.
60 while (testView) {
61 if (testView.clipsToBounds) {
62 if (clipView) {
63 CGRect testRect = [clipView convertRect:clipRect toView:testView];
64 if (!CGRectContainsRect(testView.bounds, testRect)) {
65 clipView = testView;
66 clipRect = CGRectIntersection(testView.bounds, testRect);
67 }
68 } else {
69 clipView = testView;
70 clipRect = [self convertRect:self.bounds toView:clipView];
71 }
72 }
73 if ([testView isReactRootView]) {
74 break;
75 }
76 testView = testView.superview;
77 }
78 return clipView ?: self.window;
79}
80
81@end
82
83static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
84{
85 NSMutableString *str = [NSMutableString stringWithString:@""];
86 for (UIView *subview in view.subviews) {
87 NSString *label = subview.accessibilityLabel;
88 if (!label) {
89 label = RCTRecursiveAccessibilityLabel(subview);
90 }
91 if (label && label.length > 0) {
92 if (str.length > 0) {
93 [str appendString:@" "];
94 }
95 [str appendString:label];
96 }
97 }
98 return str.length == 0 ? nil : str;
99}
100
101@implementation RCTView
102{
103 UIColor *_backgroundColor;
104 NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsNameMap;
105 NSMutableDictionary<NSString *, NSDictionary *> *accessibilityActionsLabelMap;
106}
107
108- (instancetype)initWithFrame:(CGRect)frame
109{
110 if ((self = [super initWithFrame:frame])) {
111 _borderWidth = -1;
112 _borderTopWidth = -1;
113 _borderRightWidth = -1;
114 _borderBottomWidth = -1;
115 _borderLeftWidth = -1;
116 _borderStartWidth = -1;
117 _borderEndWidth = -1;
118 _borderTopLeftRadius = -1;
119 _borderTopRightRadius = -1;
120 _borderTopStartRadius = -1;
121 _borderTopEndRadius = -1;
122 _borderBottomLeftRadius = -1;
123 _borderBottomRightRadius = -1;
124 _borderBottomStartRadius = -1;
125 _borderBottomEndRadius = -1;
126 _borderStyle = RCTBorderStyleSolid;
127 _hitTestEdgeInsets = UIEdgeInsetsZero;
128
129 _backgroundColor = super.backgroundColor;
130 }
131
132 return self;
133}
134
135RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
136
137- (void)setReactLayoutDirection:(UIUserInterfaceLayoutDirection)layoutDirection
138{
139 if (_reactLayoutDirection != layoutDirection) {
140 _reactLayoutDirection = layoutDirection;
141 [self.layer setNeedsDisplay];
142 }
143
144 if ([self respondsToSelector:@selector(setSemanticContentAttribute:)]) {
145 self.semanticContentAttribute =
146 layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight ?
147 UISemanticContentAttributeForceLeftToRight :
148 UISemanticContentAttributeForceRightToLeft;
149 }
150}
151
152- (NSString *)accessibilityLabel
153{
154 NSString *label = super.accessibilityLabel;
155 if (label) {
156 return label;
157 }
158 return RCTRecursiveAccessibilityLabel(self);
159}
160
161- (NSArray <UIAccessibilityCustomAction *> *)accessibilityCustomActions
162{
163 if (!self.accessibilityActions.count) {
164 return nil;
165 }
166
167 accessibilityActionsNameMap = [[NSMutableDictionary alloc] init];
168 accessibilityActionsLabelMap = [[NSMutableDictionary alloc] init];
169 NSMutableArray *actions = [NSMutableArray array];
170 for (NSDictionary *action in self.accessibilityActions) {
171 if (action[@"name"]) {
172 accessibilityActionsNameMap[action[@"name"]] = action;
173 }
174 if (action[@"label"]) {
175 accessibilityActionsLabelMap[action[@"label"]] = action;
176 [actions addObject:[[UIAccessibilityCustomAction alloc] initWithName:action[@"label"]
177 target:self
178 selector:@selector(didActivateAccessibilityCustomAction:)]];
179 }
180 }
181
182 return [actions copy];
183}
184
185- (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action
186{
187 if (!_onAccessibilityAction || !accessibilityActionsLabelMap) {
188 return NO;
189 }
190
191 // 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.
192
193 NSDictionary *actionObject = accessibilityActionsLabelMap[action.name];
194 if (actionObject) {
195 _onAccessibilityAction(@{
196 @"actionName": actionObject[@"name"],
197 @"actionTarget": self.reactTag
198 });
199 }
200 return YES;
201}
202
203- (NSString *)accessibilityValue
204{
205 if ((self.accessibilityTraits & SwitchAccessibilityTrait) == SwitchAccessibilityTrait) {
206 for (NSString *state in self.accessibilityState) {
207 id val = self.accessibilityState[state];
208 if (!val) {
209 continue;
210 }
211 if ([state isEqualToString:@"checked"] && [val isKindOfClass:[NSNumber class]]) {
212 return [val boolValue] ? @"1" : @"0";
213 }
214 }
215 }
216 NSMutableArray *valueComponents = [NSMutableArray new];
217 static NSDictionary<NSString *, NSString *> *roleDescriptions = nil;
218 static dispatch_once_t onceToken1;
219 dispatch_once(&onceToken1, ^{
220 roleDescriptions = @{
221 @"alert" : @"alert",
222 @"checkbox" : @"checkbox",
223 @"combobox" : @"combo box",
224 @"menu" : @"menu",
225 @"menubar" : @"menu bar",
226 @"menuitem" : @"menu item",
227 @"progressbar" : @"progress bar",
228 @"radio" : @"radio button",
229 @"radiogroup" : @"radio group",
230 @"scrollbar" : @"scroll bar",
231 @"spinbutton" : @"spin button",
232 @"switch" : @"switch",
233 @"tab" : @"tab",
234 @"tablist" : @"tab list",
235 @"timer" : @"timer",
236 @"toolbar" : @"tool bar",
237 };
238 });
239 static NSDictionary<NSString *, NSString *> *stateDescriptions = nil;
240 static dispatch_once_t onceToken2;
241 dispatch_once(&onceToken2, ^{
242 stateDescriptions = @{
243 @"checked" : @"checked",
244 @"unchecked" : @"not checked",
245 @"busy" : @"busy",
246 @"expanded" : @"expanded",
247 @"collapsed" : @"collapsed",
248 @"mixed": @"mixed",
249 };
250 });
251 NSString *roleDescription = self.accessibilityRole ? roleDescriptions[self.accessibilityRole]: nil;
252 if (roleDescription) {
253 [valueComponents addObject:roleDescription];
254 }
255 for (NSString *state in self.accessibilityState) {
256 id val = self.accessibilityState[state];
257 if (!val) {
258 continue;
259 }
260 if ([state isEqualToString:@"checked"]) {
261 if ([val isKindOfClass:[NSNumber class]]) {
262 [valueComponents addObject:stateDescriptions[[val boolValue] ? @"checked" : @"unchecked"]];
263 } else if ([val isKindOfClass:[NSString class]] && [val isEqualToString:@"mixed"]) {
264 [valueComponents addObject:stateDescriptions[@"mixed"]];
265 }
266 }
267 if ([state isEqualToString:@"expanded"] && [val isKindOfClass:[NSNumber class]]) {
268 [valueComponents addObject:stateDescriptions[[val boolValue] ? @"expanded" : @"collapsed"]];
269 }
270 if ([state isEqualToString:@"busy"] && [val isKindOfClass:[NSNumber class]] && [val boolValue]) {
271 [valueComponents addObject:stateDescriptions[@"busy"]];
272 }
273 }
274
275 // handle accessibilityValue
276
277 if (self.accessibilityValueInternal) {
278 id min = self.accessibilityValueInternal[@"min"];
279 id now = self.accessibilityValueInternal[@"now"];
280 id max = self.accessibilityValueInternal[@"max"];
281 id text = self.accessibilityValueInternal[@"text"];
282 if (text && [text isKindOfClass:[NSString class]]) {
283 [valueComponents addObject:text];
284 } else if ([min isKindOfClass:[NSNumber class]] &&
285 [now isKindOfClass:[NSNumber class]] &&
286 [max isKindOfClass:[NSNumber class]] &&
287 ([min intValue] < [max intValue]) &&
288 ([min intValue] <= [now intValue] && [now intValue] <= [max intValue])) {
289 int val = ([now intValue]*100)/([max intValue]-[min intValue]);
290 [valueComponents addObject:[NSString stringWithFormat:@"%d percent", val]];
291 }
292 }
293
294 if (valueComponents.count > 0) {
295 return [valueComponents componentsJoinedByString:@", "];
296 }
297 return nil;
298}
299
300- (void)setPointerEvents:(RCTPointerEvents)pointerEvents
301{
302 _pointerEvents = pointerEvents;
303 self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone);
304 if (pointerEvents == RCTPointerEventsBoxNone) {
305 self.accessibilityViewIsModal = NO;
306 }
307}
308
309- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
310{
311 BOOL canReceiveTouchEvents = ([self isUserInteractionEnabled] && ![self isHidden]);
312 if(!canReceiveTouchEvents) {
313 return nil;
314 }
315
316 // `hitSubview` is the topmost subview which was hit. The hit point can
317 // be outside the bounds of `view` (e.g., if -clipsToBounds is NO).
318 UIView *hitSubview = nil;
319 BOOL isPointInside = [self pointInside:point withEvent:event];
320 BOOL needsHitSubview = !(_pointerEvents == RCTPointerEventsNone || _pointerEvents == RCTPointerEventsBoxOnly);
321 if (needsHitSubview && (![self clipsToBounds] || isPointInside)) {
322 // Take z-index into account when calculating the touch target.
323 NSArray<UIView *> *sortedSubviews = [self reactZIndexSortedSubviews];
324
325 // The default behaviour of UIKit is that if a view does not contain a point,
326 // then no subviews will be returned from hit testing, even if they contain
327 // the hit point. By doing hit testing directly on the subviews, we bypass
328 // the strict containment policy (i.e., UIKit guarantees that every ancestor
329 // of the hit view will return YES from -pointInside:withEvent:). See:
330 // - https://developer.apple.com/library/ios/qa/qa2013/qa1812.html
331 for (UIView *subview in [sortedSubviews reverseObjectEnumerator]) {
332 CGPoint convertedPoint = [subview convertPoint:point fromView:self];
333 hitSubview = [subview hitTest:convertedPoint withEvent:event];
334 if (hitSubview != nil) {
335 break;
336 }
337 }
338 }
339
340 UIView *hitView = (isPointInside ? self : nil);
341
342 switch (_pointerEvents) {
343 case RCTPointerEventsNone:
344 return nil;
345 case RCTPointerEventsUnspecified:
346 return hitSubview ?: hitView;
347 case RCTPointerEventsBoxOnly:
348 return hitView;
349 case RCTPointerEventsBoxNone:
350 return hitSubview;
351 default:
352 RCTLogError(@"Invalid pointer-events specified %lld on %@", (long long)_pointerEvents, self);
353 return hitSubview ?: hitView;
354 }
355}
356
357- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
358{
359 if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) {
360 return [super pointInside:point withEvent:event];
361 }
362 CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
363 return CGRectContainsPoint(hitFrame, point);
364}
365
366- (UIView *)reactAccessibilityElement
367{
368 return self;
369}
370
371- (BOOL)isAccessibilityElement
372{
373 if (self.reactAccessibilityElement == self) {
374 return [super isAccessibilityElement];
375 }
376
377 return NO;
378}
379
380- (BOOL)performAccessibilityAction:(NSString *) name
381{
382 if (_onAccessibilityAction && accessibilityActionsNameMap[name]) {
383 _onAccessibilityAction(@{
384 @"actionName" : name,
385 @"actionTarget" : self.reactTag
386 });
387 return YES;
388 }
389 return NO;
390}
391
392- (BOOL)accessibilityActivate
393{
394 if ([self performAccessibilityAction:@"activate"]) {
395 return YES;
396 } else if (_onAccessibilityTap) {
397 _onAccessibilityTap(nil);
398 return YES;
399 } else {
400 return NO;
401 }
402}
403
404- (BOOL)accessibilityPerformMagicTap
405{
406 if ([self performAccessibilityAction:@"magicTap"]) {
407 return YES;
408 } else if (_onMagicTap) {
409 _onMagicTap(nil);
410 return YES;
411 } else {
412 return NO;
413 }
414}
415
416- (BOOL)accessibilityPerformEscape
417{
418 if ([self performAccessibilityAction:@"escape"]) {
419 return YES;
420 } else if (_onAccessibilityEscape) {
421 _onAccessibilityEscape(nil);
422 return YES;
423 } else {
424 return NO;
425 }
426}
427
428- (void)accessibilityIncrement
429{
430 [self performAccessibilityAction:@"increment"];
431}
432
433- (void)accessibilityDecrement
434{
435 [self performAccessibilityAction:@"decrement"];
436}
437
438- (NSString *)description
439{
440 NSString *superDescription = super.description;
441 NSRange semicolonRange = [superDescription rangeOfString:@";"];
442 NSString *replacement = [NSString stringWithFormat:@"; reactTag: %@;", self.reactTag];
443 return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement];
444}
445
446#pragma mark - Statics for dealing with layoutGuides
447
448+ (void)autoAdjustInsetsForView:(UIView<RCTAutoInsetsProtocol> *)parentView
449 withScrollView:(UIScrollView *)scrollView
450 updateOffset:(BOOL)updateOffset
451{
452 UIEdgeInsets baseInset = parentView.contentInset;
453 CGFloat previousInsetTop = scrollView.contentInset.top;
454 CGPoint contentOffset = scrollView.contentOffset;
455
456 if (parentView.automaticallyAdjustContentInsets) {
457 UIEdgeInsets autoInset = [self contentInsetsForView:parentView];
458 baseInset.top += autoInset.top;
459 baseInset.bottom += autoInset.bottom;
460 baseInset.left += autoInset.left;
461 baseInset.right += autoInset.right;
462 }
463 scrollView.contentInset = baseInset;
464 scrollView.scrollIndicatorInsets = baseInset;
465
466 if (updateOffset) {
467 // If we're adjusting the top inset, then let's also adjust the contentOffset so that the view
468 // elements above the top guide do not cover the content.
469 // This is generally only needed when your views are initially laid out, for
470 // manual changes to contentOffset, you can optionally disable this step
471 CGFloat currentInsetTop = scrollView.contentInset.top;
472 if (currentInsetTop != previousInsetTop) {
473 contentOffset.y -= (currentInsetTop - previousInsetTop);
474 scrollView.contentOffset = contentOffset;
475 }
476 }
477}
478
479+ (UIEdgeInsets)contentInsetsForView:(UIView *)view
480{
481 while (view) {
482 UIViewController *controller = view.reactViewController;
483 if (controller) {
484 return (UIEdgeInsets){
485 controller.topLayoutGuide.length, 0,
486 controller.bottomLayoutGuide.length, 0
487 };
488 }
489 view = view.superview;
490 }
491 return UIEdgeInsetsZero;
492}
493
494#pragma mark - View unmounting
495
496- (void)react_remountAllSubviews
497{
498 if (_removeClippedSubviews) {
499 for (UIView *view in self.reactSubviews) {
500 if (view.superview != self) {
501 [self addSubview:view];
502 [view react_remountAllSubviews];
503 }
504 }
505 } else {
506 // If _removeClippedSubviews is false, we must already be showing all subviews
507 [super react_remountAllSubviews];
508 }
509}
510
511- (void)react_updateClippedSubviewsWithClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
512{
513 // TODO (#5906496): for scrollviews (the primary use-case) we could
514 // optimize this by only doing a range check along the scroll axis,
515 // instead of comparing the whole frame
516
517 if (!_removeClippedSubviews) {
518 // Use default behavior if unmounting is disabled
519 return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
520 }
521
522 if (self.reactSubviews.count == 0) {
523 // Do nothing if we have no subviews
524 return;
525 }
526
527 if (CGSizeEqualToSize(self.bounds.size, CGSizeZero)) {
528 // Do nothing if layout hasn't happened yet
529 return;
530 }
531
532 // Convert clipping rect to local coordinates
533 clipRect = [clipView convertRect:clipRect toView:self];
534 clipRect = CGRectIntersection(clipRect, self.bounds);
535 clipView = self;
536
537 // Mount / unmount views
538 for (UIView *view in self.reactSubviews) {
539 if (!CGSizeEqualToSize(CGRectIntersection(clipRect, view.frame).size, CGSizeZero)) {
540 // View is at least partially visible, so remount it if unmounted
541 [self addSubview:view];
542
543 // Then test its subviews
544 if (CGRectContainsRect(clipRect, view.frame)) {
545 // View is fully visible, so remount all subviews
546 [view react_remountAllSubviews];
547 } else {
548 // View is partially visible, so update clipped subviews
549 [view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
550 }
551
552 } else if (view.superview) {
553
554 // View is completely outside the clipRect, so unmount it
555 [view removeFromSuperview];
556 }
557 }
558}
559
560- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews
561{
562 if (!removeClippedSubviews && _removeClippedSubviews) {
563 [self react_remountAllSubviews];
564 }
565 _removeClippedSubviews = removeClippedSubviews;
566}
567
568- (void)didUpdateReactSubviews
569{
570 if (_removeClippedSubviews) {
571 [self updateClippedSubviews];
572 } else {
573 [super didUpdateReactSubviews];
574 }
575}
576
577- (void)updateClippedSubviews
578{
579 // Find a suitable view to use for clipping
580 UIView *clipView = [self react_findClipView];
581 if (clipView) {
582 [self react_updateClippedSubviewsWithClipRect:clipView.bounds relativeToView:clipView];
583 }
584}
585
586- (void)layoutSubviews
587{
588 // TODO (#5906496): this a nasty performance drain, but necessary
589 // to prevent gaps appearing when the loading spinner disappears.
590 // We might be able to fix this another way by triggering a call
591 // to updateClippedSubviews manually after loading
592
593 [super layoutSubviews];
594
595 if (_removeClippedSubviews) {
596 [self updateClippedSubviews];
597 }
598}
599
600#pragma mark - Borders
601
602- (UIColor *)backgroundColor
603{
604 return _backgroundColor;
605}
606
607- (void)setBackgroundColor:(UIColor *)backgroundColor
608{
609 if ([_backgroundColor isEqual:backgroundColor]) {
610 return;
611 }
612
613 _backgroundColor = backgroundColor;
614 [self.layer setNeedsDisplay];
615}
616
617static CGFloat RCTDefaultIfNegativeTo(CGFloat defaultValue, CGFloat x) {
618 return x >= 0 ? x : defaultValue;
619};
620
621- (UIEdgeInsets)bordersAsInsets
622{
623 const CGFloat borderWidth = MAX(0, _borderWidth);
624 const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
625
626 if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) {
627 const CGFloat borderStartWidth = RCTDefaultIfNegativeTo(_borderLeftWidth, _borderStartWidth);
628 const CGFloat borderEndWidth = RCTDefaultIfNegativeTo(_borderRightWidth, _borderEndWidth);
629
630 const CGFloat directionAwareBorderLeftWidth = isRTL ? borderEndWidth : borderStartWidth;
631 const CGFloat directionAwareBorderRightWidth = isRTL ? borderStartWidth : borderEndWidth;
632
633 return (UIEdgeInsets) {
634 RCTDefaultIfNegativeTo(borderWidth, _borderTopWidth),
635 RCTDefaultIfNegativeTo(borderWidth, directionAwareBorderLeftWidth),
636 RCTDefaultIfNegativeTo(borderWidth, _borderBottomWidth),
637 RCTDefaultIfNegativeTo(borderWidth, directionAwareBorderRightWidth),
638 };
639 }
640
641 const CGFloat directionAwareBorderLeftWidth = isRTL ? _borderEndWidth : _borderStartWidth;
642 const CGFloat directionAwareBorderRightWidth = isRTL ? _borderStartWidth : _borderEndWidth;
643
644 return (UIEdgeInsets) {
645 RCTDefaultIfNegativeTo(borderWidth, _borderTopWidth),
646 RCTDefaultIfNegativeTo(borderWidth, RCTDefaultIfNegativeTo(_borderLeftWidth, directionAwareBorderLeftWidth)),
647 RCTDefaultIfNegativeTo(borderWidth, _borderBottomWidth),
648 RCTDefaultIfNegativeTo(borderWidth, RCTDefaultIfNegativeTo(_borderRightWidth, directionAwareBorderRightWidth)),
649 };
650}
651
652- (RCTCornerRadii)cornerRadii
653{
654 const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
655 const CGFloat radius = MAX(0, _borderRadius);
656
657 CGFloat topLeftRadius;
658 CGFloat topRightRadius;
659 CGFloat bottomLeftRadius;
660 CGFloat bottomRightRadius;
661
662 if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) {
663 const CGFloat topStartRadius = RCTDefaultIfNegativeTo(_borderTopLeftRadius, _borderTopStartRadius);
664 const CGFloat topEndRadius = RCTDefaultIfNegativeTo(_borderTopRightRadius, _borderTopEndRadius);
665 const CGFloat bottomStartRadius = RCTDefaultIfNegativeTo(_borderBottomLeftRadius, _borderBottomStartRadius);
666 const CGFloat bottomEndRadius = RCTDefaultIfNegativeTo(_borderBottomRightRadius, _borderBottomEndRadius);
667
668 const CGFloat directionAwareTopLeftRadius = isRTL ? topEndRadius : topStartRadius;
669 const CGFloat directionAwareTopRightRadius = isRTL ? topStartRadius : topEndRadius;
670 const CGFloat directionAwareBottomLeftRadius = isRTL ? bottomEndRadius : bottomStartRadius;
671 const CGFloat directionAwareBottomRightRadius = isRTL ? bottomStartRadius : bottomEndRadius;
672
673 topLeftRadius = RCTDefaultIfNegativeTo(radius, directionAwareTopLeftRadius);
674 topRightRadius = RCTDefaultIfNegativeTo(radius, directionAwareTopRightRadius);
675 bottomLeftRadius = RCTDefaultIfNegativeTo(radius, directionAwareBottomLeftRadius);
676 bottomRightRadius = RCTDefaultIfNegativeTo(radius, directionAwareBottomRightRadius);
677 } else {
678 const CGFloat directionAwareTopLeftRadius = isRTL ? _borderTopEndRadius : _borderTopStartRadius;
679 const CGFloat directionAwareTopRightRadius = isRTL ? _borderTopStartRadius : _borderTopEndRadius;
680 const CGFloat directionAwareBottomLeftRadius = isRTL ? _borderBottomEndRadius : _borderBottomStartRadius;
681 const CGFloat directionAwareBottomRightRadius = isRTL ? _borderBottomStartRadius : _borderBottomEndRadius;
682
683 topLeftRadius = RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderTopLeftRadius, directionAwareTopLeftRadius));
684 topRightRadius = RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderTopRightRadius, directionAwareTopRightRadius));
685 bottomLeftRadius = RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderBottomLeftRadius, directionAwareBottomLeftRadius));
686 bottomRightRadius = RCTDefaultIfNegativeTo(radius, RCTDefaultIfNegativeTo(_borderBottomRightRadius, directionAwareBottomRightRadius));
687 }
688
689 // Get scale factors required to prevent radii from overlapping
690 const CGSize size = self.bounds.size;
691 const CGFloat topScaleFactor = RCTZeroIfNaN(MIN(1, size.width / (topLeftRadius + topRightRadius)));
692 const CGFloat bottomScaleFactor = RCTZeroIfNaN(MIN(1, size.width / (bottomLeftRadius + bottomRightRadius)));
693 const CGFloat rightScaleFactor = RCTZeroIfNaN(MIN(1, size.height / (topRightRadius + bottomRightRadius)));
694 const CGFloat leftScaleFactor = RCTZeroIfNaN(MIN(1, size.height / (topLeftRadius + bottomLeftRadius)));
695
696 // Return scaled radii
697 return (RCTCornerRadii){
698 topLeftRadius * MIN(topScaleFactor, leftScaleFactor),
699 topRightRadius * MIN(topScaleFactor, rightScaleFactor),
700 bottomLeftRadius * MIN(bottomScaleFactor, leftScaleFactor),
701 bottomRightRadius * MIN(bottomScaleFactor, rightScaleFactor),
702 };
703}
704
705- (RCTBorderColors)borderColors
706{
707 const BOOL isRTL = _reactLayoutDirection == UIUserInterfaceLayoutDirectionRightToLeft;
708
709 if ([[RCTI18nUtil sharedInstance] doLeftAndRightSwapInRTL]) {
710 const CGColorRef borderStartColor = _borderStartColor ?: _borderLeftColor;
711 const CGColorRef borderEndColor = _borderEndColor ?: _borderRightColor;
712
713 const CGColorRef directionAwareBorderLeftColor = isRTL ? borderEndColor : borderStartColor;
714 const CGColorRef directionAwareBorderRightColor = isRTL ? borderStartColor : borderEndColor;
715
716 return (RCTBorderColors){
717 _borderTopColor ?: _borderColor,
718 directionAwareBorderLeftColor ?: _borderColor,
719 _borderBottomColor ?: _borderColor,
720 directionAwareBorderRightColor ?: _borderColor,
721 };
722 }
723
724 const CGColorRef directionAwareBorderLeftColor = isRTL ? _borderEndColor : _borderStartColor;
725 const CGColorRef directionAwareBorderRightColor = isRTL ? _borderStartColor : _borderEndColor;
726
727 return (RCTBorderColors){
728 _borderTopColor ?: _borderColor,
729 directionAwareBorderLeftColor ?: _borderLeftColor ?: _borderColor,
730 _borderBottomColor ?: _borderColor,
731 directionAwareBorderRightColor ?: _borderRightColor ?: _borderColor,
732 };
733}
734
735- (void)reactSetFrame:(CGRect)frame
736{
737 // If frame is zero, or below the threshold where the border radii can
738 // be rendered as a stretchable image, we'll need to re-render.
739 // TODO: detect up-front if re-rendering is necessary
740 CGSize oldSize = self.bounds.size;
741 [super reactSetFrame:frame];
742 if (!CGSizeEqualToSize(self.bounds.size, oldSize)) {
743 [self.layer setNeedsDisplay];
744 }
745}
746
747- (void)displayLayer:(CALayer *)layer
748{
749 if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) {
750 return;
751 }
752
753 RCTUpdateShadowPathForView(self);
754
755 const RCTCornerRadii cornerRadii = [self cornerRadii];
756 const UIEdgeInsets borderInsets = [self bordersAsInsets];
757 const RCTBorderColors borderColors = [self borderColors];
758
759 BOOL useIOSBorderRendering =
760 RCTCornerRadiiAreEqual(cornerRadii) &&
761 RCTBorderInsetsAreEqual(borderInsets) &&
762 RCTBorderColorsAreEqual(borderColors) &&
763 _borderStyle == RCTBorderStyleSolid &&
764
765 // iOS draws borders in front of the content whereas CSS draws them behind
766 // the content. For this reason, only use iOS border drawing when clipping
767 // or when the border is hidden.
768
769 (borderInsets.top == 0 || (borderColors.top && CGColorGetAlpha(borderColors.top) == 0) || self.clipsToBounds);
770
771 // iOS clips to the outside of the border, but CSS clips to the inside. To
772 // solve this, we'll need to add a container view inside the main view to
773 // correctly clip the subviews.
774
775 if (useIOSBorderRendering) {
776 layer.cornerRadius = cornerRadii.topLeft;
777 layer.borderColor = borderColors.left;
778 layer.borderWidth = borderInsets.left;
779 layer.backgroundColor = _backgroundColor.CGColor;
780 layer.contents = nil;
781 layer.needsDisplayOnBoundsChange = NO;
782 layer.mask = nil;
783 return;
784 }
785
786 UIImage *image = RCTGetBorderImage(_borderStyle,
787 layer.bounds.size,
788 cornerRadii,
789 borderInsets,
790 borderColors,
791 _backgroundColor.CGColor,
792 self.clipsToBounds);
793
794 layer.backgroundColor = NULL;
795
796 if (image == nil) {
797 layer.contents = nil;
798 layer.needsDisplayOnBoundsChange = NO;
799 return;
800 }
801
802 CGRect contentsCenter = ({
803 CGSize size = image.size;
804 UIEdgeInsets insets = image.capInsets;
805 CGRectMake(
806 insets.left / size.width,
807 insets.top / size.height,
808 (CGFloat)1.0 / size.width,
809 (CGFloat)1.0 / size.height
810 );
811 });
812
813 layer.contents = (id)image.CGImage;
814 layer.contentsScale = image.scale;
815 layer.needsDisplayOnBoundsChange = YES;
816 layer.magnificationFilter = kCAFilterNearest;
817
818 const BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero);
819 if (isResizable) {
820 layer.contentsCenter = contentsCenter;
821 } else {
822 layer.contentsCenter = CGRectMake(0.0, 0.0, 1.0, 1.0);
823 }
824
825 [self updateClippingForLayer:layer];
826}
827
828static BOOL RCTLayerHasShadow(CALayer *layer)
829{
830 return layer.shadowOpacity * CGColorGetAlpha(layer.shadowColor) > 0;
831}
832
833static void RCTUpdateShadowPathForView(RCTView *view)
834{
835 if (RCTLayerHasShadow(view.layer)) {
836 if (CGColorGetAlpha(view.backgroundColor.CGColor) > 0.999) {
837
838 // If view has a solid background color, calculate shadow path from border
839 const RCTCornerRadii cornerRadii = [view cornerRadii];
840 const RCTCornerInsets cornerInsets = RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero);
841 CGPathRef shadowPath = RCTPathCreateWithRoundedRect(view.bounds, cornerInsets, NULL);
842 view.layer.shadowPath = shadowPath;
843 CGPathRelease(shadowPath);
844
845 } else {
846
847 // Can't accurately calculate box shadow, so fall back to pixel-based shadow
848 view.layer.shadowPath = nil;
849
850 RCTLogAdvice(@"View #%@ of type %@ has a shadow set but cannot calculate "
851 "shadow efficiently. Consider setting a background color to "
852 "fix this, or apply the shadow to a more specific component.",
853 view.reactTag, [view class]);
854 }
855 }
856}
857
858- (void)updateClippingForLayer:(CALayer *)layer
859{
860 CALayer *mask = nil;
861 CGFloat cornerRadius = 0;
862
863 if (self.clipsToBounds) {
864
865 const RCTCornerRadii cornerRadii = [self cornerRadii];
866 if (RCTCornerRadiiAreEqual(cornerRadii)) {
867
868 cornerRadius = cornerRadii.topLeft;
869
870 } else {
871
872 CAShapeLayer *shapeLayer = [CAShapeLayer layer];
873 CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL);
874 shapeLayer.path = path;
875 CGPathRelease(path);
876 mask = shapeLayer;
877 }
878 }
879
880 layer.cornerRadius = cornerRadius;
881 layer.mask = mask;
882}
883
884#pragma mark Border Color
885
886#define setBorderColor(side) \
887 - (void)setBorder##side##Color:(CGColorRef)color \
888 { \
889 if (CGColorEqualToColor(_border##side##Color, color)) { \
890 return; \
891 } \
892 CGColorRelease(_border##side##Color); \
893 _border##side##Color = CGColorRetain(color); \
894 [self.layer setNeedsDisplay]; \
895 }
896
897setBorderColor()
898setBorderColor(Top)
899setBorderColor(Right)
900setBorderColor(Bottom)
901setBorderColor(Left)
902setBorderColor(Start)
903setBorderColor(End)
904
905#pragma mark - Border Width
906
907#define setBorderWidth(side) \
908 - (void)setBorder##side##Width:(CGFloat)width \
909 { \
910 if (_border##side##Width == width) { \
911 return; \
912 } \
913 _border##side##Width = width; \
914 [self.layer setNeedsDisplay]; \
915 }
916
917setBorderWidth()
918setBorderWidth(Top)
919setBorderWidth(Right)
920setBorderWidth(Bottom)
921setBorderWidth(Left)
922setBorderWidth(Start)
923setBorderWidth(End)
924
925#pragma mark - Border Radius
926
927#define setBorderRadius(side) \
928 - (void)setBorder##side##Radius:(CGFloat)radius \
929 { \
930 if (_border##side##Radius == radius) { \
931 return; \
932 } \
933 _border##side##Radius = radius; \
934 [self.layer setNeedsDisplay]; \
935 }
936
937setBorderRadius()
938setBorderRadius(TopLeft)
939setBorderRadius(TopRight)
940setBorderRadius(TopStart)
941setBorderRadius(TopEnd)
942setBorderRadius(BottomLeft)
943setBorderRadius(BottomRight)
944setBorderRadius(BottomStart)
945setBorderRadius(BottomEnd)
946
947#pragma mark - Border Style
948
949#define setBorderStyle(side) \
950 - (void)setBorder##side##Style:(RCTBorderStyle)style \
951 { \
952 if (_border##side##Style == style) { \
953 return; \
954 } \
955 _border##side##Style = style; \
956 [self.layer setNeedsDisplay]; \
957 }
958
959setBorderStyle()
960
961- (void)dealloc
962{
963 CGColorRelease(_borderColor);
964 CGColorRelease(_borderTopColor);
965 CGColorRelease(_borderRightColor);
966 CGColorRelease(_borderBottomColor);
967 CGColorRelease(_borderLeftColor);
968 CGColorRelease(_borderStartColor);
969 CGColorRelease(_borderEndColor);
970}
971
972@end