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:
tfallon@mail.depaul.edu 2016-08-10 11:14:36 -07:00 committed by Facebook Github Bot 0
parent 6775d1f136
commit c6b6f53ae7
7 changed files with 286 additions and 6 deletions

View File

@ -29,6 +29,7 @@ var {
StyleSheet, StyleSheet,
Text, Text,
View, View,
LayoutAnimation,
} = ReactNative; } = ReactNative;
class Entity extends React.Component { 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.title = '<Text>';
exports.description = 'Base component for rendering styled text.'; exports.description = 'Base component for rendering styled text.';
exports.displayName = 'TextExample'; exports.displayName = 'TextExample';
@ -492,6 +573,11 @@ exports.examples = [
</View> </View>
); );
}, },
}, {
title: 'Dynamic Font Size Adjustment',
render: function(): ReactElement<any> {
return <AdjustingFontSize />;
},
}]; }];
var styles = StyleSheet.create({ var styles = StyleSheet.create({

View File

@ -10,6 +10,14 @@
#import "RCTShadowView.h" #import "RCTShadowView.h"
#import "RCTTextDecorationLineType.h" #import "RCTTextDecorationLineType.h"
typedef NS_ENUM(NSInteger, RCTSizeComparison)
{
RCTSizeTooLarge,
RCTSizeTooSmall,
RCTSizeWithinRange,
};
extern NSString *const RCTIsHighlightedAttributeName; extern NSString *const RCTIsHighlightedAttributeName;
extern NSString *const RCTReactTagAttributeName; extern NSString *const RCTReactTagAttributeName;
@ -38,6 +46,8 @@ extern NSString *const RCTReactTagAttributeName;
@property (nonatomic, assign) CGSize textShadowOffset; @property (nonatomic, assign) CGSize textShadowOffset;
@property (nonatomic, assign) CGFloat textShadowRadius; @property (nonatomic, assign) CGFloat textShadowRadius;
@property (nonatomic, strong) UIColor *textShadowColor; @property (nonatomic, strong) UIColor *textShadowColor;
@property (nonatomic, assign) BOOL adjustsFontSizeToFit;
@property (nonatomic, assign) CGFloat minimumFontScale;
- (void)recomputeText; - (void)recomputeText;

View File

@ -24,6 +24,11 @@ NSString *const RCTShadowViewAttributeName = @"RCTShadowViewAttributeName";
NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName"; NSString *const RCTIsHighlightedAttributeName = @"IsHighlightedAttributeName";
NSString *const RCTReactTagAttributeName = @"ReactTagAttributeName"; 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 @implementation RCTShadowText
{ {
NSTextStorage *_cachedTextStorage; NSTextStorage *_cachedTextStorage;
@ -37,6 +42,7 @@ static CSSSize RCTMeasure(void *context, float width, CSSMeasureMode widthMode,
{ {
RCTShadowText *shadowText = (__bridge RCTShadowText *)context; RCTShadowText *shadowText = (__bridge RCTShadowText *)context;
NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width widthMode:widthMode]; NSTextStorage *textStorage = [shadowText buildTextStorageForWidth:width widthMode:widthMode];
[shadowText calculateTextFrame:textStorage];
NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject; NSLayoutManager *layoutManager = textStorage.layoutManagers.firstObject;
NSTextContainer *textContainer = layoutManager.textContainers.firstObject; NSTextContainer *textContainer = layoutManager.textContainers.firstObject;
CGSize computedSize = [layoutManager usedRectForTextContainer:textContainer].size; CGSize computedSize = [layoutManager usedRectForTextContainer:textContainer].size;
@ -105,10 +111,13 @@ static CSSSize RCTMeasure(void *context, float width, CSSMeasureMode widthMode,
UIEdgeInsets padding = self.paddingAsInsets; UIEdgeInsets padding = self.paddingAsInsets;
CGFloat width = self.frame.size.width - (padding.left + padding.right); CGFloat width = self.frame.size.width - (padding.left + padding.right);
NSNumber *parentTag = [[self reactSuperview] reactTag]; NSNumber *parentTag = [[self reactSuperview] reactTag];
NSTextStorage *textStorage = [self buildTextStorageForWidth:width widthMode:CSSMeasureModeExactly]; NSTextStorage *textStorage = [self buildTextStorageForWidth:width widthMode:CSSMeasureModeExactly];
CGRect textFrame = [self calculateTextFrame:textStorage];
[applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) { [applierBlocks addObject:^(NSDictionary<NSNumber *, UIView *> *viewRegistry) {
RCTText *view = (RCTText *)viewRegistry[self.reactTag]; RCTText *view = (RCTText *)viewRegistry[self.reactTag];
view.textFrame = textFrame;
view.textStorage = textStorage; 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 - (void)setBackgroundColor:(UIColor *)backgroundColor
{ {
super.backgroundColor = backgroundColor; super.backgroundColor = backgroundColor;
@ -465,6 +623,7 @@ static CSSSize RCTMeasure(void *context, float width, CSSMeasureMode widthMode,
[self dirtyText]; \ [self dirtyText]; \
} }
RCT_TEXT_PROPERTY(AdjustsFontSizeToFit, _adjustsFontSizeToFit, BOOL)
RCT_TEXT_PROPERTY(Color, _color, UIColor *) RCT_TEXT_PROPERTY(Color, _color, UIColor *)
RCT_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *) RCT_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *)
RCT_TEXT_PROPERTY(FontSize, _fontSize, CGFloat) RCT_TEXT_PROPERTY(FontSize, _fontSize, CGFloat)
@ -512,4 +671,12 @@ RCT_TEXT_PROPERTY(TextShadowColor, _textShadowColor, UIColor *);
[self dirtyText]; [self dirtyText];
} }
- (void)setMinimumFontScale:(CGFloat)minimumFontScale
{
if (minimumFontScale >= 0.01) {
_minimumFontScale = minimumFontScale;
}
[self dirtyText];
}
@end @end

View File

@ -13,5 +13,7 @@
@property (nonatomic, assign) UIEdgeInsets contentInset; @property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, strong) NSTextStorage *textStorage; @property (nonatomic, strong) NSTextStorage *textStorage;
@property (nonatomic, assign) CGRect textFrame;
@end @end

View File

@ -34,7 +34,6 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc
{ {
if ((self = [super initWithFrame:frame])) { if ((self = [super initWithFrame:frame])) {
_textStorage = [NSTextStorage new]; _textStorage = [NSTextStorage new];
self.isAccessibilityElement = YES; self.isAccessibilityElement = YES;
self.accessibilityTraits |= UIAccessibilityTraitStaticText; self.accessibilityTraits |= UIAccessibilityTraitStaticText;
@ -97,11 +96,11 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc
- (void)drawRect:(CGRect)rect - (void)drawRect:(CGRect)rect
{ {
NSLayoutManager *layoutManager = _textStorage.layoutManagers.firstObject; NSLayoutManager *layoutManager = [_textStorage.layoutManagers firstObject];
NSTextContainer *textContainer = layoutManager.textContainers.firstObject; NSTextContainer *textContainer = [layoutManager.textContainers firstObject];
CGRect textFrame = UIEdgeInsetsInsetRect(self.bounds, _contentInset);
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer];
CGRect textFrame = self.textFrame;
[layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textFrame.origin]; [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:textFrame.origin];
[layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin]; [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:textFrame.origin];
@ -170,6 +169,7 @@ static void collectNonTextDescendants(RCTText *view, NSMutableArray *nonTextDesc
} }
} }
#pragma mark - Accessibility #pragma mark - Accessibility
- (NSString *)accessibilityLabel - (NSString *)accessibilityLabel

View File

@ -76,6 +76,8 @@ RCT_EXPORT_SHADOW_PROPERTY(opacity, CGFloat)
RCT_EXPORT_SHADOW_PROPERTY(textShadowOffset, CGSize) RCT_EXPORT_SHADOW_PROPERTY(textShadowOffset, CGSize)
RCT_EXPORT_SHADOW_PROPERTY(textShadowRadius, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(textShadowRadius, CGFloat)
RCT_EXPORT_SHADOW_PROPERTY(textShadowColor, UIColor) RCT_EXPORT_SHADOW_PROPERTY(textShadowColor, UIColor)
RCT_EXPORT_SHADOW_PROPERTY(adjustsFontSizeToFit, BOOL)
RCT_EXPORT_SHADOW_PROPERTY(minimumFontScale, CGFloat)
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(NSDictionary<NSNumber *, RCTShadowView *> *)shadowViewRegistry - (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(NSDictionary<NSNumber *, RCTShadowView *> *)shadowViewRegistry
{ {

View File

@ -32,6 +32,8 @@ const viewConfig = {
ellipsizeMode: true, ellipsizeMode: true,
allowFontScaling: true, allowFontScaling: true,
selectable: true, selectable: true,
adjustsFontSizeToFit: true,
minimumFontScale: true,
}), }),
uiViewClassName: 'RCTText', uiViewClassName: 'RCTText',
}; };
@ -166,7 +168,18 @@ const Text = React.createClass({
* [Accessibility guide](/react-native/docs/accessibility.html#accessible-ios-android) * [Accessibility guide](/react-native/docs/accessibility.html#accessible-ios-android)
* for more information. * 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 { getDefaultProps(): Object {
return { return {