Skip to content

Add numberOfLines, lineHeight options; create flatSizes; increase accuracy #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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