diff --git a/Examples/UIExplorer/js/TextExample.ios.js b/Examples/UIExplorer/js/TextExample.ios.js
index 755594274..605ed2b8a 100644
--- a/Examples/UIExplorer/js/TextExample.ios.js
+++ b/Examples/UIExplorer/js/TextExample.ios.js
@@ -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 ();
+ }
+ return (
+
+
+ Truncated text is baaaaad.
+
+
+ Shrinking to fit available space is much better!
+
+
+
+ {'Add text to me to watch me shrink!' + ' ' + this.state.dynamicText}
+
+
+
+ {'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}
+
+
+
+
+ {'Differently sized nested elements will shrink together. '}
+
+
+ {'LARGE TEXT! ' + this.state.dynamicText}
+
+
+
+
+
+ Reset
+
+
+ Remove Text
+
+
+ Add Text
+
+
+
+ );
+ }
+});
+
exports.title = '';
exports.description = 'Base component for rendering styled text.';
exports.displayName = 'TextExample';
@@ -492,6 +573,11 @@ exports.examples = [
);
},
+}, {
+ title: 'Dynamic Font Size Adjustment',
+ render: function(): ReactElement {
+ return ;
+ },
}];
var styles = StyleSheet.create({
diff --git a/Libraries/Text/RCTShadowText.h b/Libraries/Text/RCTShadowText.h
index 7468a2ad4..2c8944221 100644
--- a/Libraries/Text/RCTShadowText.h
+++ b/Libraries/Text/RCTShadowText.h
@@ -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;
diff --git a/Libraries/Text/RCTShadowText.m b/Libraries/Text/RCTShadowText.m
index f177ad78d..15f33ddef 100644
--- a/Libraries/Text/RCTShadowText.m
+++ b/Libraries/Text/RCTShadowText.m
@@ -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 *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
diff --git a/Libraries/Text/RCTText.h b/Libraries/Text/RCTText.h
index 487954f8a..aef976303 100644
--- a/Libraries/Text/RCTText.h
+++ b/Libraries/Text/RCTText.h
@@ -13,5 +13,7 @@
@property (nonatomic, assign) UIEdgeInsets contentInset;
@property (nonatomic, strong) NSTextStorage *textStorage;
+@property (nonatomic, assign) CGRect textFrame;
+
@end
diff --git a/Libraries/Text/RCTText.m b/Libraries/Text/RCTText.m
index fec4f31d9..427c9ac68 100644
--- a/Libraries/Text/RCTText.m
+++ b/Libraries/Text/RCTText.m
@@ -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
diff --git a/Libraries/Text/RCTTextManager.m b/Libraries/Text/RCTTextManager.m
index adc8dea8a..0e66829e6 100644
--- a/Libraries/Text/RCTTextManager.m
+++ b/Libraries/Text/RCTTextManager.m
@@ -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 *)shadowViewRegistry
{
diff --git a/Libraries/Text/Text.js b/Libraries/Text/Text.js
index d90d75301..710d8f861 100644
--- a/Libraries/Text/Text.js
+++ b/Libraries/Text/Text.js
@@ -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 {