Skip to content

Commit

Permalink
improve text line height calculation
Browse files Browse the repository at this point in the history
Summary:
## Context
There is a recurring issue in React Native where text containing accented characters, non-latin scripts, and special glyphs experiences truncation when line height is set too small, even when the line height equals or exceeds the font size. This problem has a significant impact, particularly when rendering complex text in multiple languages or special character sets.

See: https://docs.google.com/document/d/1W-A80gKAyhVbz_WKktSwwJP5qm6h6ZBjFNcsVbknXhI/edit?usp=sharing for more context

## Investigation
Previously, when font metrics (ascent, descent, top, bottom) exceeded the line height, the logic arbitrarily prioritized descent over ascent and bottom top. This led to vertical misalignment and text clipping at the top of the text.

## Proposed Implementation:

1. Descent Exceeds Line Height:
Descent is capped to fit within the line height, setting ascent and top to 0, similar to the current behavior.
2. Shrink ascent and descent equally:
When the combined ascent and descent exceed the line height, the vertical deficit is split proportionally between them, ensuring even distribution of the space.
3. Proportionally shrink top and bottom:
If the top and bottom together exceed the line height, reductions are now applied proportionally based on the delta between top and ascent and bottom and descent.

Differential Revision: D62295350
  • Loading branch information
mellyeliu authored and facebook-github-bot committed Sep 6, 2024
1 parent 5e77784 commit 5fbcc87
Showing 1 changed file with 33 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ package com.facebook.react.views.text.internal.span
import android.graphics.Paint.FontMetricsInt
import android.text.style.LineHeightSpan
import kotlin.math.ceil
import kotlin.math.abs
import kotlin.math.floor
import kotlin.math.min

/**
* We use a custom [LineHeightSpan], because `lineSpacingExtra` is broken. Details here:
Expand All @@ -26,31 +26,48 @@ public class CustomLineHeightSpan(height: Float) : LineHeightSpan, ReactSpan {
end: Int,
spanstartv: Int,
v: Int,
fm: FontMetricsInt
fm: FontMetricsInt,
) {
// This is more complicated that I wanted it to be. You can find a good explanation of what the
// FontMetrics mean here: http://stackoverflow.com/questions/27631736.
// The general solution is that if there's not enough height to show the full line height, we
// will prioritize in this order: descent, ascent, bottom, top

// will generally try to distribute the deficit proportionally between the ascent/top and descent/bottom.
if (fm.descent > lineHeight) {
// Show as much descent as possible
fm.descent = min(lineHeight.toDouble(), fm.descent.toDouble()).toInt()
fm.bottom = fm.descent
fm.descent = lineHeight
fm.bottom = fm.ascent
fm.top = 0
fm.ascent = 0
fm.top = fm.ascent
} else if (-fm.ascent + fm.descent > lineHeight) {
// Show all descent, and as much ascent as possible
fm.bottom = fm.descent
fm.ascent = -lineHeight + fm.descent
fm.top = fm.ascent
} else if (-fm.ascent + fm.bottom > lineHeight) {
// Show all ascent, descent, as much bottom as possible
// Calculate the amount we are over, and split the adjustment between descent and ascent
val difference = -(lineHeight + fm.ascent - fm.descent)/ 2
val remainder = difference % 2
fm.ascent = fm.ascent + difference
fm.descent = fm.descent - difference - remainder
fm.top = fm.ascent
fm.bottom = fm.ascent + lineHeight
fm.bottom = fm.descent
} else if (-fm.top + fm.bottom > lineHeight) {
// Show all ascent, descent, bottom, as much top as possible
fm.top = fm.bottom - lineHeight
val excess = (-fm.top + fm.bottom) - lineHeight

// Calculate the differences from top to ascent and bottom to descent
val topToAscent = abs(fm.top - fm.ascent)
val bottomToDescent = abs(fm.bottom - fm.descent)

// Calculate the total delta
val totalDelta = topToAscent + bottomToDescent

// Calculate proportional reductions
val topReduction = (excess * topToAscent / totalDelta).toInt()
val bottomReduction = (excess * bottomToDescent / totalDelta).toInt()

// Adjust fm.top and fm.bottom
fm.top += topReduction
fm.bottom += bottomReduction

// If there's a remainder, put it on the top
val remainder = excess - (topReduction + bottomReduction)
fm.top += remainder
} else {
// Show proportionally additional ascent / top & descent / bottom
val additional = lineHeight - (-fm.top + fm.bottom)
Expand Down

0 comments on commit 5fbcc87

Please sign in to comment.