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 +12 -0
- package/android/src/main/java/com/advancedtext/AdvancedTextView.kt +65 -15
- package/android/src/main/java/com/advancedtext/AdvancedTextViewManager.kt +4 -1
- package/ios/AdvancedTextView.mm +34 -2
- package/lib/module/AdvancedTextViewNativeComponent.ts +3 -0
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts +1 -0
- package/lib/typescript/src/AdvancedTextViewNativeComponent.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/AdvancedTextViewNativeComponent.ts +3 -0
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
package/ios/AdvancedTextView.mm
CHANGED
|
@@ -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 {
|
|
@@ -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;
|
|
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
|
@@ -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 {
|