react-native-enriched 0.1.4 → 0.1.6
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/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerDelegate.java +3 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedTextInputViewManagerInterface.java +1 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
- package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.h +1 -0
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputView.kt +65 -28
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -46
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewManager.kt +16 -19
- package/android/src/main/java/com/swmansion/enriched/MeasurementStore.kt +158 -0
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedBlockQuoteSpan.kt +6 -2
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedCodeBlockSpan.kt +9 -5
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedInlineCodeSpan.kt +10 -5
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedOrderedListSpan.kt +11 -1
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedSpans.kt +61 -46
- package/android/src/main/java/com/swmansion/enriched/spans/EnrichedUnorderedListSpan.kt +11 -1
- package/android/src/main/java/com/swmansion/enriched/styles/ListStyles.kt +1 -2
- package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +29 -12
- package/android/src/main/java/com/swmansion/enriched/utils/EnrichedParser.java +23 -3
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedSpanWatcher.kt +0 -1
- package/android/src/main/java/com/swmansion/enriched/watchers/EnrichedTextWatcher.kt +1 -1
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.cpp +15 -2
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputMeasurementManager.h +1 -0
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/EnrichedTextInputShadowNode.cpp +1 -2
- package/android/src/main/new_arch/react/renderer/components/RNEnrichedTextInputViewSpec/conversions.h +27 -0
- package/ios/EnrichedTextInputView.h +0 -1
- package/ios/EnrichedTextInputView.mm +72 -36
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.cpp +5 -0
- package/ios/generated/RNEnrichedTextInputViewSpec/Props.h +1 -0
- package/ios/inputParser/InputParser.mm +32 -7
- package/ios/inputTextView/InputTextView.mm +3 -5
- package/ios/styles/LinkStyle.mm +7 -7
- package/ios/styles/OrderedListStyle.mm +3 -8
- package/ios/styles/UnorderedListStyle.mm +3 -8
- package/ios/utils/StringExtension.h +1 -1
- package/ios/utils/StringExtension.mm +17 -8
- package/lib/module/EnrichedTextInput.js +2 -0
- package/lib/module/EnrichedTextInput.js.map +1 -1
- package/lib/module/EnrichedTextInputNativeComponent.ts +1 -0
- package/lib/typescript/src/EnrichedTextInput.d.ts +2 -1
- package/lib/typescript/src/EnrichedTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts +1 -0
- package/lib/typescript/src/EnrichedTextInputNativeComponent.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/EnrichedTextInput.tsx +3 -0
- package/src/EnrichedTextInputNativeComponent.ts +1 -0
|
@@ -55,6 +55,9 @@ public class EnrichedTextInputViewManagerDelegate<T extends View, U extends Base
|
|
|
55
55
|
case "htmlStyle":
|
|
56
56
|
mViewManager.setHtmlStyle(view, (ReadableMap) value);
|
|
57
57
|
break;
|
|
58
|
+
case "scrollEnabled":
|
|
59
|
+
mViewManager.setScrollEnabled(view, value == null ? false : (boolean) value);
|
|
60
|
+
break;
|
|
58
61
|
case "color":
|
|
59
62
|
mViewManager.setColor(view, ColorPropConverter.getColor(value, view.getContext()));
|
|
60
63
|
break;
|
|
@@ -26,6 +26,7 @@ public interface EnrichedTextInputViewManagerInterface<T extends View> extends V
|
|
|
26
26
|
void setSelectionColor(T view, @Nullable Integer value);
|
|
27
27
|
void setAutoCapitalize(T view, @Nullable String value);
|
|
28
28
|
void setHtmlStyle(T view, @Nullable ReadableMap value);
|
|
29
|
+
void setScrollEnabled(T view, boolean value);
|
|
29
30
|
void setColor(T view, @Nullable Integer value);
|
|
30
31
|
void setFontSize(T view, float value);
|
|
31
32
|
void setFontFamily(T view, @Nullable String value);
|
package/android/generated/jni/react/renderer/components/RNEnrichedTextInputViewSpec/Props.cpp
CHANGED
|
@@ -30,6 +30,7 @@ EnrichedTextInputViewProps::EnrichedTextInputViewProps(
|
|
|
30
30
|
selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
|
|
31
31
|
autoCapitalize(convertRawProp(context, rawProps, "autoCapitalize", sourceProps.autoCapitalize, {})),
|
|
32
32
|
htmlStyle(convertRawProp(context, rawProps, "htmlStyle", sourceProps.htmlStyle, {})),
|
|
33
|
+
scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {false})),
|
|
33
34
|
color(convertRawProp(context, rawProps, "color", sourceProps.color, {})),
|
|
34
35
|
fontSize(convertRawProp(context, rawProps, "fontSize", sourceProps.fontSize, {0.0})),
|
|
35
36
|
fontFamily(convertRawProp(context, rawProps, "fontFamily", sourceProps.fontFamily, {})),
|
|
@@ -94,6 +95,10 @@ folly::dynamic EnrichedTextInputViewProps::getDiffProps(
|
|
|
94
95
|
result["htmlStyle"] = toDynamic(htmlStyle);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
if (scrollEnabled != oldProps->scrollEnabled) {
|
|
99
|
+
result["scrollEnabled"] = scrollEnabled;
|
|
100
|
+
}
|
|
101
|
+
|
|
97
102
|
if (color != oldProps->color) {
|
|
98
103
|
result["color"] = *color;
|
|
99
104
|
}
|
|
@@ -559,6 +559,7 @@ class EnrichedTextInputViewProps final : public ViewProps {
|
|
|
559
559
|
SharedColor selectionColor{};
|
|
560
560
|
std::string autoCapitalize{};
|
|
561
561
|
EnrichedTextInputViewHtmlStyleStruct htmlStyle{};
|
|
562
|
+
bool scrollEnabled{false};
|
|
562
563
|
SharedColor color{};
|
|
563
564
|
Float fontSize{0.0};
|
|
564
565
|
std::string fontFamily{};
|
|
@@ -7,6 +7,7 @@ import android.graphics.BlendMode
|
|
|
7
7
|
import android.graphics.BlendModeColorFilter
|
|
8
8
|
import android.graphics.Color
|
|
9
9
|
import android.graphics.Rect
|
|
10
|
+
import android.graphics.text.LineBreaker
|
|
10
11
|
import android.os.Build
|
|
11
12
|
import android.text.InputType
|
|
12
13
|
import android.text.Spannable
|
|
@@ -51,11 +52,9 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
51
52
|
val paragraphStyles: ParagraphStyles? = ParagraphStyles(this)
|
|
52
53
|
val listStyles: ListStyles? = ListStyles(this)
|
|
53
54
|
val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
|
|
54
|
-
// Sometimes setting up style triggers many changes in sequence
|
|
55
|
-
// Eg. removing conflicting styles -> changing text -> applying spans
|
|
56
|
-
// In such scenario we want to prevent from handling side effects (eg. onTextChanged)
|
|
57
55
|
var isDuringTransaction: Boolean = false
|
|
58
56
|
var isRemovingMany: Boolean = false
|
|
57
|
+
var scrollEnabled: Boolean = true
|
|
59
58
|
|
|
60
59
|
val mentionHandler: MentionHandler? = MentionHandler(this)
|
|
61
60
|
var htmlStyle: HtmlStyle = HtmlStyle(this, null)
|
|
@@ -72,6 +71,8 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
72
71
|
private var fontFamily: String? = null
|
|
73
72
|
private var fontStyle: Int = ReactConstants.UNSET
|
|
74
73
|
private var fontWeight: Int = ReactConstants.UNSET
|
|
74
|
+
private var defaultValue: CharSequence? = null
|
|
75
|
+
private var defaultValueDirty: Boolean = false
|
|
75
76
|
|
|
76
77
|
private var inputMethodManager: InputMethodManager? = null
|
|
77
78
|
|
|
@@ -102,6 +103,10 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
102
103
|
gravity = Gravity.TOP or Gravity.START
|
|
103
104
|
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
104
105
|
|
|
106
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
107
|
+
breakStrategy = LineBreaker.BREAK_STRATEGY_HIGH_QUALITY
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
setPadding(0, 0, 0, 0)
|
|
106
111
|
setBackgroundColor(Color.TRANSPARENT)
|
|
107
112
|
|
|
@@ -135,6 +140,14 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
135
140
|
return super.onTouchEvent(ev)
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
override fun canScrollVertically(direction: Int): Boolean {
|
|
144
|
+
return scrollEnabled
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
override fun canScrollHorizontally(direction: Int): Boolean {
|
|
148
|
+
return scrollEnabled
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
|
139
152
|
super.onSelectionChanged(selStart, selEnd)
|
|
140
153
|
selection?.onSelection(selStart, selEnd)
|
|
@@ -236,18 +249,17 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
236
249
|
|
|
237
250
|
fun setValue(value: CharSequence?) {
|
|
238
251
|
if (value == null) return
|
|
239
|
-
isDuringTransaction = true
|
|
240
|
-
|
|
241
|
-
val newText = parseText(value)
|
|
242
|
-
setText(newText)
|
|
243
252
|
|
|
244
|
-
|
|
245
|
-
|
|
253
|
+
runAsATransaction {
|
|
254
|
+
val newText = parseText(value)
|
|
255
|
+
setText(newText)
|
|
246
256
|
|
|
247
|
-
|
|
248
|
-
|
|
257
|
+
// Assign SpanWatcher one more time as our previous spannable has been replaced
|
|
258
|
+
addSpanWatcher(EnrichedSpanWatcher(this))
|
|
249
259
|
|
|
250
|
-
|
|
260
|
+
// Scroll to the last line of text
|
|
261
|
+
setSelection(text?.length ?: 0)
|
|
262
|
+
}
|
|
251
263
|
}
|
|
252
264
|
|
|
253
265
|
fun setAutoFocus(autoFocus: Boolean) {
|
|
@@ -305,7 +317,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
305
317
|
|
|
306
318
|
// This ensured that newly created spans will take the new font size into account
|
|
307
319
|
htmlStyle.invalidateStyles()
|
|
308
|
-
layoutManager.invalidateLayout(
|
|
320
|
+
layoutManager.invalidateLayout()
|
|
309
321
|
}
|
|
310
322
|
|
|
311
323
|
fun setFontFamily(family: String?) {
|
|
@@ -359,7 +371,24 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
359
371
|
return false
|
|
360
372
|
}
|
|
361
373
|
|
|
362
|
-
fun
|
|
374
|
+
fun afterUpdateTransaction() {
|
|
375
|
+
updateTypeface()
|
|
376
|
+
updateDefaultValue()
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
fun setDefaultValue(value: CharSequence?) {
|
|
380
|
+
defaultValue = value
|
|
381
|
+
defaultValueDirty = true
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private fun updateDefaultValue() {
|
|
385
|
+
if (!defaultValueDirty) return
|
|
386
|
+
|
|
387
|
+
defaultValueDirty = false
|
|
388
|
+
setValue(defaultValue ?: "")
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private fun updateTypeface() {
|
|
363
392
|
if (!typefaceDirty) return
|
|
364
393
|
typefaceDirty = false
|
|
365
394
|
|
|
@@ -367,7 +396,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
367
396
|
typeface = newTypeface
|
|
368
397
|
paint.typeface = newTypeface
|
|
369
398
|
|
|
370
|
-
layoutManager.invalidateLayout(
|
|
399
|
+
layoutManager.invalidateLayout()
|
|
371
400
|
}
|
|
372
401
|
|
|
373
402
|
private fun toggleStyle(name: String) {
|
|
@@ -387,7 +416,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
387
416
|
else -> Log.w("EnrichedTextInputView", "Unknown style: $name")
|
|
388
417
|
}
|
|
389
418
|
|
|
390
|
-
layoutManager.invalidateLayout(
|
|
419
|
+
layoutManager.invalidateLayout()
|
|
391
420
|
}
|
|
392
421
|
|
|
393
422
|
private fun removeStyle(name: String, start: Int, end: Int): Boolean {
|
|
@@ -437,7 +466,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
437
466
|
}
|
|
438
467
|
|
|
439
468
|
private fun verifyStyle(name: String): Boolean {
|
|
440
|
-
val mergingConfig = EnrichedSpans.
|
|
469
|
+
val mergingConfig = EnrichedSpans.getMergingConfigForStyle(name, htmlStyle) ?: return true
|
|
441
470
|
val conflictingStyles = mergingConfig.conflictingStyles
|
|
442
471
|
val blockingStyles = mergingConfig.blockingStyles
|
|
443
472
|
val isEnabling = spanState?.getStart(name) == null
|
|
@@ -455,13 +484,13 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
455
484
|
val end = selection?.end ?: 0
|
|
456
485
|
val lengthBefore = text?.length ?: 0
|
|
457
486
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
487
|
+
runAsATransaction {
|
|
488
|
+
val targetRange = getTargetRange(name)
|
|
489
|
+
val removed = removeStyle(style, targetRange.first, targetRange.second)
|
|
490
|
+
if (removed) {
|
|
491
|
+
spanState?.setStart(style, null)
|
|
492
|
+
}
|
|
463
493
|
}
|
|
464
|
-
isDuringTransaction = false
|
|
465
494
|
|
|
466
495
|
val lengthAfter = text?.length ?: 0
|
|
467
496
|
val charactersRemoved = lengthBefore - lengthAfter
|
|
@@ -503,6 +532,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
503
532
|
if (!isValid) return
|
|
504
533
|
|
|
505
534
|
parametrizedStyles?.setImageSpan(src)
|
|
535
|
+
layoutManager.invalidateLayout()
|
|
506
536
|
}
|
|
507
537
|
|
|
508
538
|
fun startMention(indicator: String) {
|
|
@@ -519,6 +549,18 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
519
549
|
parametrizedStyles?.setMentionSpan(text, indicator, attributes)
|
|
520
550
|
}
|
|
521
551
|
|
|
552
|
+
// Sometimes setting up style triggers many changes in sequence
|
|
553
|
+
// Eg. removing conflicting styles -> changing text -> applying spans
|
|
554
|
+
// In such scenario we want to prevent from handling side effects (eg. onTextChanged)
|
|
555
|
+
fun runAsATransaction(block: () -> Unit) {
|
|
556
|
+
try {
|
|
557
|
+
isDuringTransaction = true
|
|
558
|
+
block()
|
|
559
|
+
} finally {
|
|
560
|
+
isDuringTransaction = false
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
522
564
|
override fun onAttachedToWindow() {
|
|
523
565
|
super.onAttachedToWindow()
|
|
524
566
|
|
|
@@ -529,11 +571,6 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
529
571
|
didAttachToWindow = true
|
|
530
572
|
}
|
|
531
573
|
|
|
532
|
-
override fun onDetachedFromWindow() {
|
|
533
|
-
layoutManager.cleanup()
|
|
534
|
-
super.onDetachedFromWindow()
|
|
535
|
-
}
|
|
536
|
-
|
|
537
574
|
companion object {
|
|
538
575
|
const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
|
|
539
576
|
}
|
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
package com.swmansion.enriched
|
|
2
2
|
|
|
3
|
-
import android.text.Editable
|
|
4
|
-
import android.text.StaticLayout
|
|
5
3
|
import com.facebook.react.bridge.Arguments
|
|
6
|
-
import com.facebook.react.uimanager.PixelUtil
|
|
7
4
|
|
|
8
5
|
class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView) {
|
|
9
|
-
private var cachedSize: Pair<Float, Float> = Pair(0f, 0f)
|
|
10
|
-
private var cachedYogaWidth: Float = 0f
|
|
11
6
|
private var forceHeightRecalculationCounter: Int = 0
|
|
12
7
|
|
|
13
|
-
fun
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
fun invalidateLayout() {
|
|
9
|
+
val text = view.text
|
|
10
|
+
val paint = view.paint
|
|
16
11
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
measureSize(text ?: "")
|
|
12
|
+
val needUpdate = MeasurementStore.store(view.id, text, paint)
|
|
13
|
+
if (!needUpdate) return
|
|
20
14
|
|
|
21
15
|
val counter = forceHeightRecalculationCounter
|
|
22
16
|
forceHeightRecalculationCounter++
|
|
@@ -25,40 +19,7 @@ class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView
|
|
|
25
19
|
view.stateWrapper?.updateState(state)
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
fun
|
|
29
|
-
|
|
30
|
-
return cachedSize
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
val text = view.text ?: ""
|
|
34
|
-
val result = measureAndCacheSize(text, maxWidth)
|
|
35
|
-
cachedYogaWidth = maxWidth
|
|
36
|
-
return result
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
fun measureSize(text: CharSequence): Pair<Float, Float> {
|
|
40
|
-
return measureAndCacheSize(text, cachedYogaWidth)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
private fun measureAndCacheSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
|
|
44
|
-
val result = measureSize(text, maxWidth)
|
|
45
|
-
cachedSize = result
|
|
46
|
-
return result
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
private fun measureSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
|
|
50
|
-
val paint = view.paint
|
|
51
|
-
val textLength = text.length
|
|
52
|
-
|
|
53
|
-
val staticLayout = StaticLayout.Builder
|
|
54
|
-
.obtain(text, 0, textLength, paint, maxWidth.toInt())
|
|
55
|
-
.setIncludePad(true)
|
|
56
|
-
.setLineSpacing(0f, 1f)
|
|
57
|
-
.build()
|
|
58
|
-
|
|
59
|
-
val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
|
|
60
|
-
val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
|
|
61
|
-
|
|
62
|
-
return Pair(widthInSP, heightInSP)
|
|
22
|
+
fun releaseMeasurementStore() {
|
|
23
|
+
MeasurementStore.release(view.id)
|
|
63
24
|
}
|
|
64
25
|
}
|
|
@@ -15,7 +15,6 @@ import com.facebook.react.uimanager.annotations.ReactProp
|
|
|
15
15
|
import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerDelegate
|
|
16
16
|
import com.facebook.react.viewmanagers.EnrichedTextInputViewManagerInterface
|
|
17
17
|
import com.facebook.yoga.YogaMeasureMode
|
|
18
|
-
import com.facebook.yoga.YogaMeasureOutput
|
|
19
18
|
import com.swmansion.enriched.events.OnInputBlurEvent
|
|
20
19
|
import com.swmansion.enriched.events.OnChangeHtmlEvent
|
|
21
20
|
import com.swmansion.enriched.events.OnChangeSelectionEvent
|
|
@@ -32,12 +31,8 @@ import com.swmansion.enriched.utils.jsonStringToStringMap
|
|
|
32
31
|
@ReactModule(name = EnrichedTextInputViewManager.NAME)
|
|
33
32
|
class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
34
33
|
EnrichedTextInputViewManagerInterface<EnrichedTextInputView> {
|
|
35
|
-
private val mDelegate: ViewManagerDelegate<EnrichedTextInputView>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
init {
|
|
39
|
-
mDelegate = EnrichedTextInputViewManagerDelegate(this)
|
|
40
|
-
}
|
|
34
|
+
private val mDelegate: ViewManagerDelegate<EnrichedTextInputView> =
|
|
35
|
+
EnrichedTextInputViewManagerDelegate(this)
|
|
41
36
|
|
|
42
37
|
override fun getDelegate(): ViewManagerDelegate<EnrichedTextInputView>? {
|
|
43
38
|
return mDelegate
|
|
@@ -48,10 +43,12 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
public override fun createViewInstance(context: ThemedReactContext): EnrichedTextInputView {
|
|
51
|
-
|
|
52
|
-
|
|
46
|
+
return EnrichedTextInputView(context)
|
|
47
|
+
}
|
|
53
48
|
|
|
54
|
-
|
|
49
|
+
override fun onDropViewInstance(view: EnrichedTextInputView) {
|
|
50
|
+
super.onDropViewInstance(view)
|
|
51
|
+
view.layoutManager.releaseMeasurementStore()
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
override fun updateState(
|
|
@@ -80,7 +77,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
80
77
|
|
|
81
78
|
@ReactProp(name = "defaultValue")
|
|
82
79
|
override fun setDefaultValue(view: EnrichedTextInputView?, value: String?) {
|
|
83
|
-
view?.
|
|
80
|
+
view?.setDefaultValue(value)
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
@ReactProp(name = "placeholder")
|
|
@@ -157,9 +154,14 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
157
154
|
view?.setFontStyle(style)
|
|
158
155
|
}
|
|
159
156
|
|
|
157
|
+
@ReactProp(name = "scrollEnabled")
|
|
158
|
+
override fun setScrollEnabled(view: EnrichedTextInputView, scrollEnabled: Boolean) {
|
|
159
|
+
view.scrollEnabled = scrollEnabled
|
|
160
|
+
}
|
|
161
|
+
|
|
160
162
|
override fun onAfterUpdateTransaction(view: EnrichedTextInputView) {
|
|
161
163
|
super.onAfterUpdateTransaction(view)
|
|
162
|
-
view.
|
|
164
|
+
view.afterUpdateTransaction()
|
|
163
165
|
}
|
|
164
166
|
|
|
165
167
|
override fun setPadding(
|
|
@@ -277,13 +279,8 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
|
|
|
277
279
|
heightMode: YogaMeasureMode?,
|
|
278
280
|
attachmentsPositions: FloatArray?
|
|
279
281
|
): Long {
|
|
280
|
-
val
|
|
281
|
-
|
|
282
|
-
if (size != null) {
|
|
283
|
-
return YogaMeasureOutput.make(size.first, size.second)
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return YogaMeasureOutput.make(0, 0)
|
|
282
|
+
val id = localData?.getInt("viewTag")
|
|
283
|
+
return MeasurementStore.getMeasureById(context, id, width, props)
|
|
287
284
|
}
|
|
288
285
|
|
|
289
286
|
companion object {
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
package com.swmansion.enriched
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Typeface
|
|
5
|
+
import android.graphics.text.LineBreaker
|
|
6
|
+
import android.os.Build
|
|
7
|
+
import android.text.Spannable
|
|
8
|
+
import android.text.StaticLayout
|
|
9
|
+
import android.text.TextPaint
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import com.facebook.react.bridge.ReadableMap
|
|
12
|
+
import com.facebook.react.uimanager.PixelUtil
|
|
13
|
+
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
|
|
14
|
+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
|
|
15
|
+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
|
|
16
|
+
import com.facebook.yoga.YogaMeasureOutput
|
|
17
|
+
import com.swmansion.enriched.styles.HtmlStyle
|
|
18
|
+
import com.swmansion.enriched.utils.EnrichedParser
|
|
19
|
+
import java.util.concurrent.ConcurrentHashMap
|
|
20
|
+
import kotlin.math.ceil
|
|
21
|
+
|
|
22
|
+
object MeasurementStore {
|
|
23
|
+
data class PaintParams(
|
|
24
|
+
val typeface: Typeface,
|
|
25
|
+
val fontSize: Float,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
data class MeasurementParams(
|
|
29
|
+
val initialized: Boolean,
|
|
30
|
+
|
|
31
|
+
val cachedWidth: Float,
|
|
32
|
+
val cachedSize: Long,
|
|
33
|
+
|
|
34
|
+
val spannable: CharSequence?,
|
|
35
|
+
val paintParams: PaintParams,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
private val data = ConcurrentHashMap<Int, MeasurementParams>()
|
|
39
|
+
|
|
40
|
+
fun store(id: Int, spannable: Spannable?, paint: TextPaint): Boolean {
|
|
41
|
+
val cachedWidth = data[id]?.cachedWidth ?: 0f
|
|
42
|
+
val cachedSize = data[id]?.cachedSize ?: 0L
|
|
43
|
+
val initialized = data[id]?.initialized ?: true
|
|
44
|
+
|
|
45
|
+
val size = measure(cachedWidth, spannable, paint)
|
|
46
|
+
val paintParams = PaintParams(paint.typeface, paint.textSize)
|
|
47
|
+
|
|
48
|
+
data[id] = MeasurementParams(initialized, cachedWidth, size, spannable, paintParams)
|
|
49
|
+
return cachedSize != size
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fun release(id: Int) {
|
|
53
|
+
data.remove(id)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun measure(maxWidth: Float, spannable: CharSequence?, paintParams: PaintParams): Long {
|
|
57
|
+
val paint = TextPaint().apply {
|
|
58
|
+
typeface = paintParams.typeface
|
|
59
|
+
textSize = paintParams.fontSize
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return measure(maxWidth, spannable, paint)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private fun measure(maxWidth: Float, spannable: CharSequence?, paint: TextPaint): Long {
|
|
66
|
+
val text = spannable ?: ""
|
|
67
|
+
val textLength = text.length
|
|
68
|
+
val builder = StaticLayout.Builder
|
|
69
|
+
.obtain(text, 0, textLength, paint, maxWidth.toInt())
|
|
70
|
+
.setIncludePad(true)
|
|
71
|
+
.setLineSpacing(0f, 1f)
|
|
72
|
+
|
|
73
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
74
|
+
builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
78
|
+
builder.setUseLineSpacingFromFallbacks(true)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
val staticLayout = builder.build()
|
|
82
|
+
val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
|
|
83
|
+
val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
|
|
84
|
+
return YogaMeasureOutput.make(widthInSP, heightInSP)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Returns either: Spannable parsed from HTML defaultValue, or plain text defaultValue, or "I" if no defaultValue
|
|
88
|
+
private fun getInitialText(defaultView: EnrichedTextInputView, props: ReadableMap?): CharSequence {
|
|
89
|
+
val defaultValue = props?.getString("defaultValue")
|
|
90
|
+
|
|
91
|
+
// If there is no default value, assume text is one line, "I" is a good approximation of height
|
|
92
|
+
if (defaultValue == null) return "I"
|
|
93
|
+
|
|
94
|
+
val isHtml = defaultValue.startsWith("<html>") && defaultValue.endsWith("</html>")
|
|
95
|
+
if (!isHtml) return defaultValue
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
val htmlStyle = HtmlStyle(defaultView, props.getMap("htmlStyle"))
|
|
99
|
+
val parsed = EnrichedParser.fromHtml(defaultValue, htmlStyle, null)
|
|
100
|
+
return parsed.trimEnd('\n')
|
|
101
|
+
} catch (e: Exception) {
|
|
102
|
+
Log.w("MeasurementStore", "Error parsing initial HTML text: ${e.message}")
|
|
103
|
+
return defaultValue
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private fun getInitialFontSize(defaultView: EnrichedTextInputView, props: ReadableMap?): Float {
|
|
108
|
+
val propsFontSize = props?.getDouble("fontSize")?.toFloat()
|
|
109
|
+
if (propsFontSize == null) return defaultView.textSize
|
|
110
|
+
|
|
111
|
+
return ceil(PixelUtil.toPixelFromSP(propsFontSize))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Called when view measurements are not available in the store
|
|
115
|
+
// Most likely first measurement, we can use defaultValue, as no native state is set yet
|
|
116
|
+
private fun initialMeasure(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
|
|
117
|
+
val defaultView = EnrichedTextInputView(context)
|
|
118
|
+
|
|
119
|
+
val text = getInitialText(defaultView, props)
|
|
120
|
+
val fontSize = getInitialFontSize(defaultView, props)
|
|
121
|
+
|
|
122
|
+
val fontFamily = props?.getString("fontFamily")
|
|
123
|
+
val fontStyle = parseFontStyle(props?.getString("fontStyle"))
|
|
124
|
+
val fontWeight = parseFontWeight(props?.getString("fontWeight"))
|
|
125
|
+
|
|
126
|
+
val typeface = applyStyles(defaultView.typeface, fontStyle, fontWeight, fontFamily, context.assets)
|
|
127
|
+
val paintParams = PaintParams(typeface, fontSize)
|
|
128
|
+
val size = measure(width, text, PaintParams(typeface, fontSize))
|
|
129
|
+
|
|
130
|
+
if (id != null) {
|
|
131
|
+
data[id] = MeasurementParams(true, width, size, text, paintParams)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return size
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fun getMeasureById(context: Context, id: Int?, width: Float, props: ReadableMap?): Long {
|
|
138
|
+
val id = id ?: return initialMeasure(context, id, width, props)
|
|
139
|
+
val value = data[id] ?: return initialMeasure(context, id, width, props)
|
|
140
|
+
|
|
141
|
+
// First measure has to be done using initialMeasure
|
|
142
|
+
// That way it's free of any side effects and async initializations
|
|
143
|
+
if (!value.initialized) return initialMeasure(context, id, width, props)
|
|
144
|
+
|
|
145
|
+
if (width == value.cachedWidth) {
|
|
146
|
+
return value.cachedSize
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
val paint = TextPaint().apply {
|
|
150
|
+
typeface = value.paintParams.typeface
|
|
151
|
+
textSize = value.paintParams.fontSize
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
val size = measure(width, value.spannable, paint)
|
|
155
|
+
data[id] = MeasurementParams(true, width, size, value.spannable, value.paintParams)
|
|
156
|
+
return size
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -4,13 +4,17 @@ import android.graphics.Canvas
|
|
|
4
4
|
import android.graphics.Paint
|
|
5
5
|
import android.text.Layout
|
|
6
6
|
import android.text.TextPaint
|
|
7
|
-
import android.text.style.CharacterStyle
|
|
8
7
|
import android.text.style.LeadingMarginSpan
|
|
8
|
+
import android.text.style.MetricAffectingSpan
|
|
9
9
|
import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
|
|
10
10
|
import com.swmansion.enriched.styles.HtmlStyle
|
|
11
11
|
|
|
12
12
|
// https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/core/java/android/text/style/QuoteSpan.java
|
|
13
|
-
class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) :
|
|
13
|
+
class EnrichedBlockQuoteSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedBlockSpan {
|
|
14
|
+
override fun updateMeasureState(p0: TextPaint) {
|
|
15
|
+
// Do nothing, but inform layout that this span affects text metrics
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
override fun getLeadingMargin(p0: Boolean): Int {
|
|
15
19
|
return htmlStyle.blockquoteStripeWidth + htmlStyle.blockquoteGapWidth
|
|
16
20
|
}
|
|
@@ -5,15 +5,19 @@ import android.graphics.Paint
|
|
|
5
5
|
import android.graphics.RectF
|
|
6
6
|
import android.graphics.Typeface
|
|
7
7
|
import android.text.TextPaint
|
|
8
|
-
import android.text.style.CharacterStyle
|
|
9
8
|
import android.text.style.LineBackgroundSpan
|
|
9
|
+
import android.text.style.MetricAffectingSpan
|
|
10
10
|
import com.swmansion.enriched.spans.interfaces.EnrichedBlockSpan
|
|
11
11
|
import com.swmansion.enriched.styles.HtmlStyle
|
|
12
12
|
|
|
13
|
-
class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) :
|
|
14
|
-
override fun updateDrawState(paint: TextPaint
|
|
15
|
-
paint
|
|
16
|
-
paint
|
|
13
|
+
class EnrichedCodeBlockSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LineBackgroundSpan, EnrichedBlockSpan {
|
|
14
|
+
override fun updateDrawState(paint: TextPaint) {
|
|
15
|
+
paint.typeface = Typeface.MONOSPACE
|
|
16
|
+
paint.color = htmlStyle.codeBlockColor
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
override fun updateMeasureState(paint: TextPaint) {
|
|
20
|
+
paint.typeface = Typeface.MONOSPACE
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
override fun drawBackground(
|
|
@@ -2,15 +2,20 @@ package com.swmansion.enriched.spans
|
|
|
2
2
|
|
|
3
3
|
import android.graphics.Typeface
|
|
4
4
|
import android.text.TextPaint
|
|
5
|
-
import android.text.style.
|
|
5
|
+
import android.text.style.MetricAffectingSpan
|
|
6
6
|
import com.swmansion.enriched.spans.interfaces.EnrichedInlineSpan
|
|
7
7
|
import com.swmansion.enriched.styles.HtmlStyle
|
|
8
8
|
|
|
9
|
-
class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) :
|
|
9
|
+
class EnrichedInlineCodeSpan(private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), EnrichedInlineSpan {
|
|
10
10
|
override fun updateDrawState(textPaint: TextPaint) {
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
|
|
12
|
+
textPaint.typeface = typeface
|
|
13
13
|
textPaint.color = htmlStyle.inlineCodeColor
|
|
14
|
-
textPaint.
|
|
14
|
+
textPaint.bgColor = htmlStyle.inlineCodeBackgroundColor
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
override fun updateMeasureState(textPaint: TextPaint) {
|
|
18
|
+
val typeface = Typeface.create(Typeface.MONOSPACE, Typeface.NORMAL)
|
|
19
|
+
textPaint.typeface = typeface
|
|
15
20
|
}
|
|
16
21
|
}
|
|
@@ -4,11 +4,21 @@ import android.graphics.Canvas
|
|
|
4
4
|
import android.graphics.Paint
|
|
5
5
|
import android.graphics.Typeface
|
|
6
6
|
import android.text.Layout
|
|
7
|
+
import android.text.TextPaint
|
|
7
8
|
import android.text.style.LeadingMarginSpan
|
|
9
|
+
import android.text.style.MetricAffectingSpan
|
|
8
10
|
import com.swmansion.enriched.spans.interfaces.EnrichedParagraphSpan
|
|
9
11
|
import com.swmansion.enriched.styles.HtmlStyle
|
|
10
12
|
|
|
11
|
-
class EnrichedOrderedListSpan(private var index: Int, private val htmlStyle: HtmlStyle) : LeadingMarginSpan, EnrichedParagraphSpan {
|
|
13
|
+
class EnrichedOrderedListSpan(private var index: Int, private val htmlStyle: HtmlStyle) : MetricAffectingSpan(), LeadingMarginSpan, EnrichedParagraphSpan {
|
|
14
|
+
override fun updateMeasureState(p0: TextPaint) {
|
|
15
|
+
// Do nothing, but inform layout that this span affects text metrics
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
override fun updateDrawState(p0: TextPaint?) {
|
|
19
|
+
// Do nothing, but inform layout that this span affects text metrics
|
|
20
|
+
}
|
|
21
|
+
|
|
12
22
|
override fun getLeadingMargin(first: Boolean): Int {
|
|
13
23
|
return htmlStyle.olMarginLeft + htmlStyle.olGapWidth
|
|
14
24
|
}
|