react-native-highlight-text-view 0.1.30 → 0.1.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import android.content.Context
4
4
  import android.graphics.Canvas
5
5
  import android.graphics.Color
6
6
  import android.graphics.Paint
7
+ import android.graphics.Path
7
8
  import android.graphics.RectF
8
9
  import android.graphics.Typeface
9
10
  import android.text.Editable
@@ -42,6 +43,7 @@ class HighlightTextView : AppCompatEditText {
42
43
 
43
44
  // Line height control
44
45
  private var customLineHeight: Float = 0f
46
+ private var customLineSpacing: Float = 0f
45
47
 
46
48
  // Font + alignment state
47
49
  private var currentFontFamily: String? = null
@@ -58,6 +60,8 @@ class HighlightTextView : AppCompatEditText {
58
60
  style = Paint.Style.FILL
59
61
  }
60
62
  private val backgroundRect = RectF()
63
+ private val backgroundPath = Path()
64
+ private val radii = FloatArray(8)
61
65
 
62
66
  var onTextChangeListener: ((String) -> Unit)? = null
63
67
 
@@ -80,7 +84,7 @@ class HighlightTextView : AppCompatEditText {
80
84
  private fun init() {
81
85
  setBackgroundColor(Color.TRANSPARENT)
82
86
  setTextSize(TypedValue.COMPLEX_UNIT_SP, 32f)
83
- gravity = Gravity.CENTER
87
+ gravity = Gravity.START or Gravity.CENTER_VERTICAL
84
88
  setPadding(20, 20, 20, 20)
85
89
  textColorValue = currentTextColor
86
90
 
@@ -88,6 +92,7 @@ class HighlightTextView : AppCompatEditText {
88
92
  maxLines = Int.MAX_VALUE
89
93
  isSingleLine = false
90
94
  setHorizontallyScrolling(false)
95
+ includeFontPadding = false
91
96
 
92
97
  applyLineHeightAndSpacing()
93
98
 
@@ -148,6 +153,37 @@ class HighlightTextView : AppCompatEditText {
148
153
  val lineStart = layout.getLineStart(line)
149
154
  val lineEnd = layout.getLineEnd(line)
150
155
 
156
+ // Determine adjacency for selective rounding
157
+ val hasLeftNeighbor = if (i > 0) {
158
+ val prevCh = text[i - 1]
159
+ val sameLine = layout.getLineForOffset(i - 1) == line
160
+ sameLine && prevCh != '\n' && prevCh != '\t'
161
+ } else false
162
+
163
+ val hasRightNeighbor = if (i < length - 1) {
164
+ val nextCh = text[i + 1]
165
+ val sameLine = layout.getLineForOffset(i + 1) == line
166
+
167
+ if (!sameLine || nextCh == '\n' || nextCh == '\t') {
168
+ false
169
+ } else if (nextCh == ' ') {
170
+ // Lookahead: If next is space, check if any visible char follows on same line
171
+ var hasVisibleAfter = false
172
+ for (k in i + 2 until length) {
173
+ if (layout.getLineForOffset(k) != line) break
174
+ val c = text[k]
175
+ if (c == '\n' || c == '\t') break
176
+ if (c != ' ') {
177
+ hasVisibleAfter = true
178
+ break
179
+ }
180
+ }
181
+ hasVisibleAfter
182
+ } else {
183
+ true
184
+ }
185
+ } else false
186
+
151
187
  // Horizontal bounds based on layout positions
152
188
  val xStart = layout.getPrimaryHorizontal(i)
153
189
  val isLastCharInLine = i == lineEnd - 1
@@ -179,13 +215,189 @@ class HighlightTextView : AppCompatEditText {
179
215
  top -= charPaddingTop
180
216
  bottom += charPaddingBottom
181
217
 
218
+ if (customLineSpacing < 0f) {
219
+ val originalLineTop = layout.getLineTop(line).toFloat()
220
+ val originalLineBottom = layout.getLineBottom(line).toFloat()
221
+
222
+ if (line > 0 && top < originalLineTop) {
223
+ val prevLineBottom = layout.getLineBottom(line - 1).toFloat()
224
+ if (top < prevLineBottom) {
225
+ top = prevLineBottom
226
+ }
227
+ }
228
+
229
+ if (line < layout.lineCount - 1 && bottom > originalLineBottom) {
230
+ val nextLineTop = layout.getLineTop(line + 1).toFloat()
231
+ if (bottom > nextLineTop) {
232
+ bottom = nextLineTop
233
+ }
234
+ }
235
+ }
236
+
182
237
  if (right <= left || bottom <= top) continue
183
238
 
184
239
  backgroundRect.set(left, top, right, bottom)
185
- canvas.drawRoundRect(backgroundRect, radius, radius, backgroundPaint)
240
+
241
+ // Detect paragraph boundaries (empty lines above/below)
242
+ val isFirstLineOfParagraph = line == 0 || isLineEmpty(text, layout, line - 1)
243
+ val isLastLineOfParagraph = line == layout.lineCount - 1 || isLineEmpty(text, layout, line + 1)
244
+
245
+ // Detect text alignment from view's gravity
246
+ val horizontalGravity = gravity and Gravity.HORIZONTAL_GRAVITY_MASK
247
+ val isLeftAligned = horizontalGravity == Gravity.START || horizontalGravity == Gravity.LEFT
248
+ val isRightAligned = horizontalGravity == Gravity.END || horizontalGravity == Gravity.RIGHT
249
+ val isCenterAligned = horizontalGravity == Gravity.CENTER_HORIZONTAL
250
+
251
+ var tl = 0f
252
+ var tr = 0f
253
+ var br = 0f
254
+ var bl = 0f
255
+
256
+ when {
257
+ isLeftAligned -> {
258
+ // LEFT ALIGNMENT (default behavior)
259
+ // Left Edge Logic
260
+ if (!hasLeftNeighbor) {
261
+ // Top-Left: Round if first line of paragraph
262
+ tl = if (isFirstLineOfParagraph) radius else 0f
263
+ // Bottom-Left: Round if last line of paragraph
264
+ bl = if (isLastLineOfParagraph) radius else 0f
265
+ }
266
+
267
+ // Right Edge Logic
268
+ if (!hasRightNeighbor) {
269
+ val currentLineWidth = layout.getLineMax(line)
270
+
271
+ // Top-Right
272
+ if (isFirstLineOfParagraph) {
273
+ tr = radius
274
+ } else {
275
+ val prevLineWidth = layout.getLineMax(line - 1)
276
+ // Round Top-Right if we stick out further than the line above
277
+ tr = if (currentLineWidth > prevLineWidth) radius else 0f
278
+ }
279
+
280
+ // Bottom-Right
281
+ if (isLastLineOfParagraph) {
282
+ br = radius
283
+ } else {
284
+ val nextLineWidth = layout.getLineMax(line + 1)
285
+ // Round Bottom-Right if we overhang the line below
286
+ br = if (currentLineWidth > nextLineWidth) radius else 0f
287
+ }
288
+ }
289
+ }
290
+
291
+ isRightAligned -> {
292
+ // RIGHT ALIGNMENT (mirror of left alignment)
293
+ // Right Edge Logic
294
+ if (!hasRightNeighbor) {
295
+ // Top-Right: Round if first line of paragraph
296
+ tr = if (isFirstLineOfParagraph) radius else 0f
297
+ // Bottom-Right: Round if last line of paragraph
298
+ br = if (isLastLineOfParagraph) radius else 0f
299
+ }
300
+
301
+ // Left Edge Logic
302
+ if (!hasLeftNeighbor) {
303
+ val currentLineWidth = layout.getLineMax(line)
304
+
305
+ // Top-Left
306
+ if (isFirstLineOfParagraph) {
307
+ tl = radius
308
+ } else {
309
+ val prevLineWidth = layout.getLineMax(line - 1)
310
+ // Round Top-Left if we stick out further than the line above
311
+ tl = if (currentLineWidth > prevLineWidth) radius else 0f
312
+ }
313
+
314
+ // Bottom-Left
315
+ if (isLastLineOfParagraph) {
316
+ bl = radius
317
+ } else {
318
+ val nextLineWidth = layout.getLineMax(line + 1)
319
+ // Round Bottom-Left if we overhang the line below
320
+ bl = if (currentLineWidth > nextLineWidth) radius else 0f
321
+ }
322
+ }
323
+ }
324
+
325
+ isCenterAligned -> {
326
+ // CENTER ALIGNMENT
327
+ val currentLineWidth = layout.getLineMax(line)
328
+
329
+ // Left Edge Logic
330
+ if (!hasLeftNeighbor) {
331
+ // Top-Left
332
+ if (isFirstLineOfParagraph) {
333
+ tl = radius
334
+ } else {
335
+ val prevLineWidth = layout.getLineMax(line - 1)
336
+ // Round Top-Left if we stick out further than the line above
337
+ tl = if (currentLineWidth > prevLineWidth) radius else 0f
338
+ }
339
+
340
+ // Bottom-Left
341
+ if (isLastLineOfParagraph) {
342
+ bl = radius
343
+ } else {
344
+ val nextLineWidth = layout.getLineMax(line + 1)
345
+ // Round Bottom-Left if we overhang the line below
346
+ bl = if (currentLineWidth > nextLineWidth) radius else 0f
347
+ }
348
+ }
349
+
350
+ // Right Edge Logic
351
+ if (!hasRightNeighbor) {
352
+ // Top-Right
353
+ if (isFirstLineOfParagraph) {
354
+ tr = radius
355
+ } else {
356
+ val prevLineWidth = layout.getLineMax(line - 1)
357
+ // Round Top-Right if we stick out further than the line above
358
+ tr = if (currentLineWidth > prevLineWidth) radius else 0f
359
+ }
360
+
361
+ // Bottom-Right
362
+ if (isLastLineOfParagraph) {
363
+ br = radius
364
+ } else {
365
+ val nextLineWidth = layout.getLineMax(line + 1)
366
+ // Round Bottom-Right if we overhang the line below
367
+ br = if (currentLineWidth > nextLineWidth) radius else 0f
368
+ }
369
+ }
370
+ }
371
+ }
372
+
373
+ // Arrays: Top-Left x,y; Top-Right x,y; Bottom-Right x,y; Bottom-Left x,y
374
+ radii[0] = tl; radii[1] = tl
375
+ radii[2] = tr; radii[3] = tr
376
+ radii[4] = br; radii[5] = br
377
+ radii[6] = bl; radii[7] = bl
378
+
379
+ backgroundPath.reset()
380
+ backgroundPath.addRoundRect(backgroundRect, radii, Path.Direction.CW)
381
+ canvas.drawPath(backgroundPath, backgroundPaint)
186
382
  }
187
383
  }
188
384
 
385
+ private fun isLineEmpty(text: CharSequence, layout: android.text.Layout, line: Int): Boolean {
386
+ if (line < 0 || line >= layout.lineCount) return false
387
+
388
+ val lineStart = layout.getLineStart(line)
389
+ val lineEnd = layout.getLineEnd(line)
390
+
391
+ // Check if line contains only whitespace/newlines
392
+ for (i in lineStart until lineEnd) {
393
+ val ch = text[i]
394
+ if (ch != '\n' && ch != '\t' && ch != ' ') {
395
+ return false
396
+ }
397
+ }
398
+ return true
399
+ }
400
+
189
401
  // --- Public API used from the ViewManager ------------------------------------
190
402
 
191
403
  fun setCharacterBackgroundColor(color: Int) {
@@ -371,6 +583,13 @@ class HighlightTextView : AppCompatEditText {
371
583
  post { invalidate() }
372
584
  }
373
585
 
586
+ fun setCustomLineSpacing(spacing: Float) {
587
+ customLineSpacing = spacing
588
+ applyLineHeightAndSpacing()
589
+ requestLayout()
590
+ post { invalidate() }
591
+ }
592
+
374
593
  fun setLetterSpacingProp(points: Float) {
375
594
  letterSpacingPoints = points
376
595
  applyLetterSpacing()
@@ -415,15 +634,24 @@ class HighlightTextView : AppCompatEditText {
415
634
  // --- Layout helpers ----------------------------------------------------------
416
635
 
417
636
  private fun applyLineHeightAndSpacing() {
637
+ val metrics = resources.displayMetrics
638
+
418
639
  if (customLineHeight > 0f) {
419
640
  // customLineHeight comes from JS as "points"; convert to px using scaledDensity
420
- val metrics = resources.displayMetrics
421
641
  val desiredLineHeightPx = customLineHeight * metrics.scaledDensity
422
642
  val textHeightPx = textSize
423
643
  if (textHeightPx > 0f) {
424
644
  val multiplier = desiredLineHeightPx / textHeightPx
425
- setLineSpacing(0f, multiplier)
645
+ val extraSpacing = if (customLineSpacing != 0f) {
646
+ customLineSpacing * metrics.scaledDensity
647
+ } else {
648
+ 0f
649
+ }
650
+ setLineSpacing(extraSpacing, multiplier)
426
651
  }
652
+ } else if (customLineSpacing != 0f) {
653
+ val extraSpacing = customLineSpacing * metrics.scaledDensity
654
+ setLineSpacing(extraSpacing, 1.0f)
427
655
  } else {
428
656
  // Default: add extra spacing equal to vertical padding so backgrounds don't collide
429
657
  val extraSpacing = charPaddingTop + charPaddingBottom
@@ -183,6 +183,13 @@ class HighlightTextViewManager : SimpleViewManager<HighlightTextView>(),
183
183
  }
184
184
  }
185
185
 
186
+ @ReactProp(name = "lineSpacing")
187
+ override fun setLineSpacing(view: HighlightTextView?, value: String?) {
188
+ value?.toFloatOrNull()?.let { spacing ->
189
+ view?.setCustomLineSpacing(spacing)
190
+ }
191
+ }
192
+
186
193
  @ReactProp(name = "highlightBorderRadius")
187
194
  override fun setHighlightBorderRadius(view: HighlightTextView?, value: String?) {
188
195
  value?.toFloatOrNull()?.let { radius ->
@@ -50,6 +50,7 @@ export interface HighlightTextViewProps extends ViewProps {
50
50
  /** Additional space between characters, in layout points (matches React Native's letterSpacing). */
51
51
  letterSpacing?: string;
52
52
  lineHeight?: string;
53
+ lineSpacing?: string;
53
54
  highlightBorderRadius?: string;
54
55
  padding?: string;
55
56
  paddingLeft?: string;
@@ -32,6 +32,7 @@ export interface HighlightTextViewProps extends ViewProps {
32
32
  /** Additional space between characters, in layout points (matches React Native's letterSpacing). */
33
33
  letterSpacing?: string;
34
34
  lineHeight?: string;
35
+ lineSpacing?: string;
35
36
  highlightBorderRadius?: string;
36
37
  padding?: string;
37
38
  paddingLeft?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"HighlightTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/HighlightTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,OAAO,GACP,SAAS,GACT,YAAY,GACZ,UAAU,GACV,KAAK,GACL,QAAQ,GACR,UAAU,GACV,YAAY,GACZ,WAAW,GACX,aAAa,GACb,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,WAAW,sBAAuB,SAAQ,SAAS;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oGAAoG;IACpG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iFAAiF;IACjF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;CACpD;;AAED,wBAEE"}
1
+ {"version":3,"file":"HighlightTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/HighlightTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEtF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,OAAO,GACP,SAAS,GACT,YAAY,GACZ,UAAU,GACV,KAAK,GACL,QAAQ,GACR,UAAU,GACV,YAAY,GACZ,WAAW,GACX,aAAa,GACb,eAAe,GACf,cAAc,CAAC;AAEnB,MAAM,WAAW,sBAAuB,SAAQ,SAAS;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,oGAAoG;IACpG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iFAAiF;IACjF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oFAAoF;IACpF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iFAAiF;IACjF,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,oBAAoB,CAAC,iBAAiB,CAAC,CAAC;CACpD;;AAED,wBAEE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-highlight-text-view",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "description": "A native text input for React Native that supports inline text highlighting",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -50,6 +50,7 @@ export interface HighlightTextViewProps extends ViewProps {
50
50
  /** Additional space between characters, in layout points (matches React Native's letterSpacing). */
51
51
  letterSpacing?: string;
52
52
  lineHeight?: string;
53
+ lineSpacing?: string;
53
54
  highlightBorderRadius?: string;
54
55
  padding?: string;
55
56
  paddingLeft?: string;