Skip to content

Commit 02f0a9c

Browse files
authored
feat(ios): add html support (#56)
* feat(android): add NitroText HybridView implementation - add Android Nitro module with Kotlin view, Fabric state updater, and JNI bridge - share component descriptor/state logic so Fabric can hydrate props on Android - update TypeScript entrypoint, codegen pipeline, and example app for Android support * feat: add support for `onTextLayout` * feat: implement fragment background color support in NitroText * feat(android): html support * feat: enhance NitroText with improved HTML rendering and whitespace handling - Implemented HTML fragment parsing to support a wider range of HTML elements and styles. - Added functionality to trim trailing whitespace in both HTML and text content. - Updated NitroText component to handle various inline and block elements, ensuring proper rendering and styling. - Enhanced the example app to showcase new HTML features and improved text layout. * feat: html support on ios - Introduced NitroHtmlRenderer for better HTML parsing and rendering capabilities. - Updated NitroTextImpl to support HTML content and fragment background colors. - Refactored NitroText to streamline rendering logic and improve performance. - Enhanced NitroTextView to handle link interactions more effectively. - Improved whitespace handling in text rendering for better layout consistency.
1 parent ca1ffb0 commit 02f0a9c

15 files changed

Lines changed: 2499 additions & 345 deletions

android/src/main/java/com/nitrotext/HtmlLinkMovementMethod.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ internal object HtmlLinkMovementMethod : MovementMethod {
1919

2020
override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean {
2121
val action = event.action
22-
2322
if (action != MotionEvent.ACTION_UP && action != MotionEvent.ACTION_DOWN) {
2423
return false
2524
}
@@ -41,7 +40,6 @@ internal object HtmlLinkMovementMethod : MovementMethod {
4140
}
4241
return true
4342
}
44-
4543
return false
4644
}
4745
}

android/src/main/java/com/nitrotext/NitroTextImpl.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,6 @@ class NitroTextImpl(private val view: AppCompatTextView) {
121121
val lines = numberOfLines?.toInt() ?: Int.MAX_VALUE
122122
view.maxLines = if (lines <= 0) Int.MAX_VALUE else lines
123123
view.isSingleLine = (lines == 1)
124-
125124
view.ellipsize = when (ellipsizeMode) {
126125
EllipsizeMode.HEAD -> TextUtils.TruncateAt.START
127126
EllipsizeMode.MIDDLE -> TextUtils.TruncateAt.MIDDLE

android/src/main/java/com/nitrotext/NitroTextView.kt

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@ import android.graphics.text.LineBreaker
77
import android.text.Layout
88
import android.text.TextPaint
99
import android.view.View
10+
import android.view.View.MeasureSpec
1011
import androidx.appcompat.widget.AppCompatTextView
1112
import com.facebook.react.views.view.ReactViewGroup
1213
import com.margelo.nitro.nitrotext.TextLayout
1314
import com.margelo.nitro.nitrotext.TextLayoutEvent
15+
import kotlin.math.max
16+
import kotlin.math.min
1417

1518

1619
interface NitroTextViewDelegate {
@@ -20,11 +23,16 @@ interface NitroTextViewDelegate {
2023
@SuppressLint("ViewConstructor")
2124
class NitroTextView(ctx: Context) : ReactViewGroup(ctx) {
2225
val textView = AppCompatTextView(ctx).apply {
23-
includeFontPadding = true
26+
includeFontPadding = false
2427
minWidth = 0; minHeight = 0
2528
breakStrategy = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
2629
hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NORMAL
2730
setHorizontallyScrolling(false)
31+
overScrollMode = OVER_SCROLL_NEVER
32+
isVerticalScrollBarEnabled = false
33+
isNestedScrollingEnabled = false
34+
isScrollContainer = false
35+
background = null
2836
}
2937

3038
var nitroTextDelegate: NitroTextViewDelegate? = null
@@ -44,6 +52,45 @@ class NitroTextView(ctx: Context) : ReactViewGroup(ctx) {
4452
LayoutParams.MATCH_PARENT
4553
)
4654
)
55+
56+
}
57+
58+
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
59+
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
60+
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
61+
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
62+
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
63+
64+
val horizontalPadding = paddingLeft + paddingRight
65+
val verticalPadding = paddingTop + paddingBottom
66+
67+
val childWidthSpec = when (widthMode) {
68+
MeasureSpec.UNSPECIFIED -> MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)
69+
else -> {
70+
val available = max(0, widthSize - horizontalPadding)
71+
MeasureSpec.makeMeasureSpec(available, widthMode)
72+
}
73+
}
74+
75+
val childHeightSpec = getChildMeasureSpec(heightMeasureSpec, verticalPadding, LayoutParams.WRAP_CONTENT)
76+
77+
textView.measure(childWidthSpec, childHeightSpec)
78+
79+
val measuredWidth = when (widthMode) {
80+
MeasureSpec.EXACTLY -> widthSize
81+
MeasureSpec.AT_MOST -> min(widthSize, textView.measuredWidth + horizontalPadding)
82+
MeasureSpec.UNSPECIFIED -> textView.measuredWidth + horizontalPadding
83+
else -> textView.measuredWidth + horizontalPadding
84+
}
85+
86+
val measuredHeight = when (heightMode) {
87+
MeasureSpec.EXACTLY -> heightSize
88+
MeasureSpec.AT_MOST -> min(heightSize, textView.measuredHeight + verticalPadding)
89+
MeasureSpec.UNSPECIFIED -> textView.measuredHeight + verticalPadding
90+
else -> textView.measuredHeight + verticalPadding
91+
}
92+
93+
setMeasuredDimension(measuredWidth, measuredHeight)
4794
}
4895

4996
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {

android/src/main/java/com/nitrotext/renderers/NitroHtmlRenderer.kt

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class NitroHtmlRenderer(
6666
traverse(document.body(), builder, stateStack, blockStack, blockRanges, listStack, ruleMap)
6767

6868
applyBlockMargins(builder, blockRanges)
69+
trimTrailingWhitespace(builder)
6970
return builder
7071
}
7172

@@ -86,9 +87,11 @@ class NitroHtmlRenderer(
8687

8788
private fun appendText(textNode: TextNode, builder: SpannableStringBuilder, state: StyleState) {
8889
val raw = if (state.preserveWhitespace) textNode.wholeText else textNode.text()
90+
if (!state.preserveWhitespace && raw.isBlank()) return
8991
if (raw.isEmpty()) return
9092

9193
val content = applyTextTransform(raw, state.style.textTransform)
94+
if (!state.preserveWhitespace && content.isBlank()) return
9295
if (content.isEmpty()) return
9396

9497
val start = builder.length
@@ -274,6 +277,21 @@ class NitroHtmlRenderer(
274277
}
275278
}
276279

280+
private fun trimTrailingWhitespace(builder: SpannableStringBuilder) {
281+
var end = builder.length
282+
while (end > 0) {
283+
val ch = builder[end - 1]
284+
if (ch == '\n' || ch == ' ') {
285+
end -= 1
286+
} else {
287+
break
288+
}
289+
}
290+
if (end < builder.length) {
291+
builder.delete(end, builder.length)
292+
}
293+
}
294+
277295
private fun applyListSpan(
278296
builder: SpannableStringBuilder,
279297
start: Int,
@@ -334,9 +352,9 @@ class NitroHtmlRenderer(
334352
private fun defaultStyleForTag(tag: String): RichTextStyle? = when (tag) {
335353
"strong", "b" -> createStyle(fontWeight = FontWeight.BOLD)
336354
"em", "i" -> createStyle(fontStyle = FontStyle.ITALIC)
337-
"u" -> createStyle(textDecorationLine = TextDecorationLine.UNDERLINE)
338-
"s", "strike", "del" -> createStyle(textDecorationLine = TextDecorationLine.LINE_THROUGH)
339-
"code" -> createStyle(fontFamily = "monospace")
355+
"u" -> createStyle(textDecorationLine = TextDecorationLine.UNDERLINE, textDecorationStyle = TextDecorationStyle.SOLID)
356+
"s", "strike", "del" -> createStyle(textDecorationLine = TextDecorationLine.LINE_THROUGH, textDecorationStyle = TextDecorationStyle.SOLID)
357+
"code", "pre" -> createStyle(fontFamily = "monospace")
340358
else -> null
341359
}
342360

0 commit comments

Comments
 (0)