react-native/Libraries/Text/RCTText.m

182 lines
5.6 KiB
Objective-C

/**
* 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.
*/
#import "RCTText.h"
#import "RCTShadowText.h"
#import "RCTUtils.h"
#import "UIView+React.h"
static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDescendants)
{
for (UIView *child in view.reactSubviews) {
if ([child isKindOfClass:[RCTText class]]) {
collectNonTextDescendants((RCTText *)child, nonTextDescendants);
} else if (!CGRectEqualToRect(child.frame, CGRectZero)) {
[nonTextDescendants addObject:child];
}
}
}
@implementation RCTText
{
NSTextStorage *_textStorage;
CAShapeLayer *_highlightLayer;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if ((self = [super initWithFrame:frame])) {
_textStorage = [NSTextStorage new];
self.isAccessibilityElement = YES;
self.accessibilityTraits |= UIAccessibilityTraitStaticText;
self.opaque = NO;
self.contentMode = UIViewContentModeRedraw;
}
return self;
}
- (NSString *)description
{
NSString *superDescription = super.description;
NSRange semicolonRange = [superDescription rangeOfString:@";"];
NSString *replacement = [NSString stringWithFormat:@"; reactTag: %@; text: %@", self.reactTag, self.textStorage.string];
return [superDescription stringByReplacingCharactersInRange:semicolonRange withString:replacement];
}
- (void)reactSetFrame:(CGRect)frame
{
// Text looks super weird if its frame is animated.
// This disables the frame animation, without affecting opacity, etc.
[UIView performWithoutAnimation:^{
[super reactSetFrame:frame];
}];
}
- (void)reactSetInheritedBackgroundColor:(UIColor *)inheritedBackgroundColor
{
self.backgroundColor = inheritedBackgroundColor;
}
- (void)reactUpdateSubviews
{
// Do nothing, as subviews are managed by `setTextStorage:` method
}
- (void)setTextStorage:(NSTextStorage *)textStorage
{
if (_textStorage != textStorage) {
_textStorage = textStorage;
// Update subviews
NSMutableArray *nonTextDescendants = [NSMutableArray new];
collectNonTextDescendants(self, nonTextDescendants);
NSArray *subviews = self.subviews;
if (![subviews isEqualToArray:nonTextDescendants]) {
for (UIView *child in subviews) {
if (![nonTextDescendants containsObject:child]) {
[child removeFromSuperview];
}
}
for (UIView *child in nonTextDescendants) {
[self addSubview:child];
}
}
[self setNeedsDisplay];
}
}
- (void)drawRect:(CGRect)rect
{
NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, _contentInset);
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textFrame.origin];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin];
__block UIBezierPath *highlightPath = nil;
NSRange characterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
[layoutManager.textStorage enumerateAttribute:RCTIsHighlightedAttributeName inRange:characterRange options:0 usingBlock:^(NSNumber *value, NSRange range, BOOL *_) {
if (!value.boolValue) {
return;
}
[layoutManager enumerateEnclosingRectsForGlyphRange:range withinSelectedGlyphRange:range inTextContainer:textContainer usingBlock:^(CGRect enclosingRect, __unused BOOL *__) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(enclosingRect, -2, -2) cornerRadius:2];
if (highlightPath) {
[highlightPath appendPath:path];
} else {
highlightPath = path;
}
}];
}];
if (highlightPath) {
if (!_highlightLayer) {
_highlightLayer = [CAShapeLayer layer];
_highlightLayer.fillColor = [UIColor colorWithWhite:0 alpha:0.25].CGColor;
[self.layer addSublayer:_highlightLayer];
}
_highlightLayer.position = (CGPoint){_contentInset.left, _contentInset.top};
_highlightLayer.path = highlightPath.CGPath;
} else {
[_highlightLayer removeFromSuperlayer];
_highlightLayer = nil;
}
}
- (NSNumber *)reactTagAtPoint:(CGPoint)point
{
NSNumber *reactTag = self.reactTag;
CGFloat fraction;
NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
NSUInteger characterIndex = [layoutManager characterIndexForPoint:point
inTextContainer:textContainer
fractionOfDistanceBetweenInsertionPoints:&fraction];
// If the point is not before (fraction == 0.0) the first character and not
// after (fraction == 1.0) the last character, then the attribute is valid.
if (_textStorage.length > 0 && (fraction > 0 || characterIndex > 0) && (fraction < 1 || characterIndex < _textStorage.length - 1)) {
reactTag = [_textStorage attribute:RCTReactTagAttributeName atIndex:characterIndex effectiveRange:NULL];
}
return reactTag;
}
- (void)didMoveToWindow
{
[super didMoveToWindow];
if (!self.window) {
self.layer.contents = nil;
if (_highlightLayer) {
[_highlightLayer removeFromSuperlayer];
_highlightLayer = nil;
}
} else if (_textStorage.length) {
[self setNeedsDisplay];
}
}
#pragma mark - Accessibility
- (NSString *)accessibilityLabel
{
return _textStorage.string;
}
@end