Dynamic Text Sizes for Text component

Summary:
Dynamic Text Sizes for Text component.
Text gains new prop - allowFontScaling (true by default).
There is also AccessibilityManager module that allows you to tune multipliers per each content size category, but predefined multipliers are there.
This could potentially break some apps so please test carefully.
This commit is contained in:
Vladislav Alexeev 2015-07-14 03:12:15 -07:00
parent eb06659711
commit be2cabc3f8
14 changed files with 331 additions and 12 deletions

View File

@ -369,6 +369,25 @@ exports.examples = [
</View>
);
},
}, {
title: 'allowFontScaling attribute',
render: function() {
return (
<View>
<Text>
By default, text will respect Text Size accessibility setting on iOS.
It means that all font sizes will be increased or descreased depending on the value of Text Size setting in
{" "}<Text style={{fontWeight: 'bold'}}>Settings.app - Display & Brightness - Text Size</Text>
</Text>
<Text style={{marginTop: 10}}>
You can disable scaling for your Text component by passing {"\""}allowFontScaling={"{"}false{"}\""} prop.
</Text>
<Text allowFontScaling={false} style={{marginTop: 20}}>
This text will not scale.
</Text>
</View>
);
},
}];
var styles = StyleSheet.create({

View File

@ -87,7 +87,7 @@ RCT_ENUM_CONVERTER(CTTextAlignment, (@{
}
NSDictionary *fontDict = dict[@"font"];
CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"]];
CTFontRef font = (__bridge CTFontRef)[self UIFont:nil withFamily:fontDict[@"fontFamily"] size:fontDict[@"fontSize"] weight:fontDict[@"fontWeight"] style:fontDict[@"fontStyle"] scaleMultiplier:1.0];
if (!font) {
return frame;
}

View File

@ -9,8 +9,32 @@
#import "RCTShadowRawText.h"
#import "RCTUIManager.h"
@implementation RCTShadowRawText
- (instancetype)init
{
if ((self = [super init])) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeMultiplierDidChange:)
name:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)contentSizeMultiplierDidChange:(NSNotification *)note
{
[self dirtyLayout];
[self dirtyText];
}
- (void)setText:(NSString *)text
{
if (_text != text) {

View File

@ -30,6 +30,8 @@ extern NSString *const RCTReactTagAttributeName;
@property (nonatomic, strong) UIColor *textDecorationColor;
@property (nonatomic, assign) NSUnderlineStyle textDecorationStyle;
@property (nonatomic, assign) RCTTextDecorationLineType textDecorationLine;
@property (nonatomic, assign) CGFloat fontSizeMultiplier;
@property (nonatomic, assign) BOOL allowFontScaling;
- (void)recomputeText;

View File

@ -9,6 +9,9 @@
#import "RCTShadowText.h"
#import "RCTAccessibilityManager.h"
#import "RCTUIManager.h"
#import "RCTBridge.h"
#import "RCTConvert.h"
#import "RCTLog.h"
#import "RCTShadowRawText.h"
@ -51,16 +54,31 @@ static css_dim_t RCTMeasure(void *context, float width)
_letterSpacing = NAN;
_isHighlighted = NO;
_textDecorationStyle = NSUnderlineStyleSingle;
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(contentSizeMultiplierDidChange:)
name:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (NSString *)description
{
NSString *superDescription = super.description;
return [[superDescription substringToIndex:superDescription.length - 1] stringByAppendingFormat:@"; text: %@>", [self attributedString].string];
}
- (void)contentSizeMultiplierDidChange:(NSNotification *)note
{
[self dirtyLayout];
[self dirtyText];
}
- (NSDictionary *)processUpdatedProperties:(NSMutableSet *)applierBlocks
parentProperties:(NSDictionary *)parentProperties
{
@ -190,7 +208,9 @@ static css_dim_t RCTMeasure(void *context, float width)
[self _addAttribute:NSBackgroundColorAttributeName withValue:self.backgroundColor toAttributedString:attributedString];
}
UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily size:fontSize weight:fontWeight style:fontStyle];
UIFont *font = [RCTConvert UIFont:nil withFamily:fontFamily
size:fontSize weight:fontWeight style:fontStyle
scaleMultiplier:(_allowFontScaling && _fontSizeMultiplier > 0.0 ? _fontSizeMultiplier : 1.0)];
[self _addAttribute:NSFontAttributeName withValue:font toAttributedString:attributedString];
[self _addAttribute:NSKernAttributeName withValue:letterSpacing toAttributedString:attributedString];
[self _addAttribute:RCTReactTagAttributeName withValue:self.reactTag toAttributedString:attributedString];
@ -247,8 +267,9 @@ static css_dim_t RCTMeasure(void *context, float width)
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = _textAlign;
paragraphStyle.baseWritingDirection = _writingDirection;
paragraphStyle.minimumLineHeight = _lineHeight;
paragraphStyle.maximumLineHeight = _lineHeight;
CGFloat lineHeight = round(_lineHeight * self.fontSizeMultiplier);
paragraphStyle.minimumLineHeight = lineHeight;
paragraphStyle.maximumLineHeight = lineHeight;
[attributedString addAttribute:NSParagraphStyleAttributeName
value:paragraphStyle
range:(NSRange){0, attributedString.length}];
@ -321,4 +342,26 @@ RCT_TEXT_PROPERTY(TextDecorationLine, _textDecorationLine, RCTTextDecorationLine
RCT_TEXT_PROPERTY(TextDecorationStyle, _textDecorationStyle, NSUnderlineStyle);
RCT_TEXT_PROPERTY(WritingDirection, _writingDirection, NSWritingDirection)
- (void)setAllowFontScaling:(BOOL)allowFontScaling
{
_allowFontScaling = allowFontScaling;
for (RCTShadowView *child in [self reactSubviews]) {
if ([child isKindOfClass:[RCTShadowText class]]) {
[(RCTShadowText *)child setAllowFontScaling:allowFontScaling];
}
}
[self dirtyText];
}
- (void)setFontSizeMultiplier:(CGFloat)fontSizeMultiplier
{
_fontSizeMultiplier = fontSizeMultiplier;
for (RCTShadowView *child in [self reactSubviews]) {
if ([child isKindOfClass:[RCTShadowText class]]) {
[(RCTShadowText *)child setFontSizeMultiplier:fontSizeMultiplier];
}
}
[self dirtyText];
}
@end

View File

@ -9,6 +9,7 @@
#import "RCTTextManager.h"
#import "RCTAccessibilityManager.h"
#import "RCTAssert.h"
#import "RCTConvert.h"
#import "RCTLog.h"
@ -49,6 +50,7 @@ RCT_EXPORT_SHADOW_PROPERTY(textDecorationStyle, NSUnderlineStyle)
RCT_EXPORT_SHADOW_PROPERTY(textDecorationColor, UIColor)
RCT_EXPORT_SHADOW_PROPERTY(textDecorationLine, RCTTextDecorationLineType)
RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection)
RCT_EXPORT_SHADOW_PROPERTY(allowFontScaling, BOOL)
- (RCTViewManagerUIBlock)uiBlockToAmendWithShadowViewRegistry:(RCTSparseArray *)shadowViewRegistry
{
@ -69,6 +71,7 @@ RCT_EXPORT_SHADOW_PROPERTY(writingDirection, NSWritingDirection)
RCTAssert([shadowView isTextDirty], @"Don't process any nodes that don't have dirty text");
if ([shadowView isKindOfClass:[RCTShadowText class]]) {
[(RCTShadowText *)shadowView setFontSizeMultiplier:self.bridge.accessibilityManager.multiplier];
[(RCTShadowText *)shadowView recomputeText];
} else if ([shadowView isKindOfClass:[RCTShadowRawText class]]) {
RCTLogError(@"Raw text cannot be used outside of a <Text> tag. Not rendering string: '%@'",

View File

@ -30,6 +30,7 @@ var viewConfig = {
validAttributes: merge(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
numberOfLines: true,
allowFontScaling: true,
}),
uiViewClassName: 'RCTText',
};
@ -99,16 +100,27 @@ var Text = React.createClass({
*
* {nativeEvent: {layout: {x, y, width, height}}}.
*/
onLayout: React.PropTypes.func,
onLayout: React.PropTypes.func,
/**
* Specifies should fonts scale to respect Text Size accessibility setting.
*/
allowFontScaling: React.PropTypes.bool,
},
viewConfig: viewConfig,
getInitialState: function() {
getInitialState: function(): Object {
return merge(this.touchableGetInitialState(), {
isHighlighted: false,
});
},
getDefaultProps: function(): Object {
return {
numberOfLines: 0,
allowFontScaling: true,
};
},
onStartShouldSetResponder: function(): bool {
var shouldSetFromProps = this.props.onStartShouldSetResponder &&
@ -231,6 +243,7 @@ if (Platform.OS === 'android') {
RCTVirtualText = createReactNativeComponentClass({
validAttributes: merge(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
allowFontScaling: false,
}),
uiViewClassName: 'RCTVirtualText',
});

View File

@ -91,7 +91,8 @@ typedef NSURL RCTFileURL;
+ (UIFont *)UIFont:(UIFont *)font withStyle:(id)json;
+ (UIFont *)UIFont:(UIFont *)font withFamily:(id)json;
+ (UIFont *)UIFont:(UIFont *)font withFamily:(id)family
size:(id)size weight:(id)weight style:(id)style;
size:(id)size weight:(id)weight style:(id)style
scaleMultiplier:(CGFloat)scaleMultiplier;
typedef NSArray NSStringArray;
+ (NSStringArray *)NSStringArray:(id)json;

View File

@ -779,31 +779,33 @@ static BOOL RCTFontIsCondensed(UIFont *font)
withFamily:json[@"fontFamily"]
size:json[@"fontSize"]
weight:json[@"fontWeight"]
style:json[@"fontStyle"]];
style:json[@"fontStyle"]
scaleMultiplier:1.0f];
}
+ (UIFont *)UIFont:(UIFont *)font withSize:(id)json
{
return [self UIFont:font withFamily:nil size:json weight:nil style:nil];
return [self UIFont:font withFamily:nil size:json weight:nil style:nil scaleMultiplier:1.0];
}
+ (UIFont *)UIFont:(UIFont *)font withWeight:(id)json
{
return [self UIFont:font withFamily:nil size:nil weight:json style:nil];
return [self UIFont:font withFamily:nil size:nil weight:json style:nil scaleMultiplier:1.0];
}
+ (UIFont *)UIFont:(UIFont *)font withStyle:(id)json
{
return [self UIFont:font withFamily:nil size:nil weight:nil style:json];
return [self UIFont:font withFamily:nil size:nil weight:nil style:json scaleMultiplier:1.0];
}
+ (UIFont *)UIFont:(UIFont *)font withFamily:(id)json
{
return [self UIFont:font withFamily:json size:nil weight:nil style:nil];
return [self UIFont:font withFamily:json size:nil weight:nil style:nil scaleMultiplier:1.0];
}
+ (UIFont *)UIFont:(UIFont *)font withFamily:(id)family
size:(id)size weight:(id)weight style:(id)style
scaleMultiplier:(CGFloat)scaleMultiplier
{
// Defaults
NSString *const RCTDefaultFontFamily = @"System";
@ -828,6 +830,9 @@ static BOOL RCTFontIsCondensed(UIFont *font)
// Get font attributes
fontSize = [self CGFloat:size] ?: fontSize;
if (scaleMultiplier > 0.0 && scaleMultiplier != 1.0) {
fontSize = round(fontSize * scaleMultiplier);
}
familyName = [self NSString:family] ?: familyName;
isItalic = style ? [self RCTFontStyle:style] : isItalic;
fontWeight = weight ? [self RCTFontWeight:weight] : fontWeight;

View File

@ -0,0 +1,27 @@
/**
* 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 <Foundation/Foundation.h>
#import "RCTBridgeModule.h"
#import "RCTBridge.h"
extern NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification; // posted when multiplier is changed
@interface RCTAccessibilityManager : NSObject <RCTBridgeModule>
@property (nonatomic, readonly) CGFloat multiplier;
@end
@interface RCTBridge (RCTAccessibilityManager)
@property (nonatomic, readonly) RCTAccessibilityManager *accessibilityManager;
@end

View File

@ -0,0 +1,144 @@
/**
* 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 "RCTAccessibilityManager.h"
#import "RCTLog.h"
NSString *const RCTAccessibilityManagerDidUpdateMultiplierNotification = @"RCTAccessibilityManagerDidUpdateMultiplierNotification";
@interface RCTAccessibilityManager ()
@property (nonatomic, copy) NSDictionary *multipliers;
@property (nonatomic, copy) NSString *contentSizeCategory;
@property (nonatomic, assign) CGFloat multiplier;
@end
@implementation RCTAccessibilityManager
@synthesize bridge = _bridge;
RCT_EXPORT_MODULE()
+ (NSDictionary *)JSToUIKitMap
{
static NSDictionary *map = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
map = @{@"extraSmall": UIContentSizeCategoryExtraSmall,
@"small": UIContentSizeCategorySmall,
@"medium": UIContentSizeCategoryMedium,
@"large": UIContentSizeCategoryLarge,
@"extraLarge": UIContentSizeCategoryExtraLarge,
@"extraExtraLarge": UIContentSizeCategoryExtraExtraLarge,
@"extraExtraExtraLarge": UIContentSizeCategoryExtraExtraExtraLarge,
@"accessibilityMedium": UIContentSizeCategoryAccessibilityMedium,
@"accessibilityLarge": UIContentSizeCategoryAccessibilityLarge,
@"accessibilityExtraLarge": UIContentSizeCategoryAccessibilityExtraLarge,
@"accessibilityExtraExtraLarge": UIContentSizeCategoryAccessibilityExtraExtraLarge,
@"accessibilityExtraExtraExtraLarge": UIContentSizeCategoryAccessibilityExtraExtraExtraLarge};
});
return map;
}
+ (NSString *)UIKitCategoryFromJSCategory:(NSString *)JSCategory
{
return self.JSToUIKitMap[JSCategory];
}
- (instancetype)init
{
self = [super init];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveNewContentSizeCategory:)
name:UIContentSizeCategoryDidChangeNotification
object:[UIApplication sharedApplication]];
self.contentSizeCategory = [[UIApplication sharedApplication] preferredContentSizeCategory];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)didReceiveNewContentSizeCategory:(NSNotification *)note
{
self.contentSizeCategory = note.userInfo[UIContentSizeCategoryNewValueKey];
}
- (void)setContentSizeCategory:(NSString *)contentSizeCategory
{
if (_contentSizeCategory != contentSizeCategory) {
_contentSizeCategory = [contentSizeCategory copy];
self.multiplier = [self multiplierForContentSizeCategory:_contentSizeCategory];
[[NSNotificationCenter defaultCenter] postNotificationName:RCTAccessibilityManagerDidUpdateMultiplierNotification object:self];
}
}
- (CGFloat)multiplierForContentSizeCategory:(NSString *)category
{
NSNumber *m = self.multipliers[category];
if (m.doubleValue <= 0.0) {
RCTLogError(@"Can't determinte multiplier for category %@. Using 1.0.", category);
m = @1.0;
}
return m.doubleValue;
}
- (NSDictionary *)multipliers
{
if (_multipliers == nil) {
_multipliers = @{UIContentSizeCategoryExtraSmall: @0.823,
UIContentSizeCategorySmall: @0.882,
UIContentSizeCategoryMedium: @0.941,
UIContentSizeCategoryLarge: @1.0,
UIContentSizeCategoryExtraLarge: @1.118,
UIContentSizeCategoryExtraExtraLarge: @1.235,
UIContentSizeCategoryExtraExtraExtraLarge: @1.353,
UIContentSizeCategoryAccessibilityMedium: @1.786,
UIContentSizeCategoryAccessibilityLarge: @2.143,
UIContentSizeCategoryAccessibilityExtraLarge: @2.643,
UIContentSizeCategoryAccessibilityExtraExtraLarge: @3.143,
UIContentSizeCategoryAccessibilityExtraExtraExtraLarge: @3.571};
}
return _multipliers;
}
RCT_EXPORT_METHOD(setAccessibilityContentSizeMultipliers:(NSDictionary *)JSMultipliers)
{
NSMutableDictionary *multipliers = [[NSMutableDictionary alloc] init];
for (NSString *__nonnull JSCategory in JSMultipliers) {
NSNumber *m = JSMultipliers[JSCategory];
NSString *UIKitCategory = [self.class UIKitCategoryFromJSCategory:JSCategory];
multipliers[UIKitCategory] = m;
}
self.multipliers = multipliers;
}
RCT_EXPORT_METHOD(getMultiplier:(RCTResponseSenderBlock)callback)
{
if (callback) {
callback(@[ @(self.multiplier) ]);
}
}
@end
@implementation RCTBridge (RCTAccessibilityManager)
- (RCTAccessibilityManager *)accessibilityManager
{
return self.modules[RCTBridgeModuleNameForClass([RCTAccessibilityManager class])];
}
@end

View File

@ -14,6 +14,12 @@
#import "RCTInvalidating.h"
#import "RCTViewManager.h"
/**
* Posted right before re-render happens. This is a chance for views to invalidate their state so
* next render cycle will pick up updated views and layout appropriately.
*/
extern NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification;
@protocol RCTScrollableProtocol;
/**

View File

@ -14,6 +14,7 @@
#import <AVFoundation/AVFoundation.h>
#import "Layout.h"
#import "RCTAccessibilityManager.h"
#import "RCTAnimationType.h"
#import "RCTAssert.h"
#import "RCTBridge.h"
@ -35,6 +36,8 @@
static void RCTTraverseViewNodes(id<RCTViewNodeProtocol> view, void (^block)(id<RCTViewNodeProtocol>));
static NSDictionary *RCTPropsMerge(NSDictionary *beforeProps, NSDictionary *newProps);
NSString *const RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification = @"RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification";
@interface RCTAnimation : NSObject
@property (nonatomic, readonly) NSTimeInterval duration;
@ -262,10 +265,33 @@ static NSDictionary *RCTViewConfigForModule(Class managerClass)
_rootViewTags = [[NSMutableSet alloc] init];
_bridgeTransactionListeners = [[NSMutableSet alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didReceiveNewContentSizeMultiplier)
name:RCTAccessibilityManagerDidUpdateMultiplierNotification
object:nil];
}
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)didReceiveNewContentSizeMultiplier
{
__weak RCTUIManager *weakSelf = self;
dispatch_async(self.methodQueue, ^{
__weak RCTUIManager *strongSelf = weakSelf;
if (strongSelf) {
[[NSNotificationCenter defaultCenter] postNotificationName:RCTUIManagerWillUpdateViewsDueToContentSizeMultiplierChangeNotification
object:strongSelf];
[strongSelf batchDidComplete];
}
});
}
- (BOOL)isValid
{
return _viewRegistry != nil;

View File

@ -75,6 +75,7 @@
83CBBA691A601EF300E9B192 /* RCTEventDispatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA661A601EF300E9B192 /* RCTEventDispatcher.m */; };
83CBBA981A6020BB00E9B192 /* RCTTouchHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBA971A6020BB00E9B192 /* RCTTouchHandler.m */; };
83CBBACC1A6023D300E9B192 /* RCTConvert.m in Sources */ = {isa = PBXBuildFile; fileRef = 83CBBACB1A6023D300E9B192 /* RCTConvert.m */; };
E9B20B7B1B500126007A2DA7 /* RCTAccessibilityManager.m in Sources */ = {isa = PBXBuildFile; fileRef = E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
@ -239,6 +240,8 @@
83CBBACA1A6023D300E9B192 /* RCTConvert.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTConvert.h; sourceTree = "<group>"; };
83CBBACB1A6023D300E9B192 /* RCTConvert.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTConvert.m; sourceTree = "<group>"; };
E3BBC8EB1ADE6F47001BBD81 /* RCTTextDecorationLineType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCTTextDecorationLineType.h; sourceTree = "<group>"; };
E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RCTAccessibilityManager.h; sourceTree = "<group>"; };
E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = RCTAccessibilityManager.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -275,6 +278,8 @@
13B07FE01A69315300A75B9A /* Modules */ = {
isa = PBXGroup;
children = (
E9B20B791B500126007A2DA7 /* RCTAccessibilityManager.h */,
E9B20B7A1B500126007A2DA7 /* RCTAccessibilityManager.m */,
13B07FE71A69327A00A75B9A /* RCTAlertManager.h */,
13B07FE81A69327A00A75B9A /* RCTAlertManager.m */,
1372B7081AB030C200659ED6 /* RCTAppState.h */,
@ -567,6 +572,7 @@
137327EA1AA5CF210034F82E /* RCTTabBarManager.m in Sources */,
13B080261A694A8400A75B9A /* RCTWrapperViewController.m in Sources */,
13B080051A6947C200A75B9A /* RCTScrollView.m in Sources */,
E9B20B7B1B500126007A2DA7 /* RCTAccessibilityManager.m in Sources */,
13B07FF21A69327A00A75B9A /* RCTTiming.m in Sources */,
1372B70A1AB030C200659ED6 /* RCTAppState.m in Sources */,
134FCB3D1A6E7F0800051CC8 /* RCTContextExecutor.m in Sources */,