This app provides monitoring and information features for the common freifunk user and the technical stuff of a freifunk community.
Code base is taken from a TUM Practical Course project and added here to see if Freifunk Altdorf can use it.
https://www.freifunk-altdorf.de
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
831 lines
27 KiB
831 lines
27 KiB
/** |
|
* Copyright (c) 2015-present, Facebook, Inc. |
|
* |
|
* 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 "RCTLog.h" |
|
#import "RCTUtils.h" |
|
#import "UIView+React.h" |
|
#import "RCTI18nUtil.h" |
|
|
|
@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; |
|
} |
|
|
|
@implementation RCTView |
|
{ |
|
UIColor *_backgroundColor; |
|
} |
|
|
|
- (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 (!_accessibilityActions.count) { |
|
return nil; |
|
} |
|
|
|
NSMutableArray *actions = [NSMutableArray array]; |
|
for (NSString *action in _accessibilityActions) { |
|
[actions addObject:[[UIAccessibilityCustomAction alloc] initWithName:action |
|
target:self |
|
selector:@selector(didActivateAccessibilityCustomAction:)]]; |
|
} |
|
|
|
return [actions copy]; |
|
} |
|
|
|
- (BOOL)didActivateAccessibilityCustomAction:(UIAccessibilityCustomAction *)action |
|
{ |
|
if (!_onAccessibilityAction) { |
|
return NO; |
|
} |
|
|
|
_onAccessibilityAction(@{ |
|
@"action": action.name, |
|
@"target": self.reactTag |
|
}); |
|
|
|
return YES; |
|
} |
|
|
|
- (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)accessibilityActivate |
|
{ |
|
if (_onAccessibilityTap) { |
|
_onAccessibilityTap(nil); |
|
return YES; |
|
} else { |
|
return NO; |
|
} |
|
} |
|
|
|
- (BOOL)accessibilityPerformMagicTap |
|
{ |
|
if (_onMagicTap) { |
|
_onMagicTap(nil); |
|
return YES; |
|
} else { |
|
return NO; |
|
} |
|
} |
|
|
|
- (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]; |
|
} |
|
} |
|
|
|
#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 = |
|
!RCTRunningInTestEnvironment() && |
|
RCTCornerRadiiAreEqual(cornerRadii) && |
|
RCTBorderInsetsAreEqual(borderInsets) && |
|
RCTBorderColorsAreEqual(borderColors) && |
|
_borderStyle == RCTBorderStyleSolid && |
|
|
|
// iOS draws borders in front of the content whereas CSS draws them behind |
|
// the content. For this reason, only use iOS border drawing when clipping |
|
// or when the border is hidden. |
|
|
|
(borderInsets.top == 0 || (borderColors.top && CGColorGetAlpha(borderColors.top) == 0) || self.clipsToBounds); |
|
|
|
// iOS clips to the outside of the border, but CSS clips to the inside. To |
|
// solve this, we'll need to add a container view inside the main view to |
|
// correctly clip the subviews. |
|
|
|
if (useIOSBorderRendering) { |
|
layer.cornerRadius = cornerRadii.topLeft; |
|
layer.borderColor = borderColors.left; |
|
layer.borderWidth = borderInsets.left; |
|
layer.backgroundColor = _backgroundColor.CGColor; |
|
layer.contents = nil; |
|
layer.needsDisplayOnBoundsChange = NO; |
|
layer.mask = nil; |
|
return; |
|
} |
|
|
|
UIImage *image = RCTGetBorderImage(_borderStyle, |
|
layer.bounds.size, |
|
cornerRadii, |
|
borderInsets, |
|
borderColors, |
|
_backgroundColor.CGColor, |
|
self.clipsToBounds); |
|
|
|
layer.backgroundColor = NULL; |
|
|
|
if (image == nil) { |
|
layer.contents = nil; |
|
layer.needsDisplayOnBoundsChange = NO; |
|
return; |
|
} |
|
|
|
CGRect contentsCenter = ({ |
|
CGSize size = image.size; |
|
UIEdgeInsets insets = image.capInsets; |
|
CGRectMake( |
|
insets.left / size.width, |
|
insets.top / size.height, |
|
1.0 / size.width, |
|
1.0 / size.height |
|
); |
|
}); |
|
|
|
if (RCTRunningInTestEnvironment()) { |
|
const CGSize size = self.bounds.size; |
|
UIGraphicsBeginImageContextWithOptions(size, NO, image.scale); |
|
[image drawInRect:(CGRect){CGPointZero, size}]; |
|
image = UIGraphicsGetImageFromCurrentImageContext(); |
|
UIGraphicsEndImageContext(); |
|
contentsCenter = CGRectMake(0, 0, 1, 1); |
|
} |
|
|
|
layer.contents = (id)image.CGImage; |
|
layer.contentsScale = image.scale; |
|
layer.needsDisplayOnBoundsChange = YES; |
|
layer.magnificationFilter = kCAFilterNearest; |
|
|
|
const BOOL isResizable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); |
|
if (isResizable) { |
|
layer.contentsCenter = contentsCenter; |
|
} else { |
|
layer.contentsCenter = CGRectMake(0.0, 0.0, 1.0, 1.0); |
|
} |
|
|
|
[self updateClippingForLayer:layer]; |
|
} |
|
|
|
static BOOL RCTLayerHasShadow(CALayer *layer) |
|
{ |
|
return layer.shadowOpacity * CGColorGetAlpha(layer.shadowColor) > 0; |
|
} |
|
|
|
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
|
|
|