Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ In both functions, the text to be measured is required, but the rest of the para
- `fontSize`
- `fontWeight`
- `fontStyle`
- `lineHeight`
- `numberOfLines`
- `fontVariant` (iOS)
- `includeFontPadding` (Android)
- `textBreakStrategy` (Android)
Expand Down Expand Up @@ -95,6 +97,8 @@ fontFamily | string | OS dependent | The default is the same applied by
fontWeight | string | 'normal' | On android, numeric ranges has no granularity and '500' to '900' becomes 'bold', but you can use a `fontFamily` of specific weight ("sans-serif-thin", "sans-serif-medium", etc).
fontSize | number | 14 | The default font size comes from RN.
fontStyle | string | 'normal' | One of "normal" or "italic".
lineHeight | number | (none) | The line height of each line. Defaults to the font size.
numberOfLines | number | (none) | Limit the number of lines the text can render on
fontVariant | array | (none) | _iOS only_
allowFontScaling | boolean | true | To respect the user' setting of large fonts (i.e. use SP units).
letterSpacing | number | (none) | Additional spacing between characters (aka `tracking`).<br>**Note:** In iOS a zero cancels automatic kerning.<br>_All iOS, Android with API 21+_
Expand Down Expand Up @@ -228,6 +232,7 @@ allowFontScaling | boolean | true
letterSpacing | number | (none)
includeFontPadding | boolean | true
textBreakStrategy | string | 'highQuality'
numberOfLines | number | (none)

The result is a Promise that resolves to an array with the height of each block (in _SP_), in the same order in which the blocks were received.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,10 @@ static boolean supportUpperCaseTransform() {
final String fontFamily;
final float fontSize;
final int fontStyle;
final float lineHeight;
final boolean includeFontPadding;
final float letterSpacing;
final @Nullable Integer numberOfLines;

/**
* Proccess the user specs. Set both `allowFontScaling` & `includeFontPadding` to the user
Expand All @@ -84,10 +86,15 @@ static boolean supportUpperCaseTransform() {
fontFamily = getString("fontFamily");
fontSize = getFontSizeOrDefault();
fontStyle = getFontStyle();
lineHeight = getFloatOrNaN("lineHeight");
includeFontPadding = forText && getBooleanOrTrue("includeFontPadding");

// letterSpacing is supported in RN 0.55+
letterSpacing = supportLetterSpacing() ? getFloatOrNaN("letterSpacing") : Float.NaN;

Integer rawNumberOfLines = getIntOrNull("numberOfLines");
if (rawNumberOfLines != null && rawNumberOfLines < 0) rawNumberOfLines = null;
numberOfLines = rawNumberOfLines;
}

boolean has(@Nonnull final String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import android.text.SpannableStringBuilder;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
Expand Down Expand Up @@ -118,13 +119,17 @@ public void measure(@Nullable final ReadableMap specs, final Promise promise) {

if (layout == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
layout = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth)
StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, hintWidth)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setBreakStrategy(conf.getTextBreakStrategy())
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
.setIncludePad(includeFontPadding)
.setLineSpacing(SPACING_ADDITION, SPACING_MULTIPLIER)
.build();
.setLineSpacing(SPACING_ADDITION, SPACING_MULTIPLIER);
if (conf.numberOfLines != null) {
builder = builder.setMaxLines(conf.numberOfLines)
.setEllipsize(TextUtils.TruncateAt.END);
}
layout = builder.build();
} else {
layout = new StaticLayout(
text,
Expand Down Expand Up @@ -228,13 +233,17 @@ public void flatHeights(@Nullable final ReadableMap specs, final Promise promise
sb.replace(0, sb.length(), text);

if (Build.VERSION.SDK_INT >= 23) {
layout = StaticLayout.Builder.obtain(sb, 0, sb.length(), textPaint, (int) width)
StaticLayout.Builder builder = StaticLayout.Builder.obtain(sb, 0, sb.length(), textPaint, (int) width)
.setAlignment(Layout.Alignment.ALIGN_NORMAL)
.setBreakStrategy(textBreakStrategy)
.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
.setIncludePad(includeFontPadding)
.setLineSpacing(SPACING_ADDITION, SPACING_MULTIPLIER)
.build();
.setLineSpacing(SPACING_ADDITION, SPACING_MULTIPLIER);
if (conf.numberOfLines != null) {
builder = builder.setMaxLines(conf.numberOfLines)
.setEllipsize(TextUtils.TruncateAt.END);
}
layout = builder.build();
} else {
layout = new StaticLayout(
sb,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import android.text.style.MetricAffectingSpan;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.views.text.CustomLineHeightSpan;
import com.facebook.react.views.text.TextAttributes;

import javax.annotation.Nonnull;

Expand Down Expand Up @@ -49,6 +51,13 @@ static Spannable spannedFromSpecsAndText(
new CustomStyleSpan(RNTextSizeConf.getFont(context, conf.fontFamily, conf.fontStyle)));
}

if (!Float.isNaN(conf.lineHeight)) {
priority++;
final TextAttributes textAttributes = new TextAttributes();
textAttributes.setLineHeight(conf.lineHeight);
setSpanOperation(text, end, priority, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight()));
}

return text;
}

Expand Down
6 changes: 6 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ declare module "react-native-text-size" {
fontSize?: number;
fontStyle?: TSFontStyle;
fontWeight?: TSFontWeight;
lineHeight?: number;
/**
* Number of lines to limit the text to. Corresponds to the `numberOfLines`
* prop on `<Text>`
*/
numberOfLines?: number;
/** @platform ios */
fontVariant?: Array<TSFontVariant>;
/** iOS all, Android SDK 21+ with RN 0.55+ */
Expand Down
6 changes: 6 additions & 0 deletions index.js.flow
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ export type TSFontSpecs = {
fontSize?: number,
fontStyle?: TSFontStyle,
fontWeight?: TSFontWeight,
lineHeight?: number;
/**
* Number of lines to limit the text to. Corresponds to the `numberOfLines`
* prop on `<Text>`
*/
numberOfLines?: number,
/** @platform ios */
fontVariant?: Array<TSFontVariant>,
/** iOS all, Android SDK 21+ with RN 0.55+ */
Expand Down
102 changes: 74 additions & 28 deletions ios/RNTextSize.m
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
#import <React/RCTUtils.h>
#else
#import "React/RCTConvert.h" // Required when used as a Pod in a Swift project
#import "React/RCTFont.h"
#import "React/RCTUtils.h"
#import <React/RCTFont.h>
#import <React/RCTUtils.h>
#endif

#import <CoreText/CoreText.h>
Expand Down Expand Up @@ -86,20 +86,15 @@ - (dispatch_queue_t)methodQueue {
return;
}

// Allow the user to specify the width or height (both optionals).
const CGFloat optWidth = CGFloatValueFrom(options[@"width"]);
const CGFloat maxWidth = isnan(optWidth) || isinf(optWidth) ? CGFLOAT_MAX : optWidth;
const CGSize maxSize = CGSizeMake(maxWidth, CGFLOAT_MAX);

// Create attributes for the font and the optional letter spacing.
const CGSize maxSize = [self maxSizeFromOptions:options];
const CGFloat letterSpacing = CGFloatValueFrom(options[@"letterSpacing"]);
NSDictionary<NSAttributedStringKey,id> *const attributes = isnan(letterSpacing)
? @{NSFontAttributeName: font}
: @{NSFontAttributeName: font, NSKernAttributeName: @(letterSpacing)};

NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:maxSize];
textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = NSLineBreakByClipping; // no maxlines support
NSTextContainer *const textContainer =
[self textContainerFromOptions:options withMaxSize:maxSize];
NSDictionary<NSAttributedStringKey,id> *const attributes =
[self textStorageAttributesFromOptions:options
withFont:font
withLetterSpacing:letterSpacing];

NSLayoutManager *layoutManager = [NSLayoutManager new];
[layoutManager addTextContainer:textContainer];
Expand Down Expand Up @@ -166,19 +161,15 @@ - (dispatch_queue_t)methodQueue {
return;
}

const CGFloat optWidth = CGFloatValueFrom(options[@"width"]);
const CGFloat maxWidth = isnan(optWidth) || isinf(optWidth) ? CGFLOAT_MAX : optWidth;
const CGSize maxSize = CGSizeMake(maxWidth, CGFLOAT_MAX);

// Create attributes for the font and the optional letter spacing.
const CGSize maxSize = [self maxSizeFromOptions:options];
const CGFloat letterSpacing = CGFloatValueFrom(options[@"letterSpacing"]);
NSDictionary<NSAttributedStringKey,id> *const attributes = isnan(letterSpacing)
? @{NSFontAttributeName: font}
: @{NSFontAttributeName: font, NSKernAttributeName: @(letterSpacing)};

NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:maxSize];
textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = NSLineBreakByClipping; // no maxlines support
NSTextContainer *const textContainer =
[self textContainerFromOptions:options withMaxSize:maxSize];
NSDictionary<NSAttributedStringKey,id> *const attributes =
[self textStorageAttributesFromOptions:options
withFont:font
withLetterSpacing:letterSpacing];

NSLayoutManager *layoutManager = [NSLayoutManager new];
[layoutManager addTextContainer:textContainer];
Expand All @@ -187,7 +178,9 @@ - (dispatch_queue_t)methodQueue {
[textStorage addLayoutManager:layoutManager];

NSMutableArray<NSNumber *> *result = [[NSMutableArray alloc] initWithCapacity:texts.count];
const CGFloat epsilon = 0.001;

const CGFloat scaleMultiplier = _bridge ? _bridge.accessibilityManager.multiplier : 1.0;
const CGFloat epsilon = scaleMultiplier != 1.0 ? 0.001 : 0;

for (int ix = 0; ix < texts.count; ix++) {
NSString *text = texts[ix];
Expand Down Expand Up @@ -496,7 +489,7 @@ - (NSDictionary *)fontInfoFromUIFont:(const UIFont *)font
* of the weight in multiples of "100", as expected by RN, or one of the words
* "bold" or "normal" if appropiate.
*
* @param trais NSDictionary with the traits of the font.
* @param traits NSDictionary with the traits of the font.
* @return NSString with the weight of the font.
*/
- (NSString *)fontWeightFromTraits:(const NSDictionary *)traits
Expand All @@ -517,7 +510,7 @@ - (NSString *)fontWeightFromTraits:(const NSDictionary *)traits
/**
* Returns a string with the style found in the trait, either "normal" or "italic".
*
* @param trais NSDictionary with the traits of the font.
* @param traits NSDictionary with the traits of the font.
* @return NSString with the style.
*/
- (NSString *)fontStyleFromTraits:(const NSDictionary *)traits
Expand Down Expand Up @@ -581,4 +574,57 @@ - (NSString *)fontStyleFromTraits:(const NSDictionary *)traits
return count ? [NSArray arrayWithObjects:outArr count:count] : nil;
}

- (CGSize)maxSizeFromOptions:(NSDictionary * _Nullable)options
{
const CGFloat optWidth = CGFloatValueFrom(options[@"width"]);
const CGFloat maxWidth = isnan(optWidth) || isinf(optWidth) ? CGFLOAT_MAX : optWidth;
const CGSize maxSize = CGSizeMake(maxWidth, CGFLOAT_MAX);
return maxSize;
}

/**
* Creates a textContainer with the width and numberOfLines from options.
*/
- (NSTextContainer *)textContainerFromOptions:(NSDictionary * _Nullable)options
withMaxSize:(CGSize)maxSize
{
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:maxSize];
textContainer.lineFragmentPadding = 0.0;
textContainer.lineBreakMode = NSLineBreakByClipping;

const NSInteger numberOfLines = [RCTConvert NSInteger:options[@"numberOfLines"]];
if (numberOfLines > 0) {
textContainer.maximumNumberOfLines = numberOfLines;
}

return textContainer;
}

/**
* Creates attributes that should be passed into the TextStorage based on
* parameters and the options the user passes in.
*/
- (NSDictionary<NSAttributedStringKey,id> *const)textStorageAttributesFromOptions:(NSDictionary * _Nullable)options
withFont:(UIFont *const _Nullable)font
withLetterSpacing:(CGFloat)letterSpacing
{
NSMutableDictionary<NSAttributedStringKey,id> *const attributes = [[NSMutableDictionary alloc] init];
[attributes setObject:font forKey:NSFontAttributeName];

if (!isnan(letterSpacing)) {
[attributes setObject:@(letterSpacing) forKey:NSKernAttributeName];
}

const CGFloat lineHeight = CGFloatValueFrom(options[@"lineHeight"]);
if (!isnan(lineHeight)) {
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
const CGFloat scaleMultiplier = _bridge ? _bridge.accessibilityManager.multiplier : 1.0;
[style setMinimumLineHeight:lineHeight * scaleMultiplier];
[style setMaximumLineHeight:lineHeight * scaleMultiplier];
[attributes setObject:style forKey:NSParagraphStyleAttributeName];
}

return attributes;
}

@end