Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Change flatHeights on Android to account for when text gets truncated (
  • Loading branch information
hsource authored Apr 3, 2025
commit e6baa3799aa2fa8ce01c0660f93b16bcb53fae45
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import android.os.Build;
import android.text.BoringLayout;
import android.text.Layout;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.StaticLayout;
import android.text.TextPaint;
Expand Down Expand Up @@ -171,7 +170,7 @@ public void measure(@Nullable final ReadableMap specs, final Promise promise) {
@SuppressWarnings("unused")
@ReactMethod
public void flatSizes(@Nullable final ReadableMap specs, final Promise promise) {
flatHeightsInner(specs, promise, true);
flatHeightsInner(specs, promise, FlatHeightsMode.sizes);
}

/**
Expand All @@ -182,7 +181,7 @@ public void flatSizes(@Nullable final ReadableMap specs, final Promise promise)
@SuppressWarnings("unused")
@ReactMethod
public void flatHeights(@Nullable final ReadableMap specs, final Promise promise) {
flatHeightsInner(specs, promise, false);
flatHeightsInner(specs, promise, FlatHeightsMode.heights);
}

/**
Expand Down Expand Up @@ -278,7 +277,12 @@ public void fontNamesForFamilyName(final String ignored, final Promise promise)
//
// ============================================================================

private void flatHeightsInner(@Nullable final ReadableMap specs, final Promise promise, boolean includeWidths) {
private enum FlatHeightsMode {
heights,
sizes,
}

private void flatHeightsInner(@Nullable final ReadableMap specs, final Promise promise, FlatHeightsMode mode) {
final RNTextSizeConf conf = getConf(specs, promise, true);
if (conf == null) {
return;
Expand All @@ -293,7 +297,6 @@ private void flatHeightsInner(@Nullable final ReadableMap specs, final Promise p
final float density = getCurrentDensity();
final float width = conf.getWidth(density);
final boolean includeFontPadding = conf.includeFontPadding;
final int textBreakStrategy = conf.getTextBreakStrategy();

final WritableArray heights = Arguments.createArray();
final WritableArray widths = Arguments.createArray();
Expand Down Expand Up @@ -324,25 +327,66 @@ private void flatHeightsInner(@Nullable final ReadableMap specs, final Promise p
// Reset the SB text, the attrs will expand to its full length
sb.replace(0, sb.length(), text);
layout = buildStaticLayout(conf, includeFontPadding, sb, textPaint, (int) width);
heights.pushDouble(layout.getHeight() / density);

if (includeWidths) {
float height = layout.getHeight() / density;

if (conf.numberOfLines != null || mode == FlatHeightsMode.sizes) {
final int lineCount = layout.getLineCount();
float measuredWidth = 0;
for (int i = 0; i < lineCount; i++) {
measuredWidth = Math.max(measuredWidth, layout.getLineMax(i));

if (conf.numberOfLines != null) {
boolean lastLineHasEllipsis = layout.getEllipsisCount(lineCount - 1) > 0;
// For unknown reasons, the text will be 2 subpixels shorter if truncated
// due to numberOfLines. See the lines mentioning `numberOfLines` in the
// TextHeights stories: this logic was created for those cases.
if (lastLineHasEllipsis) {
height -= 2 / density;
}
}

if (mode == FlatHeightsMode.sizes) {
float measuredWidth = 0;
for (int i = 0; i < lineCount; i++) {
measuredWidth = Math.max(measuredWidth, layout.getLineMax(i));
}
widths.pushDouble(measuredWidth / density);
}
widths.pushDouble(measuredWidth / density);
}

heights.pushDouble(height);
}

if (includeWidths) {
final WritableMap output = Arguments.createMap();
output.putArray("widths", widths);
output.putArray("heights", heights);
promise.resolve(output);
} else {
promise.resolve(heights);
switch (mode) {
case sizes: {
final WritableMap output = Arguments.createMap();
// We output an object with 3 arrays instead of an array of
// objects because it's much faster.
//
// Changing this output to arrays of objects quadrupled the
// running time of flatSizes: 1000 iterations of the
// following code went from 13ms to 47ms each.
//
// ```ts
// const heightsParams: TSHeightsParams = {
// text: _.times(
// 20,
// () =>
// 'This is some text that is quite long. It should wrap onto a few lines',
// ),
// ...defaultTextStyle,
// width: 150,
// };
//
// await TextSize.flatSizes(heightsParams);
// ```
output.putArray("widths", widths);
output.putArray("heights", heights);
promise.resolve(output);
break;
}
case heights: {
promise.resolve(heights);
break;
}
}
} catch (Exception e) {
promise.reject(E_UNKNOWN_ERROR, e);
Expand Down
21 changes: 21 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,29 @@ declare module "react-native-text-size" {

interface TextSizeStatic {
measure(params: TSMeasureParams): Promise<TSMeasureResult>;

/**
* On Android, we benchmarked this to take 1.55x the time of flatHeights.
* Measuring the sizes for the following input 1000 times took 13.38ms vs.
* 8.6ms.
*
* On iOS, this should run at roughly the same speed as flatHeights.
*
* ```
* {
* text: _.times(
* 20,
* () =>
* 'This is some text that is quite long. It should wrap onto a few lines',
* ),
* ...defaultTextStyle,
* width: 150,
* };
* ```
*/
flatSizes(params: TSHeightsParams): Promise<TSFlatSizes>;
flatHeights(params: TSHeightsParams): Promise<number[]>;

specsForTextStyles(): Promise<{ [key: string]: TSFontForStyle }>;
fontFromSpecs(specs?: TSFontSpecs): Promise<TSFontInfo>;
fontFamilyNames(): Promise<string[]>;
Expand Down