2015-03-23 20:28:42 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
2015-02-20 04:10:52 +00:00
|
|
|
|
|
|
|
#import "RCTView.h"
|
|
|
|
|
|
|
|
#import "RCTAutoInsetsProtocol.h"
|
2015-05-28 15:52:46 +00:00
|
|
|
#import "RCTBorderDrawing.h"
|
2015-02-20 04:10:52 +00:00
|
|
|
#import "RCTConvert.h"
|
|
|
|
#import "RCTLog.h"
|
2015-04-27 10:50:07 +00:00
|
|
|
#import "RCTUtils.h"
|
2015-03-26 09:58:06 +00:00
|
|
|
#import "UIView+React.h"
|
2015-02-20 04:10:52 +00:00
|
|
|
|
2015-03-11 02:03:59 +00:00
|
|
|
@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
|
|
|
|
|
2015-08-24 10:14:33 +00:00
|
|
|
if (self.clipsToBounds && self.subviews.count > 0) {
|
2015-03-11 02:03:59 +00:00
|
|
|
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;
|
|
|
|
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];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
testView = testView.superview;
|
|
|
|
}
|
|
|
|
return clipView ?: self.window;
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
static NSString *RCTRecursiveAccessibilityLabel(UIView *view)
|
|
|
|
{
|
|
|
|
NSMutableString *str = [NSMutableString stringWithString:@""];
|
|
|
|
for (UIView *subview in view.subviews) {
|
2015-08-24 10:14:33 +00:00
|
|
|
NSString *label = subview.accessibilityLabel;
|
2015-02-20 04:10:52 +00:00
|
|
|
if (label) {
|
|
|
|
[str appendString:@" "];
|
|
|
|
[str appendString:label];
|
|
|
|
} else {
|
|
|
|
[str appendString:RCTRecursiveAccessibilityLabel(subview)];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return str;
|
|
|
|
}
|
|
|
|
|
|
|
|
@implementation RCTView
|
2015-03-11 02:03:59 +00:00
|
|
|
{
|
2015-11-14 18:25:00 +00:00
|
|
|
NSMutableArray<UIView *> *_reactSubviews;
|
2015-04-27 10:50:07 +00:00
|
|
|
UIColor *_backgroundColor;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
|
|
{
|
|
|
|
if ((self = [super initWithFrame:frame])) {
|
|
|
|
_borderWidth = -1;
|
|
|
|
_borderTopWidth = -1;
|
|
|
|
_borderRightWidth = -1;
|
|
|
|
_borderBottomWidth = -1;
|
|
|
|
_borderLeftWidth = -1;
|
2015-05-13 15:22:21 +00:00
|
|
|
_borderTopLeftRadius = -1;
|
|
|
|
_borderTopRightRadius = -1;
|
|
|
|
_borderBottomLeftRadius = -1;
|
|
|
|
_borderBottomRightRadius = -1;
|
2015-12-01 15:41:20 +00:00
|
|
|
_borderStyle = RCTBorderStyleSolid;
|
2015-04-27 10:50:07 +00:00
|
|
|
|
2015-08-24 10:14:33 +00:00
|
|
|
_backgroundColor = super.backgroundColor;
|
2015-04-27 10:50:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
2015-08-24 10:14:33 +00:00
|
|
|
RCT_NOT_IMPLEMENTED(- (instancetype)initWithCoder:unused)
|
2015-06-15 14:53:45 +00:00
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
- (NSString *)accessibilityLabel
|
|
|
|
{
|
2015-03-01 23:33:55 +00:00
|
|
|
if (super.accessibilityLabel) {
|
|
|
|
return super.accessibilityLabel;
|
2015-02-20 04:10:52 +00:00
|
|
|
}
|
|
|
|
return RCTRecursiveAccessibilityLabel(self);
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setPointerEvents:(RCTPointerEvents)pointerEvents
|
|
|
|
{
|
|
|
|
_pointerEvents = pointerEvents;
|
|
|
|
self.userInteractionEnabled = (pointerEvents != RCTPointerEventsNone);
|
|
|
|
if (pointerEvents == RCTPointerEventsBoxNone) {
|
2015-03-26 08:43:17 +00:00
|
|
|
self.accessibilityViewIsModal = NO;
|
2015-02-20 04:10:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
|
|
|
|
{
|
2015-10-30 11:11:04 +00:00
|
|
|
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);
|
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
switch (_pointerEvents) {
|
|
|
|
case RCTPointerEventsNone:
|
|
|
|
return nil;
|
|
|
|
case RCTPointerEventsUnspecified:
|
2015-10-30 11:11:04 +00:00
|
|
|
return hitSubview ?: hitView;
|
2015-02-20 04:10:52 +00:00
|
|
|
case RCTPointerEventsBoxOnly:
|
2015-10-30 11:11:04 +00:00
|
|
|
return hitView;
|
2015-02-20 04:10:52 +00:00
|
|
|
case RCTPointerEventsBoxNone:
|
2015-10-30 11:11:04 +00:00
|
|
|
return hitSubview;
|
2015-02-20 04:10:52 +00:00
|
|
|
default:
|
|
|
|
RCTLogError(@"Invalid pointer-events specified %zd on %@", _pointerEvents, self);
|
2015-10-30 11:11:04 +00:00
|
|
|
return hitSubview ?: hitView;
|
2015-02-20 04:10:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-20 15:33:16 +00:00
|
|
|
- (BOOL)accessibilityActivate
|
|
|
|
{
|
Added mechanism for directly mapping JS event handlers to blocks
Summary:
Currently, the system for mapping JS event handlers to blocks is quite clean on the JS side, but is clunky on the native side. The event property is passed as a boolean, which can then be checked by the native side, and if true, the native side is supposed to send an event via the event dispatcher.
This diff adds the facility to declare the property as a block instead. This means that the event side can simply call the block, and it will automatically send the event. Because the blocks for bubbling and direct events are named differently, we can also use this to generate the event registration data and get rid of the arrays of event names.
The name of the event is inferred from the property name, which means that the property for an event called "load" must be called `onLoad` or the mapping won't work. This can be optionally remapped to a different property name on the view itself if necessary, e.g.
RCT_REMAP_VIEW_PROPERTY(onLoad, loadEventBlock, RCTDirectEventBlock)
If you don't want to use this mechanism then for now it is still possible to declare the property as a BOOL instead and use the old mechanism (this approach is now deprecated however, and may eventually be removed altogether).
2015-09-02 12:58:10 +00:00
|
|
|
if (_onAccessibilityTap) {
|
|
|
|
_onAccessibilityTap(nil);
|
2015-05-20 15:33:16 +00:00
|
|
|
return YES;
|
|
|
|
} else {
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-05-19 13:21:52 +00:00
|
|
|
- (BOOL)accessibilityPerformMagicTap
|
|
|
|
{
|
Added mechanism for directly mapping JS event handlers to blocks
Summary:
Currently, the system for mapping JS event handlers to blocks is quite clean on the JS side, but is clunky on the native side. The event property is passed as a boolean, which can then be checked by the native side, and if true, the native side is supposed to send an event via the event dispatcher.
This diff adds the facility to declare the property as a block instead. This means that the event side can simply call the block, and it will automatically send the event. Because the blocks for bubbling and direct events are named differently, we can also use this to generate the event registration data and get rid of the arrays of event names.
The name of the event is inferred from the property name, which means that the property for an event called "load" must be called `onLoad` or the mapping won't work. This can be optionally remapped to a different property name on the view itself if necessary, e.g.
RCT_REMAP_VIEW_PROPERTY(onLoad, loadEventBlock, RCTDirectEventBlock)
If you don't want to use this mechanism then for now it is still possible to declare the property as a BOOL instead and use the old mechanism (this approach is now deprecated however, and may eventually be removed altogether).
2015-09-02 12:58:10 +00:00
|
|
|
if (_onMagicTap) {
|
|
|
|
_onMagicTap(nil);
|
2015-05-19 13:21:52 +00:00
|
|
|
return YES;
|
|
|
|
} else {
|
|
|
|
return NO;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-06-15 19:05:04 +00:00
|
|
|
- (NSString *)description
|
|
|
|
{
|
|
|
|
NSString *superDescription = super.description;
|
|
|
|
NSRange semicolonRange = [superDescription rangeOfString:@";"];
|
|
|
|
NSString *replacement = [NSString stringWithFormat:@"; reactTag: %@;", self.reactTag];
|
|
|
|
return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement];
|
|
|
|
}
|
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
#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;
|
2015-03-01 23:33:55 +00:00
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
if (parentView.automaticallyAdjustContentInsets) {
|
|
|
|
UIEdgeInsets autoInset = [self contentInsetsForView:parentView];
|
|
|
|
baseInset.top += autoInset.top;
|
|
|
|
baseInset.bottom += autoInset.bottom;
|
|
|
|
baseInset.left += autoInset.left;
|
|
|
|
baseInset.right += autoInset.right;
|
|
|
|
}
|
2015-08-24 10:14:33 +00:00
|
|
|
scrollView.contentInset = baseInset;
|
|
|
|
scrollView.scrollIndicatorInsets = baseInset;
|
2015-03-01 23:33:55 +00:00
|
|
|
|
2015-02-20 04:10:52 +00:00
|
|
|
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) {
|
2015-07-31 18:23:29 +00:00
|
|
|
UIViewController *controller = view.reactViewController;
|
2015-02-20 04:10:52 +00:00
|
|
|
if (controller) {
|
|
|
|
return (UIEdgeInsets){
|
|
|
|
controller.topLayoutGuide.length, 0,
|
|
|
|
controller.bottomLayoutGuide.length, 0
|
|
|
|
};
|
|
|
|
}
|
|
|
|
view = view.superview;
|
|
|
|
}
|
|
|
|
return UIEdgeInsetsZero;
|
|
|
|
}
|
|
|
|
|
2015-03-11 02:03:59 +00:00
|
|
|
#pragma mark - View unmounting
|
|
|
|
|
|
|
|
- (void)react_remountAllSubviews
|
|
|
|
{
|
|
|
|
if (_reactSubviews) {
|
2015-06-15 14:53:45 +00:00
|
|
|
NSUInteger index = 0;
|
2015-03-11 02:03:59 +00:00
|
|
|
for (UIView *view in _reactSubviews) {
|
|
|
|
if (view.superview != self) {
|
2015-06-15 14:53:45 +00:00
|
|
|
if (index < self.subviews.count) {
|
2015-03-11 02:03:59 +00:00
|
|
|
[self insertSubview:view atIndex:index];
|
|
|
|
} else {
|
|
|
|
[self addSubview:view];
|
|
|
|
}
|
|
|
|
[view react_remountAllSubviews];
|
|
|
|
}
|
|
|
|
index++;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// If react_subviews is nil, we must already be showing all subviews
|
|
|
|
[super react_remountAllSubviews];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)remountSubview:(UIView *)view
|
|
|
|
{
|
|
|
|
// Calculate insertion index for view
|
|
|
|
NSInteger index = 0;
|
|
|
|
for (UIView *subview in _reactSubviews) {
|
|
|
|
if (subview == view) {
|
|
|
|
[self insertSubview:view atIndex:index];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (subview.superview) {
|
|
|
|
// View is mounted, so bump the index
|
|
|
|
index++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)mountOrUnmountSubview:(UIView *)view withClipRect:(CGRect)clipRect relativeToView:(UIView *)clipView
|
|
|
|
{
|
|
|
|
if (view.clipsToBounds) {
|
|
|
|
|
|
|
|
// View has cliping enabled, so we can easily test if it is partially
|
|
|
|
// or completely within the clipRect, and mount or unmount it accordingly
|
|
|
|
|
2015-06-02 08:40:43 +00:00
|
|
|
if (!CGRectIsEmpty(CGRectIntersection(clipRect, view.frame))) {
|
2015-03-11 02:03:59 +00:00
|
|
|
|
|
|
|
// View is at least partially visible, so remount it if unmounted
|
|
|
|
if (view.superview == nil) {
|
|
|
|
[self remountSubview:view];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Then test its subviews
|
|
|
|
if (CGRectContainsRect(clipRect, view.frame)) {
|
|
|
|
[view react_remountAllSubviews];
|
|
|
|
} else {
|
|
|
|
[view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if (view.superview) {
|
|
|
|
|
|
|
|
// View is completely outside the clipRect, so unmount it
|
|
|
|
[view removeFromSuperview];
|
|
|
|
}
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// View has clipping disabled, so there's no way to tell if it has
|
|
|
|
// any visible subviews without an expensive recursive test, so we'll
|
|
|
|
// just add it.
|
|
|
|
|
|
|
|
if (view.superview == nil) {
|
|
|
|
[self remountSubview:view];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check if subviews need to be mounted/unmounted
|
|
|
|
[view react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (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 (_reactSubviews == nil) {
|
|
|
|
// Use default behavior if unmounting is disabled
|
|
|
|
return [super react_updateClippedSubviewsWithClipRect:clipRect relativeToView:clipView];
|
|
|
|
}
|
|
|
|
|
2015-08-24 10:14:33 +00:00
|
|
|
if (_reactSubviews.count == 0) {
|
2015-03-11 02:03:59 +00:00
|
|
|
// 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];
|
|
|
|
clipView = self;
|
|
|
|
if (self.clipsToBounds) {
|
|
|
|
clipRect = CGRectIntersection(clipRect, self.bounds);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Mount / unmount views
|
|
|
|
for (UIView *view in _reactSubviews) {
|
|
|
|
[self mountOrUnmountSubview:view withClipRect:clipRect relativeToView:clipView];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)setRemoveClippedSubviews:(BOOL)removeClippedSubviews
|
|
|
|
{
|
|
|
|
if (removeClippedSubviews && !_reactSubviews) {
|
|
|
|
_reactSubviews = [self.subviews mutableCopy];
|
|
|
|
} else if (!removeClippedSubviews && _reactSubviews) {
|
|
|
|
[self react_remountAllSubviews];
|
|
|
|
_reactSubviews = nil;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (BOOL)removeClippedSubviews
|
|
|
|
{
|
|
|
|
return _reactSubviews != nil;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)insertReactSubview:(UIView *)view atIndex:(NSInteger)atIndex
|
|
|
|
{
|
|
|
|
if (_reactSubviews == nil) {
|
|
|
|
[self insertSubview:view atIndex:atIndex];
|
|
|
|
} else {
|
|
|
|
[_reactSubviews insertObject:view atIndex:atIndex];
|
|
|
|
|
|
|
|
// Find a suitable view to use for clipping
|
|
|
|
UIView *clipView = [self react_findClipView];
|
|
|
|
if (clipView) {
|
|
|
|
|
|
|
|
// If possible, don't add subviews if they are clipped
|
|
|
|
[self mountOrUnmountSubview:view withClipRect:clipView.bounds relativeToView:clipView];
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
// Fallback if we can't find a suitable clipView
|
|
|
|
[self remountSubview:view];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)removeReactSubview:(UIView *)subview
|
|
|
|
{
|
|
|
|
[_reactSubviews removeObject:subview];
|
|
|
|
[subview removeFromSuperview];
|
|
|
|
}
|
|
|
|
|
2015-11-14 18:25:00 +00:00
|
|
|
- (NSArray<UIView *> *)reactSubviews
|
2015-03-11 02:03:59 +00:00
|
|
|
{
|
|
|
|
// The _reactSubviews array is only used when we have hidden
|
|
|
|
// offscreen views. If _reactSubviews is nil, we can assume
|
|
|
|
// that [self reactSubviews] and [self subviews] are the same
|
|
|
|
|
2015-08-24 10:14:33 +00:00
|
|
|
return _reactSubviews ?: self.subviews;
|
2015-03-11 02:03:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
- (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];
|
2015-03-26 08:43:17 +00:00
|
|
|
|
2015-03-11 02:03:59 +00:00
|
|
|
if (_reactSubviews) {
|
|
|
|
[self updateClippedSubviews];
|
|
|
|
}
|
2015-03-26 08:43:17 +00:00
|
|
|
}
|
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
#pragma mark - Borders
|
|
|
|
|
|
|
|
- (UIColor *)backgroundColor
|
2015-03-26 08:43:17 +00:00
|
|
|
{
|
2015-04-27 10:50:07 +00:00
|
|
|
return _backgroundColor;
|
|
|
|
}
|
2015-03-26 08:43:17 +00:00
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
|
|
|
{
|
|
|
|
if ([_backgroundColor isEqual:backgroundColor]) {
|
|
|
|
return;
|
2015-03-26 08:43:17 +00:00
|
|
|
}
|
2015-05-26 09:12:28 +00:00
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
_backgroundColor = backgroundColor;
|
|
|
|
[self.layer setNeedsDisplay];
|
2015-03-26 08:43:17 +00:00
|
|
|
}
|
|
|
|
|
2015-05-28 15:52:46 +00:00
|
|
|
- (UIEdgeInsets)bordersAsInsets
|
2015-03-26 08:43:17 +00:00
|
|
|
{
|
2015-04-27 10:50:07 +00:00
|
|
|
const CGFloat borderWidth = MAX(0, _borderWidth);
|
2015-03-26 08:43:17 +00:00
|
|
|
|
2015-05-28 15:52:46 +00:00
|
|
|
return (UIEdgeInsets) {
|
|
|
|
_borderTopWidth >= 0 ? _borderTopWidth : borderWidth,
|
|
|
|
_borderLeftWidth >= 0 ? _borderLeftWidth : borderWidth,
|
|
|
|
_borderBottomWidth >= 0 ? _borderBottomWidth : borderWidth,
|
|
|
|
_borderRightWidth >= 0 ? _borderRightWidth : borderWidth,
|
|
|
|
};
|
|
|
|
}
|
2015-03-26 08:43:17 +00:00
|
|
|
|
2015-05-28 15:52:46 +00:00
|
|
|
- (RCTCornerRadii)cornerRadii
|
|
|
|
{
|
2016-01-07 17:48:15 +00:00
|
|
|
// Get corner radii
|
2015-05-28 15:52:46 +00:00
|
|
|
const CGFloat radius = MAX(0, _borderRadius);
|
2016-01-07 17:48:15 +00:00
|
|
|
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;
|
2016-01-09 00:14:20 +00:00
|
|
|
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)));
|
2016-01-07 17:48:15 +00:00
|
|
|
|
|
|
|
// Return scaled radii
|
2015-05-28 15:52:46 +00:00
|
|
|
return (RCTCornerRadii){
|
2016-01-07 17:48:15 +00:00
|
|
|
topLeftRadius * MIN(topScaleFactor, leftScaleFactor),
|
|
|
|
topRightRadius * MIN(topScaleFactor, rightScaleFactor),
|
|
|
|
bottomLeftRadius * MIN(bottomScaleFactor, leftScaleFactor),
|
|
|
|
bottomRightRadius * MIN(bottomScaleFactor, rightScaleFactor),
|
2015-05-28 15:52:46 +00:00
|
|
|
};
|
|
|
|
}
|
2015-04-27 10:50:07 +00:00
|
|
|
|
2015-05-28 15:52:46 +00:00
|
|
|
- (RCTBorderColors)borderColors
|
|
|
|
{
|
|
|
|
return (RCTBorderColors){
|
|
|
|
_borderTopColor ?: _borderColor,
|
|
|
|
_borderLeftColor ?: _borderColor,
|
|
|
|
_borderBottomColor ?: _borderColor,
|
2015-07-14 11:53:54 +00:00
|
|
|
_borderRightColor ?: _borderColor,
|
2015-05-28 15:52:46 +00:00
|
|
|
};
|
2015-03-26 08:43:17 +00:00
|
|
|
}
|
|
|
|
|
2016-01-09 00:14:20 +00:00
|
|
|
- (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
|
|
|
|
[super reactSetFrame:frame];
|
|
|
|
[self.layer setNeedsDisplay];
|
|
|
|
}
|
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
- (void)displayLayer:(CALayer *)layer
|
2015-03-26 08:43:17 +00:00
|
|
|
{
|
2016-01-07 17:48:15 +00:00
|
|
|
if (CGSizeEqualToSize(layer.bounds.size, CGSizeZero)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-05-28 15:52:46 +00:00
|
|
|
const RCTCornerRadii cornerRadii = [self cornerRadii];
|
|
|
|
const UIEdgeInsets borderInsets = [self bordersAsInsets];
|
|
|
|
const RCTBorderColors borderColors = [self borderColors];
|
|
|
|
|
|
|
|
BOOL useIOSBorderRendering =
|
|
|
|
!RCTRunningInTestEnvironment() &&
|
|
|
|
RCTCornerRadiiAreEqual(cornerRadii) &&
|
|
|
|
RCTBorderInsetsAreEqual(borderInsets) &&
|
2015-06-23 13:43:55 +00:00
|
|
|
RCTBorderColorsAreEqual(borderColors) &&
|
2015-12-01 15:41:20 +00:00
|
|
|
_borderStyle == RCTBorderStyleSolid &&
|
2015-05-28 15:52:46 +00:00
|
|
|
|
2015-06-23 13:43:55 +00:00
|
|
|
// 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 || 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.
|
2015-05-28 15:52:46 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2015-12-22 16:30:00 +00:00
|
|
|
UIImage *image = RCTGetBorderImage(_borderStyle,
|
|
|
|
layer.bounds.size,
|
|
|
|
cornerRadii,
|
2015-12-01 15:41:20 +00:00
|
|
|
borderInsets,
|
|
|
|
borderColors,
|
2015-05-28 15:52:46 +00:00
|
|
|
_backgroundColor.CGColor,
|
|
|
|
self.clipsToBounds);
|
|
|
|
|
2015-12-22 16:30:00 +00:00
|
|
|
layer.backgroundColor = NULL;
|
|
|
|
|
|
|
|
if (image == nil) {
|
|
|
|
layer.contents = nil;
|
|
|
|
layer.needsDisplayOnBoundsChange = NO;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2015-07-30 13:20:28 +00:00
|
|
|
CGRect contentsCenter = ({
|
2015-05-28 15:52:46 +00:00
|
|
|
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
|
|
|
|
);
|
|
|
|
});
|
2015-03-26 08:43:17 +00:00
|
|
|
|
2015-05-28 15:52:46 +00:00
|
|
|
if (RCTRunningInTestEnvironment()) {
|
2015-04-27 10:50:07 +00:00
|
|
|
const CGSize size = self.bounds.size;
|
|
|
|
UIGraphicsBeginImageContextWithOptions(size, NO, image.scale);
|
|
|
|
[image drawInRect:(CGRect){CGPointZero, size}];
|
|
|
|
image = UIGraphicsGetImageFromCurrentImageContext();
|
|
|
|
UIGraphicsEndImageContext();
|
2015-07-30 13:20:28 +00:00
|
|
|
contentsCenter = CGRectMake(0, 0, 1, 1);
|
2015-04-27 10:50:07 +00:00
|
|
|
}
|
2015-03-26 08:43:17 +00:00
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
layer.contents = (id)image.CGImage;
|
2015-05-28 15:52:46 +00:00
|
|
|
layer.contentsScale = image.scale;
|
|
|
|
layer.needsDisplayOnBoundsChange = YES;
|
2015-12-22 16:30:00 +00:00
|
|
|
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);
|
|
|
|
}
|
2015-05-26 09:12:28 +00:00
|
|
|
|
|
|
|
[self updateClippingForLayer:layer];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)updateClippingForLayer:(CALayer *)layer
|
|
|
|
{
|
|
|
|
CALayer *mask = nil;
|
|
|
|
CGFloat cornerRadius = 0;
|
|
|
|
|
|
|
|
if (self.clipsToBounds) {
|
|
|
|
|
2015-05-28 15:52:46 +00:00
|
|
|
const RCTCornerRadii cornerRadii = [self cornerRadii];
|
|
|
|
if (RCTCornerRadiiAreEqual(cornerRadii)) {
|
2015-05-26 09:12:28 +00:00
|
|
|
|
2015-05-28 15:52:46 +00:00
|
|
|
cornerRadius = cornerRadii.topLeft;
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
|
|
|
|
CGPathRef path = RCTPathCreateWithRoundedRect(self.bounds, RCTGetCornerInsets(cornerRadii, UIEdgeInsetsZero), NULL);
|
|
|
|
shapeLayer.path = path;
|
|
|
|
CGPathRelease(path);
|
|
|
|
mask = shapeLayer;
|
2015-05-26 09:12:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
layer.cornerRadius = cornerRadius;
|
|
|
|
layer.mask = mask;
|
2015-03-26 08:43:17 +00:00
|
|
|
}
|
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
#pragma mark Border Color
|
2015-03-26 08:43:17 +00:00
|
|
|
|
2015-07-14 11:53:54 +00:00
|
|
|
#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]; \
|
2015-04-27 10:50:07 +00:00
|
|
|
}
|
2015-03-26 08:43:17 +00:00
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
setBorderColor()
|
|
|
|
setBorderColor(Top)
|
|
|
|
setBorderColor(Right)
|
|
|
|
setBorderColor(Bottom)
|
|
|
|
setBorderColor(Left)
|
2015-03-26 08:43:17 +00:00
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
#pragma mark - Border Width
|
|
|
|
|
2015-07-14 11:53:54 +00:00
|
|
|
#define setBorderWidth(side) \
|
|
|
|
- (void)setBorder##side##Width:(CGFloat)width \
|
|
|
|
{ \
|
|
|
|
if (_border##side##Width == width) { \
|
|
|
|
return; \
|
|
|
|
} \
|
|
|
|
_border##side##Width = width; \
|
|
|
|
[self.layer setNeedsDisplay]; \
|
2015-03-26 08:43:17 +00:00
|
|
|
}
|
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
setBorderWidth()
|
|
|
|
setBorderWidth(Top)
|
|
|
|
setBorderWidth(Right)
|
|
|
|
setBorderWidth(Bottom)
|
|
|
|
setBorderWidth(Left)
|
|
|
|
|
2015-12-01 15:41:20 +00:00
|
|
|
#pragma mark - Border Radius
|
|
|
|
|
2015-07-14 11:53:54 +00:00
|
|
|
#define setBorderRadius(side) \
|
|
|
|
- (void)setBorder##side##Radius:(CGFloat)radius \
|
|
|
|
{ \
|
|
|
|
if (_border##side##Radius == radius) { \
|
|
|
|
return; \
|
|
|
|
} \
|
|
|
|
_border##side##Radius = radius; \
|
|
|
|
[self.layer setNeedsDisplay]; \
|
2015-05-13 15:22:21 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
setBorderRadius()
|
|
|
|
setBorderRadius(TopLeft)
|
|
|
|
setBorderRadius(TopRight)
|
|
|
|
setBorderRadius(BottomLeft)
|
|
|
|
setBorderRadius(BottomRight)
|
|
|
|
|
2015-12-01 15:41:20 +00:00
|
|
|
#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()
|
|
|
|
|
2015-07-14 11:53:54 +00:00
|
|
|
- (void)dealloc
|
|
|
|
{
|
|
|
|
CGColorRelease(_borderColor);
|
|
|
|
CGColorRelease(_borderTopColor);
|
|
|
|
CGColorRelease(_borderRightColor);
|
|
|
|
CGColorRelease(_borderBottomColor);
|
|
|
|
CGColorRelease(_borderLeftColor);
|
|
|
|
}
|
|
|
|
|
2015-04-27 10:50:07 +00:00
|
|
|
@end
|