mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Summary: Fixes https://github.com/facebook/react-native/issues/27137 This PR fixes an issue on iOS where RCTTextView height is not calculated as it should for some fonts where font `leading` attributed is not equal to zero, which results in wrong baseline alignment behaviour. The fix for this is by setting `usesFontLeading` property of `NSLayoutManager` to `NO`, which results is a layout behavior that is similar to `UILabel` Probably the documentation for `usesFontLeading` describes why UILabel has a different (correct) layout behavior in that case > // By default, a layout manager will use leading as specified by the font. However, this is not appropriate for most UI text, for which a fixed leading is usually specified by UI layout guidelines. These methods allow the use of the font's leading to be turned off. ## Changelog [iOS] [Fixed] - Fix RCTTextView layout issue that happens on some font with `leading` attribute not equal to zero, which causes wrong base-alignment layout Pull Request resolved: https://github.com/facebook/react-native/pull/27195 Test Plan: Below are the test results before and after the change, and comparing that to native UILabel behavior. The test is done with using system font and custom font (`GothamNarrow-Medium`) and font size 50 [GothamNarrow-Medium.otf.zip](https://github.com/facebook/react-native/files/3832143/GothamNarrow-Medium.otf.zip) ```js const App: () => React$Node = () => { return ( <View style={{flex: 1, margin: 40, flexDirection: 'row', justifyContent: 'center', alignItems: 'baseline'}}> <View style={{width: 30, height: 30, backgroundColor: 'lightgray'}} /> <Text style={{fontSize: 50, backgroundColor: 'green', fontFamily: 'GothamNarrow-Medium'}}>{'Settings'}</Text> </View> ); }; ``` ------- ### Before the fix <img width="962" alt="Screenshot 2019-11-11 at 16 53 26" src="https://user-images.githubusercontent.com/5355138/68601049-dd778780-04a3-11ea-879e-cc7b4eb2af95.png"> ----- ### After the fix <img width="944" alt="Screenshot 2019-11-11 at 16 55 11" src="https://user-images.githubusercontent.com/5355138/68601180-1d3e6f00-04a4-11ea-87bc-61c6fa2cdb18.png"> ----- ### Using `UILabel` <img width="805" alt="Screenshot 2019-11-11 at 16 59 28" src="https://user-images.githubusercontent.com/5355138/68601487-b2d9fe80-04a4-11ea-9a0f-c025c7753c24.png"> Differential Revision: D19576556 Pulled By: shergin fbshipit-source-id: 4eaafdab963c3f53c461884c581e205e6426718a
167 lines
7.0 KiB
Plaintext
167 lines
7.0 KiB
Plaintext
/*
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
#import "RCTTextLayoutManager.h"
|
|
|
|
#import "NSTextStorage+FontScaling.h"
|
|
#import "RCTAttributedTextUtils.h"
|
|
|
|
#import <react/utils/SimpleThreadSafeCache.h>
|
|
|
|
using namespace facebook::react;
|
|
|
|
@implementation RCTTextLayoutManager {
|
|
SimpleThreadSafeCache<AttributedString, std::shared_ptr<const void>, 256> _cache;
|
|
}
|
|
|
|
static NSLineBreakMode RCTNSLineBreakModeFromEllipsizeMode(EllipsizeMode ellipsizeMode)
|
|
{
|
|
switch (ellipsizeMode) {
|
|
case EllipsizeMode::Clip:
|
|
return NSLineBreakByClipping;
|
|
case EllipsizeMode::Head:
|
|
return NSLineBreakByTruncatingHead;
|
|
case EllipsizeMode::Tail:
|
|
return NSLineBreakByTruncatingTail;
|
|
case EllipsizeMode::Middle:
|
|
return NSLineBreakByTruncatingMiddle;
|
|
}
|
|
}
|
|
|
|
- (facebook::react::Size)measureNSAttributedString:(NSAttributedString *)attributedString
|
|
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
|
|
layoutConstraints:(LayoutConstraints)layoutConstraints
|
|
{
|
|
if (attributedString.length == 0) {
|
|
// This is not really an optimization because that should be checked much earlier on the call stack.
|
|
// Sometimes, very irregularly, measuring an empty string crashes/freezes iOS internal text infrastructure.
|
|
// This is our last line of defense.
|
|
return {0, 0};
|
|
}
|
|
|
|
CGSize maximumSize = CGSize{layoutConstraints.maximumSize.width, CGFLOAT_MAX};
|
|
|
|
NSTextStorage *textStorage = [self _textStorageAndLayoutManagerWithAttributesString:attributedString
|
|
paragraphAttributes:paragraphAttributes
|
|
size:maximumSize];
|
|
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
[layoutManager ensureLayoutForTextContainer:textContainer];
|
|
|
|
CGSize size = [layoutManager usedRectForTextContainer:textContainer].size;
|
|
|
|
return {size.width, size.height};
|
|
}
|
|
|
|
- (facebook::react::Size)measureAttributedString:(AttributedString)attributedString
|
|
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
|
|
layoutConstraints:(LayoutConstraints)layoutConstraints
|
|
{
|
|
return [self measureNSAttributedString:[self _nsAttributedStringFromAttributedString:attributedString]
|
|
paragraphAttributes:paragraphAttributes
|
|
layoutConstraints:layoutConstraints];
|
|
}
|
|
|
|
- (void)drawAttributedString:(AttributedString)attributedString
|
|
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
|
|
frame:(CGRect)frame {
|
|
NSTextStorage *textStorage = [self
|
|
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
|
|
paragraphAttributes:paragraphAttributes
|
|
size:frame.size];
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
|
|
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
|
|
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:frame.origin];
|
|
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:frame.origin];
|
|
}
|
|
|
|
- (NSTextStorage *)
|
|
_textStorageAndLayoutManagerWithAttributesString:
|
|
(NSAttributedString *)attributedString
|
|
paragraphAttributes:
|
|
(ParagraphAttributes)paragraphAttributes
|
|
size:(CGSize)size {
|
|
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:size];
|
|
|
|
textContainer.lineFragmentPadding = 0.0; // Note, the default value is 5.
|
|
textContainer.lineBreakMode = paragraphAttributes.maximumNumberOfLines > 0
|
|
? RCTNSLineBreakModeFromEllipsizeMode(paragraphAttributes.ellipsizeMode)
|
|
: NSLineBreakByClipping;
|
|
textContainer.maximumNumberOfLines = paragraphAttributes.maximumNumberOfLines;
|
|
|
|
NSLayoutManager *layoutManager = [NSLayoutManager new];
|
|
layoutManager.usesFontLeading = NO;
|
|
[layoutManager addTextContainer:textContainer];
|
|
|
|
NSTextStorage *textStorage =
|
|
[[NSTextStorage alloc] initWithAttributedString:attributedString];
|
|
|
|
[textStorage addLayoutManager:layoutManager];
|
|
|
|
if (paragraphAttributes.adjustsFontSizeToFit) {
|
|
CGFloat minimumFontSize = !isnan(paragraphAttributes.minimumFontSize)
|
|
? paragraphAttributes.minimumFontSize
|
|
: 4.0;
|
|
CGFloat maximumFontSize = !isnan(paragraphAttributes.maximumFontSize)
|
|
? paragraphAttributes.maximumFontSize
|
|
: 96.0;
|
|
[textStorage scaleFontSizeToFitSize:size
|
|
minimumFontSize:minimumFontSize
|
|
maximumFontSize:maximumFontSize];
|
|
}
|
|
|
|
return textStorage;
|
|
}
|
|
|
|
- (SharedEventEmitter)
|
|
getEventEmitterWithAttributeString:(AttributedString)attributedString
|
|
paragraphAttributes:(ParagraphAttributes)paragraphAttributes
|
|
frame:(CGRect)frame
|
|
atPoint:(CGPoint)point {
|
|
NSTextStorage *textStorage = [self
|
|
_textStorageAndLayoutManagerWithAttributesString:[self _nsAttributedStringFromAttributedString:attributedString]
|
|
paragraphAttributes:paragraphAttributes
|
|
size:frame.size];
|
|
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
|
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
|
|
|
CGFloat fraction;
|
|
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)) {
|
|
RCTWeakEventEmitterWrapper *eventEmitterWrapper =
|
|
(RCTWeakEventEmitterWrapper *)[textStorage
|
|
attribute:RCTAttributedStringEventEmitterKey
|
|
atIndex:characterIndex
|
|
effectiveRange:NULL];
|
|
return eventEmitterWrapper.eventEmitter;
|
|
}
|
|
|
|
return nil;
|
|
}
|
|
|
|
- (NSAttributedString *)_nsAttributedStringFromAttributedString:(AttributedString)attributedString
|
|
{
|
|
auto sharedNSAttributedString = _cache.get(attributedString, [](const AttributedString attributedString) {
|
|
return std::shared_ptr<void>(
|
|
(__bridge_retained void *)RCTNSAttributedStringFromAttributedString(attributedString), CFRelease);
|
|
});
|
|
|
|
return (__bridge NSAttributedString *)sharedNSAttributedString.get();
|
|
}
|
|
|
|
@end
|