mirror of
https://github.com/status-im/react-native.git
synced 2025-01-09 09:12:02 +00:00
486dbe4e8f
Summary: Previously, only Text and Image could be nested within Text. Now, any view can be nested within Text. One restriction of this feature is that developers must give inline views a width and a height via the style prop. Previously, inline Images were supported by using iOS's built-in support for rendering images with an NSAttributedString via NSTextAttachment. However, NSAttributedString doesn't support rendering arbitrary views. This change adds support for nesting views within Text by creating one NSTextAttachment per inline view. The NSTextAttachments act as placeholders. They are set to be the size of the corresponding view. After the text is laid out, we query the text system to find out where it has positioned each NSTextAttachment. We then position the views to be at those locations. This commit also contains a change in `RCTShadowText.m` `_setParagraphStyleOnAttributedString:heightOfTallestSubview:`. It now only sets `lineHeight`, `textAlign`, and `writingDirection` when they've actua Closes https://github.com/facebook/react-native/pull/7304 Reviewed By: javache Differential Revision: D3365373 Pulled By: nicklockwood fbshipit-source-id: 66d149eb80c5c6725311e1e46d7323eec086ce64
193 lines
5.9 KiB
Objective-C
193 lines
5.9 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;
|
|
NSMutableArray<UIView *> *_reactSubviews;
|
|
CAShapeLayer *_highlightLayer;
|
|
}
|
|
|
|
- (instancetype)initWithFrame:(CGRect)frame
|
|
{
|
|
if ((self = [super initWithFrame:frame])) {
|
|
_textStorage = [NSTextStorage new];
|
|
_reactSubviews = [NSMutableArray array];
|
|
|
|
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)insertReactSubview:(UIView *)subview atIndex:(NSInteger)atIndex
|
|
{
|
|
[_reactSubviews insertObject:subview atIndex:atIndex];
|
|
}
|
|
|
|
- (void)removeReactSubview:(UIView *)subview
|
|
{
|
|
[_reactSubviews removeObject:subview];
|
|
}
|
|
|
|
- (NSArray<UIView *> *)reactSubviews
|
|
{
|
|
return _reactSubviews;
|
|
}
|
|
|
|
- (void)setTextStorage:(NSTextStorage *)textStorage
|
|
{
|
|
if (_textStorage != textStorage) {
|
|
_textStorage = textStorage;
|
|
|
|
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
|