react-native-advanced-text 0.2.15 → 0.2.16

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.
package/README.md CHANGED
@@ -38,6 +38,11 @@ import { AdvancedText } from "react-native-advanced-text";
38
38
  highlightColor: '#6baeffb5',
39
39
  borderRadius: 6,
40
40
  },
41
+ {
42
+ index: 7,
43
+ highlightColor: '#818CF8',
44
+ underline: true, // a line under the word instead of a background
45
+ },
41
46
  ]}
42
47
  fontSize={24}
43
48
  color={'#FFFFFF'}
@@ -47,6 +52,13 @@ import { AdvancedText } from "react-native-advanced-text";
47
52
  />
48
53
  ```
49
54
 
55
+ ## Underlined words
56
+
57
+ An entry in `highlightedWords` with `underline: true` draws a line under the
58
+ word in `highlightColor` instead of filling its background (`borderRadius` is
59
+ ignored). A word can be both highlighted and underlined: list it twice, once
60
+ with `underline: true` and once without.
61
+
50
62
  ## Sizing
51
63
 
52
64
  `AdvancedText` measures its own height, like React Native's built‑in `<Text>`.
@@ -66,6 +66,7 @@ class AdvancedTextView : TextView {
66
66
  private val rangePath = Path()
67
67
  private val lineClipPath = Path()
68
68
  private val rangeBounds = RectF()
69
+ private val underlineBounds = RectF()
69
70
 
70
71
  constructor(context: Context?) : super(context) { init() }
71
72
  constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) { init() }
@@ -394,14 +395,25 @@ class AdvancedTextView : TextView {
394
395
 
395
396
  highlightedWords.forEach { highlightedWord ->
396
397
  val wordPos = wordPositions.find { it.index == highlightedWord.index } ?: return@forEach
397
- drawRangeBackground(
398
- canvas,
399
- currentLayout,
400
- wordPos.start,
401
- wordPos.extendedEnd.coerceAtMost(currentText.length),
402
- parseColor(highlightedWord.highlightColor),
403
- highlightedWord.borderRadius * density
404
- )
398
+ if (highlightedWord.underline) {
399
+ // The word, but not the space after it.
400
+ drawRangeUnderline(
401
+ canvas,
402
+ currentLayout,
403
+ wordPos.start,
404
+ wordPos.end.coerceAtMost(currentText.length),
405
+ parseColor(highlightedWord.highlightColor)
406
+ )
407
+ } else {
408
+ drawRangeBackground(
409
+ canvas,
410
+ currentLayout,
411
+ wordPos.start,
412
+ wordPos.extendedEnd.coerceAtMost(currentText.length),
413
+ parseColor(highlightedWord.highlightColor),
414
+ highlightedWord.borderRadius * density
415
+ )
416
+ }
405
417
  }
406
418
 
407
419
  // Draw indicator background only in Highlight mode
@@ -420,10 +432,7 @@ class AdvancedTextView : TextView {
420
432
  }
421
433
  }
422
434
 
423
- // Fills the background of text[start, end), one box per line. The edges
424
- // come from Layout.getSelectionPath, which follows the text's direction:
425
- // a right-to-left word (Arabic) starts at its right edge, so treating
426
- // getPrimaryHorizontal(start) as the left edge drew boxes in the wrong place.
435
+ // Fills the background of text[start, end), one box per line.
427
436
  private fun drawRangeBackground(
428
437
  canvas: Canvas,
429
438
  layout: Layout,
@@ -432,8 +441,44 @@ class AdvancedTextView : TextView {
432
441
  color: Int,
433
442
  radius: Float
434
443
  ) {
435
- if (start >= end) return
436
444
  highlightPaint.color = color
445
+ forEachLineOfRange(layout, start, end) {
446
+ canvas.drawRoundRect(rangeBounds, radius, radius, highlightPaint)
447
+ }
448
+ }
449
+
450
+ // Draws a line under text[start, end), one per line it spans, just below
451
+ // the baseline like a text underline.
452
+ private fun drawRangeUnderline(
453
+ canvas: Canvas,
454
+ layout: Layout,
455
+ start: Int,
456
+ end: Int,
457
+ color: Int
458
+ ) {
459
+ val density = resources.displayMetrics.density
460
+ val thickness = UNDERLINE_THICKNESS_DP * density
461
+ val gap = UNDERLINE_GAP_DP * density
462
+ highlightPaint.color = color
463
+ forEachLineOfRange(layout, start, end) { line ->
464
+ val top = layout.getLineBaseline(line) + totalPaddingTop + gap
465
+ underlineBounds.set(rangeBounds.left, top, rangeBounds.right, top + thickness)
466
+ canvas.drawRoundRect(underlineBounds, thickness / 2, thickness / 2, highlightPaint)
467
+ }
468
+ }
469
+
470
+ // Calls draw for each line text[start, end) spans, with rangeBounds set to
471
+ // that line's part of the range in view coordinates. The edges come from
472
+ // Layout.getSelectionPath, which follows the text's direction: a
473
+ // right-to-left word (Arabic) starts at its right edge, so treating
474
+ // getPrimaryHorizontal(start) as the left edge put boxes in the wrong place.
475
+ private inline fun forEachLineOfRange(
476
+ layout: Layout,
477
+ start: Int,
478
+ end: Int,
479
+ draw: (line: Int) -> Unit
480
+ ) {
481
+ if (start >= end) return
437
482
 
438
483
  val startLine = layout.getLineForOffset(start)
439
484
  val endLine = layout.getLineForOffset(end - 1)
@@ -461,7 +506,7 @@ class AdvancedTextView : TextView {
461
506
  if (rangeBounds.isEmpty) continue
462
507
 
463
508
  rangeBounds.offset(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat())
464
- canvas.drawRoundRect(rangeBounds, radius, radius, highlightPaint)
509
+ draw(line)
465
510
  }
466
511
  }
467
512
 
@@ -589,5 +634,10 @@ class AdvancedTextView : TextView {
589
634
  data class HighlightedWord(
590
635
  val index: Int,
591
636
  val highlightColor: String,
592
- val borderRadius: Float = 0f
637
+ val borderRadius: Float = 0f,
638
+ // Draws a line under the word instead of filling its background.
639
+ val underline: Boolean = false
593
640
  )
641
+
642
+ private const val UNDERLINE_THICKNESS_DP = 1.5f
643
+ private const val UNDERLINE_GAP_DP = 2.5f
@@ -54,7 +54,10 @@ class AdvancedTextViewManager : SimpleViewManager<AdvancedTextView>() {
54
54
  wordMap.getDouble("borderRadius").toFloat()
55
55
  } else {
56
56
  0f
57
- }
57
+ },
58
+ underline = wordMap.hasKey("underline") &&
59
+ !wordMap.isNull("underline") &&
60
+ wordMap.getBoolean("underline")
58
61
  )
59
62
  )
60
63
  }
@@ -31,6 +31,9 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
31
31
 
32
32
  @property (nonatomic, strong) NSMutableArray<NSDictionary *> *wordRanges;
33
33
  @property (nonatomic, strong) NSMutableDictionary<NSNumber *, NSDictionary *> *highlightStyles;
34
+ // Underline colors by word index, kept apart from highlightStyles so a word
35
+ // can be both highlighted and underlined.
36
+ @property (nonatomic, strong) NSMutableDictionary<NSNumber *, UIColor *> *underlineStyles;
34
37
  @property (nonatomic, strong) NSArray<NSString *> *menuOptions;
35
38
  @property (nonatomic, assign) CGFloat fontSize;
36
39
  @property (nonatomic, strong) NSString *fontWeight;
@@ -109,6 +112,7 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
109
112
 
110
113
  _wordRanges = [NSMutableArray array];
111
114
  _highlightStyles = [NSMutableDictionary dictionary];
115
+ _underlineStyles = [NSMutableDictionary dictionary];
112
116
  _fontSize = 16.0;
113
117
  _fontWeight = @"normal";
114
118
  _textColor = [UIColor labelColor];
@@ -237,7 +241,8 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
237
241
  const auto &newHW = newViewProps.highlightedWords[i];
238
242
  if (oldHW.index != newHW.index ||
239
243
  oldHW.highlightColor != newHW.highlightColor ||
240
- oldHW.borderRadius != newHW.borderRadius) {
244
+ oldHW.borderRadius != newHW.borderRadius ||
245
+ oldHW.underline != newHW.underline) {
241
246
  highlightsChanged = YES;
242
247
  break;
243
248
  }
@@ -410,6 +415,7 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
410
415
  highlightedWords.size());
411
416
  @try {
412
417
  [_highlightStyles removeAllObjects];
418
+ [_underlineStyles removeAllObjects];
413
419
 
414
420
  for (const auto &hw : highlightedWords) {
415
421
  NSInteger index = hw.index;
@@ -417,7 +423,9 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
417
423
  UIColor *color = [self hexStringToColor:colorString];
418
424
  CGFloat borderRadius = static_cast<CGFloat>(hw.borderRadius);
419
425
 
420
- if (color) {
426
+ if (color && hw.underline) {
427
+ _underlineStyles[@(index)] = color;
428
+ } else if (color) {
421
429
  _highlightStyles[@(index)] = @{
422
430
  @"color": color,
423
431
  @"borderRadius": @(MAX(borderRadius, 0.0))
@@ -499,6 +507,7 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
499
507
 
500
508
 
501
509
  [self applyWordColorsToString:attributedString];
510
+ [self applyUnderlinesToString:attributedString];
502
511
 
503
512
  _textView.attributedText = attributedString;
504
513
  [_textView setNeedsDisplay];
@@ -545,6 +554,29 @@ static UIFont *ATResolveFont(NSString *family, CGFloat size)
545
554
  }
546
555
  }
547
556
 
557
+ // A text underline under each underlined word, but not the space after it.
558
+ - (void)applyUnderlinesToString:(NSMutableAttributedString *)string
559
+ {
560
+ if (_underlineStyles.count == 0) {
561
+ return;
562
+ }
563
+
564
+ for (NSDictionary *wordInfo in _wordRanges) {
565
+ UIColor *color = _underlineStyles[wordInfo[@"index"]];
566
+ if (!color) {
567
+ continue;
568
+ }
569
+ NSRange wordRange = [wordInfo[@"range"] rangeValue];
570
+ if (NSMaxRange(wordRange) > string.length) {
571
+ break;
572
+ }
573
+ [string addAttributes:@{
574
+ NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle),
575
+ NSUnderlineColorAttributeName: color
576
+ } range:wordRange];
577
+ }
578
+ }
579
+
548
580
  // Recolors the live textStorage instead of reassigning attributedText, so the
549
581
  // indicator can advance while the user is selecting text: attribute-only
550
582
  // edits keep selectedRange and the edit menu, assigning attributedText resets them.
@@ -8,6 +8,9 @@ interface HighlightedWord {
8
8
  index: Int32;
9
9
  highlightColor: string;
10
10
  borderRadius?: Float;
11
+ // Underlines the word in highlightColor instead of filling its background.
12
+ // A word can have both: list it once with and once without.
13
+ underline?: boolean;
11
14
  }
12
15
 
13
16
  export interface NativeProps extends ViewProps {
@@ -4,6 +4,7 @@ interface HighlightedWord {
4
4
  index: Int32;
5
5
  highlightColor: string;
6
6
  borderRadius?: Float;
7
+ underline?: boolean;
7
8
  }
8
9
  export interface NativeProps extends ViewProps {
9
10
  text: string;
@@ -1 +1 @@
1
- {"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IACjE,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;;AAED,wBAAuE"}
1
+ {"version":3,"file":"AdvancedTextViewNativeComponent.d.ts","sourceRoot":"","sources":["../../../src/AdvancedTextViewNativeComponent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAG9C,OAAO,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAG,KAAK,EAAG,MAAM,2CAA2C,CAAC;AAEpG,UAAU,eAAe;IACvB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,KAAK,CAAC;IAGrB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;IAClD,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACpC,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,KAAK,CAAA;KAAE,CAAC,CAAC;IACjE,WAAW,CAAC,EAAE,kBAAkB,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC1E,QAAQ,CAAC,EAAE,KAAK,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,KAAK,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,CAAC;IACtB,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;;AAED,wBAAuE"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-advanced-text",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "description": " Advanced text component for React Native with custom select options.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -8,6 +8,9 @@ interface HighlightedWord {
8
8
  index: Int32;
9
9
  highlightColor: string;
10
10
  borderRadius?: Float;
11
+ // Underlines the word in highlightColor instead of filling its background.
12
+ // A word can have both: list it once with and once without.
13
+ underline?: boolean;
11
14
  }
12
15
 
13
16
  export interface NativeProps extends ViewProps {