mirror of
https://github.com/status-im/react-native.git
synced 2025-02-26 08:05:34 +00:00
Initial implementation of adjustsFontSizeToFit.
Summary: Closes https://github.com/facebook/react-native/pull/4026 Differential Revision: D2678492 Pulled By: nicklockwood fbshipit-source-id: 0467814f810fee997ac50960ffb1daa74d52acba
This commit is contained in:
parent
6775d1f136
commit
c6b6f53ae7
@ -29,6 +29,7 @@ var {
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
LayoutAnimation,
|
||||
} = ReactNative;
|
||||
|
||||
class Entity extends React.Component {
|
||||
@ -81,6 +82,86 @@ class AttributeToggler extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
var AdjustingFontSize = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {dynamicText:'', shouldRender: true,};
|
||||
},
|
||||
reset: function() {
|
||||
LayoutAnimation.easeInEaseOut();
|
||||
this.setState({
|
||||
shouldRender: false,
|
||||
});
|
||||
setTimeout(()=>{
|
||||
LayoutAnimation.easeInEaseOut();
|
||||
this.setState({
|
||||
dynamicText: '',
|
||||
shouldRender: true,
|
||||
});
|
||||
}, 300);
|
||||
},
|
||||
addText: function() {
|
||||
this.setState({
|
||||
dynamicText: this.state.dynamicText + (Math.floor((Math.random() * 10) % 2) ? ' foo' : ' bar'),
|
||||
});
|
||||
},
|
||||
removeText: function() {
|
||||
this.setState({
|
||||
dynamicText: this.state.dynamicText.slice(0, this.state.dynamicText.length - 4),
|
||||
});
|
||||
},
|
||||
render: function() {
|
||||
|
||||
if (!this.state.shouldRender) {
|
||||
return (<View/>);
|
||||
}
|
||||
return (
|
||||
<View>
|
||||
<Text lineBreakMode="tail" numberOfLines={1} style={{fontSize: 36, marginVertical:6}}>
|
||||
Truncated text is baaaaad.
|
||||
</Text>
|
||||
<Text numberOfLines={1} adjustsFontSizeToFit={true} style={{fontSize: 40, marginVertical:6}}>
|
||||
Shrinking to fit available space is much better!
|
||||
</Text>
|
||||
|
||||
<Text adjustsFontSizeToFit={true} numberOfLines={1} style={{fontSize:30, marginVertical:6}}>
|
||||
{'Add text to me to watch me shrink!' + ' ' + this.state.dynamicText}
|
||||
</Text>
|
||||
|
||||
<Text adjustsFontSizeToFit={true} numberOfLines={4} style={{fontSize:20, marginVertical:6}}>
|
||||
{'Multiline text component shrinking is supported, watch as this reeeeaaaally loooooong teeeeeeext grooooows and then shriiiinks as you add text to me! ioahsdia soady auydoa aoisyd aosdy ' + ' ' + this.state.dynamicText}
|
||||
</Text>
|
||||
|
||||
<Text adjustsFontSizeToFit={true} numberOfLines={1} style={{marginVertical:6}}>
|
||||
<Text style={{fontSize:14}}>
|
||||
{'Differently sized nested elements will shrink together. '}
|
||||
</Text>
|
||||
<Text style={{fontSize:20}}>
|
||||
{'LARGE TEXT! ' + this.state.dynamicText}
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<View style={{flexDirection:'row', justifyContent:'space-around', marginTop: 5, marginVertical:6}}>
|
||||
<Text
|
||||
style={{backgroundColor: '#ffaaaa'}}
|
||||
onPress={this.reset}>
|
||||
Reset
|
||||
</Text>
|
||||
<Text
|
||||
style={{backgroundColor: '#aaaaff'}}
|
||||
onPress={this.removeText}>
|
||||
Remove Text
|
||||
</Text>
|
||||
<Text
|
||||
style={{backgroundColor: '#aaffaa'}}
|
||||
onPress={this.addText}>
|
||||
Add Text
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
exports.title = '<Text>';
|
||||
exports.description = 'Base component for rendering styled text.';
|
||||
exports.displayName = 'TextExample';
|
||||
@ -492,6 +573,11 @@ exports.examples = [
|
||||
</View>
|
||||
);
|
||||
},
|
||||
}, {
|
||||
title: 'Dynamic Font Size Adjustment',
|
||||
render: function(): ReactElement<any> {
|
||||
return <AdjustingFontSize />;
|
||||
},
|
||||
}];
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
|
@ -10,6 +10,14 @@
|
||||
#import "RCTShadowView.h"
|
||||
#import "RCTTextDecorationLineType.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, RCTSizeComparison)
|
||||
{
|
||||
RCTSizeTooLarge,
|
||||
RCTSizeTooSmall,
|
||||
RCTSizeWithinRange,
|
||||
};
|
||||
|
||||
|
||||
extern NSString *const RCTIsHighlightedAttributeName;
|
||||
extern NSString *const RCTReactTagAttributeName;
|
||||
|
||||
@ -38,6 +46,8 @@ extern NSString *const RCTReactTagAttributeName;
|
||||
@property (nonatomic, assign) CGSize textShadowOffset;
|
||||
@property (nonatomic, assign) CGFloat textShadowRadius;
|
||||
@property (nonatomic, strong) UIColor *textShadowColor;
|
||||
@property (nonatomic, assign) BOOL adjustsFontSizeToFit;
|
||||
@property (nonatomic, assign) CGFloat minimumFontScale;
|
||||
|
||||
- (void)recomputeText;
|
||||
|
||||
|
@ -24,6 +24,11 @@ NSString *const RCTShadowViewAttributeName = @"RCTShadowViewAttributeName";
|
||||
NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName";
|
||||
NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName";
|
||||
|
||||
CGFloat const RCTTextAutoSizeDefaultMinimumFontScale = 0.5f;
|
||||
CGFloat const RCTTextAutoSizeWidthErrorMargin = 0.05f;
|
||||
CGFloat const RCTTextAutoSizeHeightErrorMargin = 0.025f;
|
||||
CGFloat const RCTTextAutoSizeGranularity = 0.001f;
|
||||
|
||||
@implementation RCTShadowText
|
||||
{
|
||||
NSTextStorage *_cachedTextStorage;
|
||||
@ -37,6 +42,7 @@ static CSSSize RCTMeasure(void *context, float width, CSSMeasureMode widthMode,
|
||||
{
|
||||
RCTShadowText *shadowText = (__bridge RCTShadowText *)context;
|
||||
NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width widthMode:widthMode];
|
||||
[shadowText calculateTextFrame:textStorage];
|
||||
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
|
||||
NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
|
||||
CGSize computedSize = [layoutManager usedRectForTextContainer:textContainer].size;
|
||||
@ -105,10 +111,13 @@ static CSSSize RCTMeasure(void *context, float width, CSSMeasureMode widthMode,
|
||||
UIEdgeInsets padding = self.paddingAsInsets;
|
||||
CGFloat width = self.frame.size.width - (padding.left + padding.right);
|
||||
|
||||
|
||||
NSNumber *parentTag = [[self reactSuperview] reactTag];
|
||||
NSTextStorage *textStorage = [self buildTextStorageForWidth:width widthMode:CSSMeasureModeExactly];
|
||||
CGRect textFrame = [self calculateTextFrame:textStorage];
|
||||
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
|
||||
RCTText *view = (RCTText *)viewRegistry[self.reactTag];
|
||||
view.textFrame = textFrame;
|
||||
view.textStorage = textStorage;
|
||||
|
||||
/**
|
||||
@ -452,6 +461,155 @@ static CSSSize RCTMeasure(void *context, float width, CSSMeasureMode widthMode,
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Autosizing
|
||||
|
||||
- (CGRect)calculateTextFrame:(NSTextStorage *)textStorage
|
||||
{
|
||||
CGRect textFrame = UIEdgeInsetsInsetRect((CGRect){CGPointZero, self.frame.size},
|
||||
self.paddingAsInsets);
|
||||
|
||||
|
||||
if (_adjustsFontSizeToFit) {
|
||||
textFrame = [self updateStorage:textStorage toFitFrame:textFrame];
|
||||
}
|
||||
|
||||
return textFrame;
|
||||
}
|
||||
|
||||
- (CGRect)updateStorage:(NSTextStorage *)textStorage toFitFrame:(CGRect)frame
|
||||
{
|
||||
|
||||
BOOL fits = [self attemptScale:1.0f
|
||||
inStorage:textStorage
|
||||
forFrame:frame];
|
||||
CGSize requiredSize;
|
||||
if (!fits) {
|
||||
requiredSize = [self calculateOptimumScaleInFrame:frame
|
||||
forStorage:textStorage
|
||||
minScale:self.minimumFontScale
|
||||
maxScale:1.0
|
||||
prevMid:INT_MAX];
|
||||
} else {
|
||||
requiredSize = [self calculateSize:textStorage];
|
||||
}
|
||||
|
||||
//Vertically center draw position for new text sizing.
|
||||
frame.origin.y = self.paddingAsInsets.top + RCTRoundPixelValue((CGRectGetHeight(frame) - requiredSize.height) / 2.0f);
|
||||
return frame;
|
||||
}
|
||||
|
||||
- (CGSize)calculateOptimumScaleInFrame:(CGRect)frame
|
||||
forStorage:(NSTextStorage *)textStorage
|
||||
minScale:(CGFloat)minScale
|
||||
maxScale:(CGFloat)maxScale
|
||||
prevMid:(CGFloat)prevMid
|
||||
{
|
||||
CGFloat midScale = (minScale + maxScale) / 2.0f;
|
||||
if (round((prevMid / RCTTextAutoSizeGranularity)) == round((midScale / RCTTextAutoSizeGranularity))) {
|
||||
//Bail because we can't meet error margin.
|
||||
return [self calculateSize:textStorage];
|
||||
} else {
|
||||
RCTSizeComparison comparison = [self attemptScale:midScale
|
||||
inStorage:textStorage
|
||||
forFrame:frame];
|
||||
if (comparison == RCTSizeWithinRange) {
|
||||
return [self calculateSize:textStorage];
|
||||
} else if (comparison == RCTSizeTooLarge) {
|
||||
return [self calculateOptimumScaleInFrame:frame
|
||||
forStorage:textStorage
|
||||
minScale:minScale
|
||||
maxScale:midScale - RCTTextAutoSizeGranularity
|
||||
prevMid:midScale];
|
||||
} else {
|
||||
return [self calculateOptimumScaleInFrame:frame
|
||||
forStorage:textStorage
|
||||
minScale:midScale + RCTTextAutoSizeGranularity
|
||||
maxScale:maxScale
|
||||
prevMid:midScale];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (RCTSizeComparison)attemptScale:(CGFloat)scale
|
||||
inStorage:(NSTextStorage *)textStorage
|
||||
forFrame:(CGRect)frame
|
||||
{
|
||||
NSLayoutManager *layoutManager = [textStorage.layoutManagers firstObject];
|
||||
NSTextContainer *textContainer = [layoutManager.textContainers firstObject];
|
||||
|
||||
NSRange glyphRange = NSMakeRange(0, textStorage.length);
|
||||
[textStorage beginEditing];
|
||||
[textStorage enumerateAttribute:NSFontAttributeName
|
||||
inRange:glyphRange
|
||||
options:0
|
||||
usingBlock:^(UIFont *font, NSRange range, BOOL *stop)
|
||||
{
|
||||
if (font) {
|
||||
UIFont *originalFont = [self.attributedString attribute:NSFontAttributeName
|
||||
atIndex:range.location
|
||||
effectiveRange:&range];
|
||||
UIFont *newFont = [font fontWithSize:originalFont.pointSize * scale];
|
||||
[textStorage removeAttribute:NSFontAttributeName range:range];
|
||||
[textStorage addAttribute:NSFontAttributeName value:newFont range:range];
|
||||
}
|
||||
}];
|
||||
|
||||
[textStorage endEditing];
|
||||
|
||||
NSInteger linesRequired = [self numberOfLinesRequired:[textStorage.layoutManagers firstObject]];
|
||||
CGSize requiredSize = [self calculateSize:textStorage];
|
||||
|
||||
BOOL fitSize = requiredSize.height <= CGRectGetHeight(frame) &&
|
||||
requiredSize.width <= CGRectGetWidth(frame);
|
||||
|
||||
BOOL fitLines = linesRequired <= textContainer.maximumNumberOfLines ||
|
||||
textContainer.maximumNumberOfLines == 0;
|
||||
|
||||
if (fitLines && fitSize) {
|
||||
if ((requiredSize.width + (CGRectGetWidth(frame) * RCTTextAutoSizeWidthErrorMargin)) > CGRectGetWidth(frame) &&
|
||||
(requiredSize.height + (CGRectGetHeight(frame) * RCTTextAutoSizeHeightErrorMargin)) > CGRectGetHeight(frame))
|
||||
{
|
||||
return RCTSizeWithinRange;
|
||||
} else {
|
||||
return RCTSizeTooSmall;
|
||||
}
|
||||
} else {
|
||||
return RCTSizeTooLarge;
|
||||
}
|
||||
}
|
||||
|
||||
// Via Apple Text Layout Programming Guide
|
||||
// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Tasks/CountLines.html
|
||||
- (NSInteger)numberOfLinesRequired:(NSLayoutManager *)layoutManager
|
||||
{
|
||||
NSInteger numberOfLines, index, numberOfGlyphs = [layoutManager numberOfGlyphs];
|
||||
NSRange lineRange;
|
||||
for (numberOfLines = 0, index = 0; index < numberOfGlyphs; numberOfLines++){
|
||||
(void) [layoutManager lineFragmentRectForGlyphAtIndex:index
|
||||
effectiveRange:&lineRange];
|
||||
index = NSMaxRange(lineRange);
|
||||
}
|
||||
|
||||
return numberOfLines;
|
||||
}
|
||||
|
||||
// Via Apple Text Layout Programming Guide
|
||||
//https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/TextLayout/Tasks/StringHeight.html
|
||||
- (CGSize)calculateSize:(NSTextStorage *)storage
|
||||
{
|
||||
NSLayoutManager *layoutManager = [storage.layoutManagers firstObject];
|
||||
NSTextContainer *textContainer = [layoutManager.textContainers firstObject];
|
||||
|
||||
[textContainer setLineBreakMode:NSLineBreakByWordWrapping];
|
||||
NSInteger maxLines = [textContainer maximumNumberOfLines];
|
||||
[textContainer setMaximumNumberOfLines:0];
|
||||
(void) [layoutManager glyphRangeForTextContainer:textContainer];
|
||||
CGSize requiredSize = [layoutManager usedRectForTextContainer:textContainer].size;
|
||||
[textContainer setMaximumNumberOfLines:maxLines];
|
||||
|
||||
return requiredSize;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
super.backgroundColor = backgroundColor;
|
||||
@ -465,6 +623,7 @@ static CSSSize RCTMeasure(void *context, float width, CSSMeasureMode widthMode,
|
||||
[self dirtyText]; \
|
||||
}
|
||||
|
||||
RCT_TEXT_PROPERTY(AdjustsFontSizeToFit, _adjustsFontSizeToFit, BOOL)
|
||||
RCT_TEXT_PROPERTY(Color, _color, UIColor *)
|
||||
RCT_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *)
|
||||
RCT_TEXT_PROPERTY(FontSize, _fontSize, CGFloat)
|
||||
@ -512,4 +671,12 @@ RCT_TEXT_PROPERTY(TextShadowColor, _textShadowColor, UIColor *);
|
||||
[self dirtyText];
|
||||
}
|
||||
|
||||
- (void)setMinimumFontScale:(CGFloat)minimumFontScale
|
||||
{
|
||||
if (minimumFontScale >= 0.01) {
|
||||
_minimumFontScale = minimumFontScale;
|
||||
}
|
||||
[self dirtyText];
|
||||
}
|
||||
|
||||
@end
|
||||
|
@ -13,5 +13,7 @@
|
||||
|
||||
@property (nonatomic, assign) UIEdgeInsets contentInset;
|
||||
@property (nonatomic, strong) NSTextStorage *textStorage;
|
||||
@property (nonatomic, assign) CGRect textFrame;
|
||||
|
||||
|
||||
@end
|
||||
|
@ -34,7 +34,6 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc
|
||||
{
|
||||
if ((self = [super initWithFrame:frame])) {
|
||||
_textStorage = [NSTextStorage new];
|
||||
|
||||
self.isAccessibilityElement = YES;
|
||||
self.accessibilityTraits |= UIAccessibilityTraitStaticText;
|
||||
|
||||
@ -97,11 +96,11 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc
|
||||
|
||||
- (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];
|
||||
NSLayoutManager *layoutManager = [_textStorage.layoutManagers firstObject];
|
||||
NSTextContainer *textContainer = [layoutManager.textContainers firstObject];
|
||||
|
||||
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
|
||||
CGRect textFrame = self.textFrame;
|
||||
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textFrame.origin];
|
||||
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin];
|
||||
|
||||
@ -170,6 +169,7 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessibility
|
||||
|
||||
- (NSString *)accessibilityLabel
|
||||
|
@ -76,6 +76,8 @@ RCT_EXPORT_SHADOW_PROPERTY(opacity, CGFloat)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(textShadowOffset, CGSize)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(textShadowRadius, CGFloat)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(textShadowColor, UIColor)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(adjustsFontSizeToFit, BOOL)
|
||||
RCT_EXPORT_SHADOW_PROPERTY(minimumFontScale, CGFloat)
|
||||
|
||||
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(NSDictionary<NSNumber *, RCTShadowView *> *)shadowViewRegistry
|
||||
{
|
||||
|
@ -32,6 +32,8 @@ const viewConfig = {
|
||||
ellipsizeMode: true,
|
||||
allowFontScaling: true,
|
||||
selectable: true,
|
||||
adjustsFontSizeToFit: true,
|
||||
minimumFontScale: true,
|
||||
}),
|
||||
uiViewClassName: 'RCTText',
|
||||
};
|
||||
@ -166,7 +168,18 @@ const Text = React.createClass({
|
||||
* [Accessibility guide](/react-native/docs/accessibility.html#accessible-ios-android)
|
||||
* for more information.
|
||||
*/
|
||||
accessible: React.PropTypes.bool,
|
||||
accessible: React.PropTypes.bool,
|
||||
/**
|
||||
* Specifies whether font should be scaled down automatically to fit given style constraints.
|
||||
* @platform ios
|
||||
*/
|
||||
adjustsFontSizeToFit: React.PropTypes.bool,
|
||||
|
||||
/**
|
||||
* Specifies smallest possible scale a font can reach when adjustsFontSizeToFit is enabled. (values 0.01-1.0).
|
||||
* @platform ios
|
||||
*/
|
||||
minimumFontScale: React.PropTypes.number,
|
||||
},
|
||||
getDefaultProps(): Object {
|
||||
return {
|
||||
|
Loading…
x
Reference in New Issue
Block a user