react-native-enriched 0.1.5 → 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 +34 -10
- package/android/src/main/java/com/swmansion/enriched/EnrichedTextInputViewLayoutManager.kt +7 -56
- 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/EnrichedSpans.kt +61 -46
- package/android/src/main/java/com/swmansion/enriched/styles/ParametrizedStyles.kt +18 -2
- 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 +71 -40
- 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 +1 -1
- 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{};
|
|
@@ -54,6 +54,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
54
54
|
val parametrizedStyles: ParametrizedStyles? = ParametrizedStyles(this)
|
|
55
55
|
var isDuringTransaction: Boolean = false
|
|
56
56
|
var isRemovingMany: Boolean = false
|
|
57
|
+
var scrollEnabled: Boolean = true
|
|
57
58
|
|
|
58
59
|
val mentionHandler: MentionHandler? = MentionHandler(this)
|
|
59
60
|
var htmlStyle: HtmlStyle = HtmlStyle(this, null)
|
|
@@ -70,6 +71,8 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
70
71
|
private var fontFamily: String? = null
|
|
71
72
|
private var fontStyle: Int = ReactConstants.UNSET
|
|
72
73
|
private var fontWeight: Int = ReactConstants.UNSET
|
|
74
|
+
private var defaultValue: CharSequence? = null
|
|
75
|
+
private var defaultValueDirty: Boolean = false
|
|
73
76
|
|
|
74
77
|
private var inputMethodManager: InputMethodManager? = null
|
|
75
78
|
|
|
@@ -137,6 +140,14 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
137
140
|
return super.onTouchEvent(ev)
|
|
138
141
|
}
|
|
139
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
|
+
|
|
140
151
|
override fun onSelectionChanged(selStart: Int, selEnd: Int) {
|
|
141
152
|
super.onSelectionChanged(selStart, selEnd)
|
|
142
153
|
selection?.onSelection(selStart, selEnd)
|
|
@@ -306,7 +317,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
306
317
|
|
|
307
318
|
// This ensured that newly created spans will take the new font size into account
|
|
308
319
|
htmlStyle.invalidateStyles()
|
|
309
|
-
layoutManager.invalidateLayout(
|
|
320
|
+
layoutManager.invalidateLayout()
|
|
310
321
|
}
|
|
311
322
|
|
|
312
323
|
fun setFontFamily(family: String?) {
|
|
@@ -360,7 +371,24 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
360
371
|
return false
|
|
361
372
|
}
|
|
362
373
|
|
|
363
|
-
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() {
|
|
364
392
|
if (!typefaceDirty) return
|
|
365
393
|
typefaceDirty = false
|
|
366
394
|
|
|
@@ -368,7 +396,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
368
396
|
typeface = newTypeface
|
|
369
397
|
paint.typeface = newTypeface
|
|
370
398
|
|
|
371
|
-
layoutManager.invalidateLayout(
|
|
399
|
+
layoutManager.invalidateLayout()
|
|
372
400
|
}
|
|
373
401
|
|
|
374
402
|
private fun toggleStyle(name: String) {
|
|
@@ -388,7 +416,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
388
416
|
else -> Log.w("EnrichedTextInputView", "Unknown style: $name")
|
|
389
417
|
}
|
|
390
418
|
|
|
391
|
-
layoutManager.invalidateLayout(
|
|
419
|
+
layoutManager.invalidateLayout()
|
|
392
420
|
}
|
|
393
421
|
|
|
394
422
|
private fun removeStyle(name: String, start: Int, end: Int): Boolean {
|
|
@@ -438,7 +466,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
438
466
|
}
|
|
439
467
|
|
|
440
468
|
private fun verifyStyle(name: String): Boolean {
|
|
441
|
-
val mergingConfig = EnrichedSpans.
|
|
469
|
+
val mergingConfig = EnrichedSpans.getMergingConfigForStyle(name, htmlStyle) ?: return true
|
|
442
470
|
val conflictingStyles = mergingConfig.conflictingStyles
|
|
443
471
|
val blockingStyles = mergingConfig.blockingStyles
|
|
444
472
|
val isEnabling = spanState?.getStart(name) == null
|
|
@@ -504,6 +532,7 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
504
532
|
if (!isValid) return
|
|
505
533
|
|
|
506
534
|
parametrizedStyles?.setImageSpan(src)
|
|
535
|
+
layoutManager.invalidateLayout()
|
|
507
536
|
}
|
|
508
537
|
|
|
509
538
|
fun startMention(indicator: String) {
|
|
@@ -542,11 +571,6 @@ class EnrichedTextInputView : AppCompatEditText {
|
|
|
542
571
|
didAttachToWindow = true
|
|
543
572
|
}
|
|
544
573
|
|
|
545
|
-
override fun onDetachedFromWindow() {
|
|
546
|
-
layoutManager.cleanup()
|
|
547
|
-
super.onDetachedFromWindow()
|
|
548
|
-
}
|
|
549
|
-
|
|
550
574
|
companion object {
|
|
551
575
|
const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
|
|
552
576
|
}
|
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
package com.swmansion.enriched
|
|
2
2
|
|
|
3
|
-
import android.graphics.text.LineBreaker
|
|
4
|
-
import android.os.Build
|
|
5
|
-
import android.text.Editable
|
|
6
|
-
import android.text.StaticLayout
|
|
7
3
|
import com.facebook.react.bridge.Arguments
|
|
8
|
-
import com.facebook.react.uimanager.PixelUtil
|
|
9
4
|
|
|
10
5
|
class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView) {
|
|
11
|
-
private var cachedSize: Pair<Float, Float> = Pair(0f, 0f)
|
|
12
|
-
private var cachedYogaWidth: Float = 0f
|
|
13
6
|
private var forceHeightRecalculationCounter: Int = 0
|
|
14
7
|
|
|
15
|
-
fun
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
fun invalidateLayout() {
|
|
9
|
+
val text = view.text
|
|
10
|
+
val paint = view.paint
|
|
18
11
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
measureSize(text ?: "")
|
|
12
|
+
val needUpdate = MeasurementStore.store(view.id, text, paint)
|
|
13
|
+
if (!needUpdate) return
|
|
22
14
|
|
|
23
15
|
val counter = forceHeightRecalculationCounter
|
|
24
16
|
forceHeightRecalculationCounter++
|
|
@@ -27,48 +19,7 @@ class EnrichedTextInputViewLayoutManager(private val view: EnrichedTextInputView
|
|
|
27
19
|
view.stateWrapper?.updateState(state)
|
|
28
20
|
}
|
|
29
21
|
|
|
30
|
-
fun
|
|
31
|
-
|
|
32
|
-
return cachedSize
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
val text = view.text ?: ""
|
|
36
|
-
val result = measureAndCacheSize(text, maxWidth)
|
|
37
|
-
cachedYogaWidth = maxWidth
|
|
38
|
-
return result
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
fun measureSize(text: CharSequence): Pair<Float, Float> {
|
|
42
|
-
return measureAndCacheSize(text, cachedYogaWidth)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
private fun measureAndCacheSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
|
|
46
|
-
val result = measureSize(text, maxWidth)
|
|
47
|
-
cachedSize = result
|
|
48
|
-
return result
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
private fun measureSize(text: CharSequence, maxWidth: Float): Pair<Float, Float> {
|
|
52
|
-
val paint = view.paint
|
|
53
|
-
val textLength = text.length
|
|
54
|
-
|
|
55
|
-
val builder = StaticLayout.Builder
|
|
56
|
-
.obtain(text, 0, textLength, paint, maxWidth.toInt())
|
|
57
|
-
.setIncludePad(true)
|
|
58
|
-
.setLineSpacing(0f, 1f)
|
|
59
|
-
|
|
60
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
61
|
-
builder.setBreakStrategy(LineBreaker.BREAK_STRATEGY_HIGH_QUALITY)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
65
|
-
builder.setUseLineSpacingFromFallbacks(true)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
val staticLayout = builder.build()
|
|
69
|
-
val heightInSP = PixelUtil.toDIPFromPixel(staticLayout.height.toFloat())
|
|
70
|
-
val widthInSP = PixelUtil.toDIPFromPixel(maxWidth)
|
|
71
|
-
|
|
72
|
-
return Pair(widthInSP, heightInSP)
|
|
22
|
+
fun releaseMeasurementStore() {
|
|
23
|
+
MeasurementStore.release(view.id)
|
|
73
24
|
}
|
|
74
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
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
package com.swmansion.enriched.spans
|
|
2
2
|
|
|
3
|
+
import com.swmansion.enriched.styles.HtmlStyle
|
|
4
|
+
|
|
3
5
|
data class BaseSpanConfig(val clazz: Class<*>)
|
|
4
6
|
data class ParagraphSpanConfig(val clazz: Class<*>, val isContinuous: Boolean)
|
|
5
7
|
data class ListSpanConfig(val clazz: Class<*>, val shortcut: String)
|
|
@@ -62,50 +64,63 @@ object EnrichedSpans {
|
|
|
62
64
|
MENTION to BaseSpanConfig(EnrichedMentionSpan::class.java),
|
|
63
65
|
)
|
|
64
66
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
67
|
+
fun getMergingConfigForStyle(style: String, htmlStyle: HtmlStyle): StylesMergingConfig? {
|
|
68
|
+
return when (style) {
|
|
69
|
+
BOLD -> {
|
|
70
|
+
val blockingStyles = mutableListOf(CODE_BLOCK)
|
|
71
|
+
if (htmlStyle.h1Bold) blockingStyles.add(H1)
|
|
72
|
+
if (htmlStyle.h2Bold) blockingStyles.add(H2)
|
|
73
|
+
if (htmlStyle.h3Bold) blockingStyles.add(H3)
|
|
74
|
+
StylesMergingConfig(blockingStyles = blockingStyles.toTypedArray())
|
|
75
|
+
}
|
|
76
|
+
ITALIC -> StylesMergingConfig(
|
|
77
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
78
|
+
)
|
|
79
|
+
UNDERLINE -> StylesMergingConfig(
|
|
80
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
81
|
+
)
|
|
82
|
+
STRIKETHROUGH -> StylesMergingConfig(
|
|
83
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
84
|
+
)
|
|
85
|
+
INLINE_CODE -> StylesMergingConfig(
|
|
86
|
+
conflictingStyles = arrayOf(MENTION, LINK),
|
|
87
|
+
blockingStyles = arrayOf(CODE_BLOCK)
|
|
88
|
+
)
|
|
89
|
+
H1 -> {
|
|
90
|
+
val conflictingStyles = mutableListOf(H2, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
|
|
91
|
+
if (htmlStyle.h1Bold) conflictingStyles.add(BOLD)
|
|
92
|
+
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
|
|
93
|
+
}
|
|
94
|
+
H2 -> {
|
|
95
|
+
val conflictingStyles = mutableListOf(H1, H3, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
|
|
96
|
+
if (htmlStyle.h2Bold) conflictingStyles.add(BOLD)
|
|
97
|
+
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
|
|
98
|
+
}
|
|
99
|
+
H3 -> {
|
|
100
|
+
val conflictingStyles = mutableListOf(H1, H2, ORDERED_LIST, UNORDERED_LIST, BLOCK_QUOTE, CODE_BLOCK)
|
|
101
|
+
if (htmlStyle.h3Bold) conflictingStyles.add(BOLD)
|
|
102
|
+
StylesMergingConfig(conflictingStyles = conflictingStyles.toTypedArray())
|
|
103
|
+
}
|
|
104
|
+
BLOCK_QUOTE -> StylesMergingConfig(
|
|
105
|
+
conflictingStyles = arrayOf(H1, H2, H3, CODE_BLOCK, ORDERED_LIST, UNORDERED_LIST)
|
|
106
|
+
)
|
|
107
|
+
CODE_BLOCK -> StylesMergingConfig(
|
|
108
|
+
conflictingStyles = arrayOf(H1, H2, H3, BOLD, ITALIC, UNDERLINE, STRIKETHROUGH, UNORDERED_LIST, ORDERED_LIST, BLOCK_QUOTE, INLINE_CODE)
|
|
109
|
+
)
|
|
110
|
+
UNORDERED_LIST -> StylesMergingConfig(
|
|
111
|
+
conflictingStyles = arrayOf(H1, H2, H3, ORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE)
|
|
112
|
+
)
|
|
113
|
+
ORDERED_LIST -> StylesMergingConfig(
|
|
114
|
+
conflictingStyles = arrayOf(H1, H2, H3, UNORDERED_LIST, CODE_BLOCK, BLOCK_QUOTE),
|
|
115
|
+
)
|
|
116
|
+
LINK -> StylesMergingConfig(
|
|
117
|
+
blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, MENTION)
|
|
118
|
+
)
|
|
119
|
+
IMAGE -> StylesMergingConfig()
|
|
120
|
+
MENTION -> StylesMergingConfig(
|
|
121
|
+
blockingStyles = arrayOf(INLINE_CODE, CODE_BLOCK, LINK)
|
|
122
|
+
)
|
|
123
|
+
else -> null
|
|
124
|
+
}
|
|
125
|
+
}
|
|
111
126
|
}
|
|
@@ -104,15 +104,31 @@ class ParametrizedStyles(private val view: EnrichedTextInputView) {
|
|
|
104
104
|
return Triple(result, start, end)
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
private fun canLinkBeApplied(): Boolean {
|
|
108
|
+
val mergingConfig = EnrichedSpans.getMergingConfigForStyle(EnrichedSpans.LINK, view.htmlStyle)?: return true
|
|
109
|
+
val conflictingStyles = mergingConfig.conflictingStyles
|
|
110
|
+
val blockingStyles = mergingConfig.blockingStyles
|
|
111
|
+
|
|
112
|
+
for (style in blockingStyles) {
|
|
113
|
+
if (view.spanState?.getStart(style) != null) return false
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (style in conflictingStyles) {
|
|
117
|
+
if (view.spanState?.getStart(style) != null) return false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return true
|
|
121
|
+
}
|
|
122
|
+
|
|
107
123
|
private fun afterTextChangedLinks(result: Triple<String, Int, Int>) {
|
|
108
124
|
// Do not detect link if it's applied manually
|
|
109
|
-
if (isSettingLinkSpan) return
|
|
125
|
+
if (isSettingLinkSpan || !canLinkBeApplied()) return
|
|
126
|
+
|
|
110
127
|
val spannable = view.text as Spannable
|
|
111
128
|
val (word, start, end) = result
|
|
112
129
|
|
|
113
130
|
// TODO: Consider using more reliable regex, this one matches almost anything
|
|
114
131
|
val urlPattern = android.util.Patterns.WEB_URL.matcher(word)
|
|
115
|
-
|
|
116
132
|
val spans = spannable.getSpans(start, end, EnrichedLinkSpan::class.java)
|
|
117
133
|
for (span in spans) {
|
|
118
134
|
spannable.removeSpan(span)
|