Skip to content

Commit c9828c5

Browse files
feat(Android): line height (#431)
# Summary Adds support for lineHeight style on Android. ## Test Plan - Specify `lineHeight` style prop - Notice that line height is properly updated ## Screenshots / Videos https://github.com/user-attachments/assets/51488bc8-7156-42f4-ac53-104df858c240 ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ❌ | | Android | ✅ | --------- Co-authored-by: Kacper Żółkiewski <kacper.zolkiewski@swmansion.com>
1 parent ef610cc commit c9828c5

File tree

8 files changed

+97
-8
lines changed

8 files changed

+97
-8
lines changed

android/src/main/java/com/swmansion/enriched/common/spans/EnrichedCheckboxListSpan.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ open class EnrichedCheckboxListSpan(
8080
if (spannedText.getSpanStart(this) == start) {
8181
checkboxDrawable.update(isChecked)
8282

83-
val lineCenter = (top + bottom) / 2f
84-
val drawableTop = lineCenter - (enrichedStyle.ulCheckboxBoxSize / 2f)
83+
val fm = paint.fontMetricsInt
84+
val textCenter = baseline + (fm.ascent + fm.descent) / 2f
85+
val drawableTop = textCenter - (enrichedStyle.ulCheckboxBoxSize / 2f)
8586

8687
canvas.withTranslation(x.toFloat() + enrichedStyle.ulCheckboxMarginLeft, drawableTop) {
8788
checkboxDrawable.draw(this)

android/src/main/java/com/swmansion/enriched/common/spans/EnrichedUnorderedListSpan.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ open class EnrichedUnorderedListSpan(
4949
paint.style = Paint.Style.FILL
5050

5151
val bulletRadius = enrichedStyle.ulBulletSize / 2f
52-
val yPosition = (top + bottom) / 2f
52+
val fm = paint.fontMetricsInt
53+
val yPosition = baseline + (fm.ascent + fm.descent) / 2f
5354
val xPosition = x + dir * bulletRadius + enrichedStyle.ulMarginLeft
5455

5556
canvas.drawCircle(xPosition, yPosition, bulletRadius, paint)

android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@ import android.view.inputmethod.InputConnection
2222
import android.view.inputmethod.InputMethodManager
2323
import androidx.appcompat.widget.AppCompatEditText
2424
import androidx.core.view.ViewCompat
25-
import androidx.core.view.inputmethod.EditorInfoCompat
26-
import androidx.core.view.inputmethod.InputConnectionCompat
2725
import com.facebook.react.bridge.ReactContext
2826
import com.facebook.react.bridge.ReadableMap
2927
import com.facebook.react.common.ReactConstants
@@ -46,6 +44,7 @@ import com.swmansion.enriched.textinput.spans.EnrichedInputH4Span
4644
import com.swmansion.enriched.textinput.spans.EnrichedInputH5Span
4745
import com.swmansion.enriched.textinput.spans.EnrichedInputH6Span
4846
import com.swmansion.enriched.textinput.spans.EnrichedInputImageSpan
47+
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
4948
import com.swmansion.enriched.textinput.spans.EnrichedSpans
5049
import com.swmansion.enriched.textinput.spans.interfaces.EnrichedInputSpan
5150
import com.swmansion.enriched.textinput.styles.HtmlStyle
@@ -96,6 +95,7 @@ class EnrichedTextInputView : AppCompatEditText {
9695
var experimentalSynchronousEvents: Boolean = false
9796

9897
var fontSize: Float? = null
98+
private var lineHeight: Float? = null
9999
private var autoFocus = false
100100
private var typefaceDirty = false
101101
private var didAttachToWindow = false
@@ -312,6 +312,7 @@ class EnrichedTextInputView : AppCompatEditText {
312312
runAsATransaction {
313313
val newText = parseText(value)
314314
setText(newText)
315+
applyLineSpacing()
315316

316317
observeAsyncImages()
317318

@@ -424,6 +425,28 @@ class EnrichedTextInputView : AppCompatEditText {
424425
forceScrollToSelection()
425426
}
426427

428+
fun setLineHeight(height: Float) {
429+
lineHeight = if (height == 0f) null else height
430+
applyLineSpacing()
431+
layoutManager.invalidateLayout()
432+
forceScrollToSelection()
433+
}
434+
435+
private fun applyLineSpacing() {
436+
val spannable = text as? Spannable ?: return
437+
spannable
438+
.getSpans(0, spannable.length, EnrichedLineHeightSpan::class.java)
439+
.forEach { spannable.removeSpan(it) }
440+
441+
val lh = lineHeight ?: return
442+
spannable.setSpan(
443+
EnrichedLineHeightSpan(lh),
444+
0,
445+
spannable.length,
446+
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
447+
)
448+
}
449+
427450
fun setFontFamily(family: String?) {
428451
if (family != fontFamily) {
429452
fontFamily = family

android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ class EnrichedTextInputViewManager :
178178
view: EnrichedTextInputView?,
179179
height: Float,
180180
) {
181-
// no-op
181+
view?.setLineHeight(height)
182182
}
183183

184184
@ReactProp(name = "fontFamily")

android/src/main/java/com/swmansion/enriched/textinput/MeasurementStore.kt

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.graphics.Typeface
55
import android.graphics.text.LineBreaker
66
import android.os.Build
77
import android.text.Spannable
8+
import android.text.SpannableString
89
import android.text.StaticLayout
910
import android.text.TextPaint
1011
import android.util.Log
@@ -16,6 +17,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
1617
import com.facebook.yoga.YogaMeasureMode
1718
import com.facebook.yoga.YogaMeasureOutput
1819
import com.swmansion.enriched.common.parser.EnrichedParser
20+
import com.swmansion.enriched.textinput.spans.EnrichedLineHeightSpan
1921
import com.swmansion.enriched.textinput.styles.HtmlStyle
2022
import java.util.concurrent.ConcurrentHashMap
2123
import kotlin.math.ceil
@@ -141,16 +143,31 @@ object MeasurementStore {
141143
): Long {
142144
val defaultView = EnrichedTextInputView(context)
143145

144-
val text = getInitialText(defaultView, props)
146+
val rawText = getInitialText(defaultView, props)
145147
val fontSize = getInitialFontSize(defaultView, props)
148+
val lineHeight = props?.getDouble("lineHeight")?.toFloat() ?: 0f
146149

147150
val fontFamily = props?.getString("fontFamily")
148151
val fontStyle = parseFontStyle(props?.getString("fontStyle"))
149152
val fontWeight = parseFontWeight(props?.getString("fontWeight"))
150153

154+
val text: CharSequence =
155+
if (lineHeight > 0f) {
156+
val spannable = SpannableString(rawText)
157+
spannable.setSpan(
158+
EnrichedLineHeightSpan(lineHeight),
159+
0,
160+
spannable.length,
161+
Spannable.SPAN_INCLUSIVE_INCLUSIVE,
162+
)
163+
spannable
164+
} else {
165+
rawText
166+
}
167+
151168
val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
152169
val paintParams = PaintParams(typeface, fontSize)
153-
val size = measure(width, text, PaintParams(typeface, fontSize))
170+
val size = measure(width, text, paintParams)
154171

155172
if (id != null) {
156173
data[id] = MeasurementParams(true, width, size, text, paintParams)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.swmansion.enriched.textinput.spans
2+
3+
import android.graphics.Paint
4+
import android.text.Spannable
5+
import android.text.TextPaint
6+
import android.text.style.LineHeightSpan
7+
import android.text.style.MetricAffectingSpan
8+
import com.facebook.react.uimanager.PixelUtil
9+
import com.swmansion.enriched.common.spans.interfaces.EnrichedHeadingSpan
10+
11+
class EnrichedLineHeightSpan(
12+
val lineHeight: Float,
13+
) : MetricAffectingSpan(),
14+
LineHeightSpan {
15+
override fun updateDrawState(p0: TextPaint?) {
16+
// Do nothing but inform TextView that line height should be recalculated
17+
}
18+
19+
override fun updateMeasureState(p0: TextPaint) {
20+
// Do nothing but inform TextView that line height should be recalculated
21+
}
22+
23+
override fun chooseHeight(
24+
text: CharSequence,
25+
start: Int,
26+
end: Int,
27+
spanstartv: Int,
28+
v: Int,
29+
fm: Paint.FontMetricsInt,
30+
) {
31+
val spannable = text as? Spannable ?: return
32+
// Do not modify line height for headings
33+
// In the future we may consider adding custom lineHeight support for each paragraph style
34+
if (spannable.getSpans(start, end, EnrichedHeadingSpan::class.java).isNotEmpty()) return
35+
36+
val lineHeightPx = PixelUtil.toPixelFromSP(lineHeight)
37+
val currentHeight = (fm.descent - fm.ascent).toFloat()
38+
if (lineHeightPx <= currentHeight) return
39+
40+
val extra = (lineHeightPx - currentHeight).toInt()
41+
fm.ascent -= extra
42+
fm.top = minOf(fm.top, fm.ascent)
43+
}
44+
}

android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ inline folly::dynamic toDynamic(const EnrichedTextInputViewProps &props) {
2222
serializedProps["fontWeight"] = props.fontWeight;
2323
serializedProps["fontStyle"] = props.fontStyle;
2424
serializedProps["fontFamily"] = props.fontFamily;
25+
serializedProps["lineHeight"] = props.lineHeight;
2526
serializedProps["htmlStyle"] = toDynamic(props.htmlStyle);
2627

2728
return serializedProps;
@@ -35,6 +36,7 @@ inline folly::dynamic toDynamic(const EnrichedTextInputViewProps &props) {
3536
serializedProps["fontWeight"] = props.fontWeight;
3637
serializedProps["fontStyle"] = props.fontStyle;
3738
serializedProps["fontFamily"] = props.fontFamily;
39+
serializedProps["lineHeight"] = props.lineHeight;
3840
// Ideally we should also serialize htmlStyle, but toDynamic function is not
3941
// generated in this RN version
4042
// As RN 0.79 and 0.80 is no longer supported, we can skip it for now

docs/API_REFERENCE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ Additionally following [TextStyle](https://reactnative.dev/docs/text#style) prop
476476
- fontFamily
477477
- fontSize
478478
- fontWeight
479+
- lineHeight
479480
- fontStyle only on Android
480481
- lineHeight only on iOS
481482

0 commit comments

Comments
 (0)