react-native-enriched-markdown-scaffold 1.0.2-scaffold.1 → 1.0.2-scaffold.10
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/build.gradle +2 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerDelegate.java +18 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownManagerInterface.java +4 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +18 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +4 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +30 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +210 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdown.kt +176 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownInternalText.kt +45 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownManager.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +112 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +34 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +46 -8
- package/android/src/main/java/com/swmansion/enriched/markdown/events/CopyRequestEvent.kt +28 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLayoutEvent.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/SelectionChangeEvent.kt +43 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +25 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/input/editing/InputConnectionWrapper.kt +7 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +37 -7
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +5 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +13 -99
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/InlineChipPainter.kt +140 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkChipSpans.kt +177 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +1 -6
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineMathStyle.kt +16 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +36 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkVariantEntry.kt +7 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/MathStyle.kt +3 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/common/MarkdownViewManagerUtils.kt +51 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +34 -2
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/HTMLGenerator.kt +2 -1
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/conversion/MarkdownExtractor.kt +6 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/HeadlessActionMode.kt +88 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/LinkLayoutCollector.kt +100 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionActionMode.kt +53 -14
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/text/view/SelectionChangeReporter.kt +232 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/BlockMenuOptions.kt +64 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/views/CodeBlockContainerView.kt +29 -3
- package/android/src/main/java/com/swmansion/enriched/markdown/views/TableContainerView.kt +30 -7
- package/android/src/math/java/com/swmansion/enriched/markdown/renderer/MathInlineRenderer.kt +1 -0
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathInlineSpan.kt +13 -6
- package/android/src/math/java/com/swmansion/enriched/markdown/spans/MathMeasureHelper.kt +2 -1
- package/android/src/math/java/com/swmansion/enriched/markdown/views/MathContainerView.kt +25 -9
- package/docs/API_REFERENCE.md +183 -4
- package/docs/COPY_OPTIONS.md +40 -0
- package/docs/INPUT.md +3 -1
- package/docs/MENTIONS.md +8 -1
- package/docs/STYLES.md +27 -0
- package/docs/TEXT.md +2 -0
- package/docs/WEB.md +1 -0
- package/ios/EnrichedMarkdown.mm +321 -9
- package/ios/EnrichedMarkdownText.mm +203 -6
- package/ios/attachments/ENRMMathInlineAttachment+macOS.m +2 -1
- package/ios/attachments/ENRMMathInlineAttachment.h +3 -0
- package/ios/attachments/ENRMMathInlineAttachment.m +2 -1
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +148 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +120 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +30 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +210 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +50 -2
- package/ios/internals/MeasurementCache.h +8 -2
- package/ios/math/ENRMRaTeXBridge.swift +23 -5
- package/ios/renderer/ENRMMathInlineRenderer.m +1 -0
- package/ios/renderer/LinkRenderer.m +47 -15
- package/ios/styles/StyleConfig.h +20 -0
- package/ios/styles/StyleConfig.mm +84 -0
- package/ios/utils/CodeBackground.m +3 -65
- package/ios/utils/ENRMBlockMenu.h +66 -0
- package/ios/utils/ENRMChipDrawing.h +24 -0
- package/ios/utils/ENRMChipDrawing.m +83 -0
- package/ios/utils/ENRMLinkChip.h +70 -0
- package/ios/utils/ENRMLinkChip.m +213 -0
- package/ios/utils/ENRMLinkLayout.h +50 -0
- package/ios/utils/ENRMLinkLayout.m +45 -0
- package/ios/utils/ENRMSelectionChange.h +120 -0
- package/ios/utils/ENRMSelectionChange.m +282 -0
- package/ios/utils/EditMenuUtils+macOS.m +4 -1
- package/ios/utils/EditMenuUtils.h +35 -6
- package/ios/utils/EditMenuUtils.m +171 -15
- package/ios/utils/HTMLGenerator.m +2 -1
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +3 -1
- package/ios/utils/MarkdownExtractor.m +2 -1
- package/ios/utils/RTFExportUtils.m +2 -0
- package/ios/utils/StylePropsUtils.h +47 -1
- package/ios/utils/TextViewLayoutManager.mm +3 -0
- package/ios/views/ENRMCodeBlockContainerView.h +16 -2
- package/ios/views/ENRMCodeBlockContainerView.m +22 -19
- package/ios/views/ENRMMathContainerView.h +11 -0
- package/ios/views/ENRMMathContainerView.m +17 -20
- package/ios/views/TableContainerView.h +14 -0
- package/ios/views/TableContainerView.m +34 -18
- package/lib/module/EnrichedMarkdownNativeComponent.ts +138 -1
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +138 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/jest/index.js +14 -0
- package/lib/module/jest/index.js.map +1 -1
- package/lib/module/native/EnrichedMarkdownText.js +169 -11
- package/lib/module/native/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.js +17 -4
- package/lib/module/normalizeMarkdownStyle.js.map +1 -1
- package/lib/module/normalizeMarkdownStyle.web.js +17 -4
- package/lib/module/normalizeMarkdownStyle.web.js.map +1 -1
- package/lib/module/web/EnrichedMarkdownText.js +6 -3
- package/lib/module/web/EnrichedMarkdownText.js.map +1 -1
- package/lib/module/web/renderers/InlineRenderers.js +96 -3
- package/lib/module/web/renderers/InlineRenderers.js.map +1 -1
- package/lib/module/web/styles.js +18 -0
- package/lib/module/web/styles.js.map +1 -1
- package/lib/module/web/utils.js +13 -0
- package/lib/module/web/utils.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts +123 -2
- package/lib/typescript/src/EnrichedMarkdownNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +123 -2
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +2 -2
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/index.web.d.ts +1 -1
- package/lib/typescript/src/index.web.d.ts.map +1 -1
- package/lib/typescript/src/jest/index.d.ts +1 -1
- package/lib/typescript/src/jest/index.d.ts.map +1 -1
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts +5 -5
- package/lib/typescript/src/native/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/normalizeMarkdownStyle.web.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyle.d.ts +50 -5
- package/lib/typescript/src/types/MarkdownStyle.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts +12 -0
- package/lib/typescript/src/types/MarkdownStyleInternal.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.d.ts +144 -11
- package/lib/typescript/src/types/MarkdownTextProps.d.ts.map +1 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts +41 -1
- package/lib/typescript/src/types/MarkdownTextProps.web.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +66 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts +1 -1
- package/lib/typescript/src/web/EnrichedMarkdownText.d.ts.map +1 -1
- package/lib/typescript/src/web/renderers/InlineRenderers.d.ts.map +1 -1
- package/lib/typescript/src/web/styles.d.ts.map +1 -1
- package/lib/typescript/src/web/types.d.ts +4 -0
- package/lib/typescript/src/web/types.d.ts.map +1 -1
- package/lib/typescript/src/web/utils.d.ts +4 -0
- package/lib/typescript/src/web/utils.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownNativeComponent.ts +138 -1
- package/src/EnrichedMarkdownTextNativeComponent.ts +138 -1
- package/src/index.tsx +6 -0
- package/src/index.web.tsx +5 -1
- package/src/jest/index.tsx +22 -1
- package/src/native/EnrichedMarkdownText.tsx +220 -9
- package/src/normalizeMarkdownStyle.ts +15 -0
- package/src/normalizeMarkdownStyle.web.ts +13 -0
- package/src/types/MarkdownStyle.ts +51 -5
- package/src/types/MarkdownStyleInternal.ts +12 -0
- package/src/types/MarkdownTextProps.ts +151 -11
- package/src/types/MarkdownTextProps.web.ts +43 -1
- package/src/types/events.ts +69 -0
- package/src/web/EnrichedMarkdownText.tsx +10 -3
- package/src/web/renderers/InlineRenderers.tsx +111 -3
- package/src/web/styles.ts +25 -0
- package/src/web/types.ts +4 -0
- package/src/web/utils.ts +8 -0
|
@@ -7,6 +7,7 @@ import android.text.SpannableString
|
|
|
7
7
|
import android.text.StaticLayout
|
|
8
8
|
import android.text.TextPaint
|
|
9
9
|
import android.util.Log
|
|
10
|
+
import androidx.emoji2.text.EmojiCompat
|
|
10
11
|
import com.facebook.react.bridge.ReadableMap
|
|
11
12
|
import com.facebook.react.uimanager.PixelUtil
|
|
12
13
|
import com.facebook.yoga.YogaMeasureMode
|
|
@@ -92,6 +93,38 @@ object MeasurementStore {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
private val measurePaint = TextPaint()
|
|
96
|
+
|
|
97
|
+
@Volatile
|
|
98
|
+
private var emojiInitCallbackRegistered = false
|
|
99
|
+
|
|
100
|
+
// A view measured before EmojiCompat loaded keeps a size without its emoji glyphs.
|
|
101
|
+
private val emojiInitCallback =
|
|
102
|
+
object : EmojiCompat.InitCallback() {
|
|
103
|
+
override fun onInitialized() {
|
|
104
|
+
data.clear()
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// AppCompatTextView draws emoji the system font lacks through EmojiCompat, which can be
|
|
109
|
+
// wider than the fallback glyph and wrap a line earlier; measure the same glyphs.
|
|
110
|
+
private fun withEmojiSpans(text: CharSequence): CharSequence {
|
|
111
|
+
if (text.isEmpty() || !EmojiCompat.isConfigured()) return text
|
|
112
|
+
val emojiCompat = EmojiCompat.get()
|
|
113
|
+
if (emojiCompat.loadState != EmojiCompat.LOAD_STATE_SUCCEEDED) {
|
|
114
|
+
if (!emojiInitCallbackRegistered) {
|
|
115
|
+
emojiInitCallbackRegistered = true
|
|
116
|
+
emojiCompat.registerInitCallback(emojiInitCallback)
|
|
117
|
+
}
|
|
118
|
+
return text
|
|
119
|
+
}
|
|
120
|
+
return try {
|
|
121
|
+
emojiCompat.process(SpannableString(text)) ?: text
|
|
122
|
+
} catch (e: IllegalStateException) {
|
|
123
|
+
Log.w(TAG, "EmojiCompat processing failed, measuring raw text", e)
|
|
124
|
+
text
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
95
128
|
private val measureRenderer = Renderer()
|
|
96
129
|
|
|
97
130
|
@Volatile
|
|
@@ -337,9 +370,12 @@ object MeasurementStore {
|
|
|
337
370
|
tryRenderMarkdown(markdown, styleMap, context, md4cFlags, allowFontScaling, maxFontSizeMultiplier, imageRequestHeaders)
|
|
338
371
|
// Placeholders must be built with the same inlineMath.paddingVertical the real spans use,
|
|
339
372
|
// or the measured line box won't match the drawn one.
|
|
373
|
+
val inlineMathMap = styleMap.getMapOrNull("inlineMath")
|
|
374
|
+
val inlineMathOverflowDip = inlineMathMap.getFloatOrDefault("glyphOverflow", 0f)
|
|
340
375
|
spannable?.replaceMathSpansWithPlaceholders(
|
|
341
376
|
context,
|
|
342
|
-
PixelUtil.toPixelFromDIP(
|
|
377
|
+
PixelUtil.toPixelFromDIP(inlineMathMap.getFloatOrDefault("paddingVertical", 0f)) +
|
|
378
|
+
if (inlineMathOverflowDip > 0f) ceil(PixelUtil.toPixelFromDIP(inlineMathOverflowDip)) else 0f,
|
|
343
379
|
)
|
|
344
380
|
val textToMeasure = spannable ?: markdown
|
|
345
381
|
val (size, _) = measureWithLayout(width, textToMeasure, measurePaint, id)
|
|
@@ -456,7 +492,8 @@ object MeasurementStore {
|
|
|
456
492
|
for (i in mathSegmentIndices.indices) {
|
|
457
493
|
val metrics = mathResults[i]
|
|
458
494
|
mathHeightByIndex[mathSegmentIndices[i]] =
|
|
459
|
-
ceil(metrics.ascent + metrics.descent).toInt() + (style.mathStyle.padding * 2)
|
|
495
|
+
ceil(metrics.ascent + metrics.descent).toInt() + (style.mathStyle.padding * 2) +
|
|
496
|
+
(style.mathStyle.glyphOverflow * 2)
|
|
460
497
|
}
|
|
461
498
|
}
|
|
462
499
|
|
|
@@ -471,7 +508,7 @@ object MeasurementStore {
|
|
|
471
508
|
|
|
472
509
|
when (segment) {
|
|
473
510
|
is RenderedSegment.Text -> {
|
|
474
|
-
segment.styledText.replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.
|
|
511
|
+
segment.styledText.replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.boxPaddingVertical)
|
|
475
512
|
|
|
476
513
|
val layout = createStaticLayout(segment.styledText, fontSize, widthPx, id)
|
|
477
514
|
totalHeightPx += layout.height
|
|
@@ -546,9 +583,10 @@ object MeasurementStore {
|
|
|
546
583
|
viewId: Int?,
|
|
547
584
|
): StaticLayout {
|
|
548
585
|
measurePaint.textSize = fontSize
|
|
549
|
-
|
|
586
|
+
val content = withEmojiSpans(text)
|
|
587
|
+
prepareImageSpansForMeasurement(content, widthPx)
|
|
550
588
|
return StaticLayout.Builder
|
|
551
|
-
.obtain(
|
|
589
|
+
.obtain(content, 0, content.length, measurePaint, widthPx)
|
|
552
590
|
.setIncludePad(false)
|
|
553
591
|
.setLineSpacing(0f, 1f)
|
|
554
592
|
.apply {
|
|
@@ -579,7 +617,7 @@ object MeasurementStore {
|
|
|
579
617
|
style.imageRequestHeaders = imageRequestHeaders
|
|
580
618
|
measureRenderer.configure(style, context)
|
|
581
619
|
measureRenderer.renderDocument(ast, null).apply {
|
|
582
|
-
replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.
|
|
620
|
+
replaceMathSpansWithPlaceholders(context, style.inlineMathStyle.boxPaddingVertical)
|
|
583
621
|
}
|
|
584
622
|
} catch (e: Exception) {
|
|
585
623
|
Log.w(TAG, "Failed to render markdown for measurement, falling back to raw text", e)
|
|
@@ -628,7 +666,7 @@ object MeasurementStore {
|
|
|
628
666
|
paint: TextPaint,
|
|
629
667
|
viewId: Int?,
|
|
630
668
|
): Long {
|
|
631
|
-
val content = text ?: ""
|
|
669
|
+
val content = withEmojiSpans(text ?: "")
|
|
632
670
|
val safeWidth = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
633
671
|
prepareImageSpansForMeasurement(content, safeWidth)
|
|
634
672
|
|
|
@@ -668,7 +706,7 @@ object MeasurementStore {
|
|
|
668
706
|
paint: TextPaint,
|
|
669
707
|
viewId: Int?,
|
|
670
708
|
): Pair<Long, StaticLayout> {
|
|
671
|
-
val content = text ?: ""
|
|
709
|
+
val content = withEmojiSpans(text ?: "")
|
|
672
710
|
val widthPx = ceil(maxWidth).toInt().coerceAtLeast(1)
|
|
673
711
|
prepareImageSpansForMeasurement(content, widthPx)
|
|
674
712
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
|
|
7
|
+
class CopyRequestEvent(
|
|
8
|
+
surfaceId: Int,
|
|
9
|
+
viewId: Int,
|
|
10
|
+
private val source: String,
|
|
11
|
+
private val markdown: String,
|
|
12
|
+
private val text: String,
|
|
13
|
+
private val whole: Boolean,
|
|
14
|
+
) : Event<CopyRequestEvent>(surfaceId, viewId) {
|
|
15
|
+
override fun getEventName(): String = EVENT_NAME
|
|
16
|
+
|
|
17
|
+
override fun getEventData(): WritableMap =
|
|
18
|
+
Arguments.createMap().apply {
|
|
19
|
+
putString("source", source)
|
|
20
|
+
putString("markdown", markdown)
|
|
21
|
+
putString("text", text)
|
|
22
|
+
putBoolean("whole", whole)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
companion object {
|
|
26
|
+
const val EVENT_NAME: String = "onCopyRequest"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
import com.swmansion.enriched.markdown.utils.text.view.LinkLayoutFragment
|
|
7
|
+
|
|
8
|
+
class LinkLayoutEvent(
|
|
9
|
+
surfaceId: Int,
|
|
10
|
+
viewId: Int,
|
|
11
|
+
private val fragments: List<LinkLayoutFragment>,
|
|
12
|
+
) : Event<LinkLayoutEvent>(surfaceId, viewId) {
|
|
13
|
+
override fun getEventName(): String = EVENT_NAME
|
|
14
|
+
|
|
15
|
+
override fun getEventData(): WritableMap {
|
|
16
|
+
val array = Arguments.createArray()
|
|
17
|
+
fragments.forEach { fragment ->
|
|
18
|
+
array.pushMap(
|
|
19
|
+
Arguments.createMap().apply {
|
|
20
|
+
putInt("linkIndex", fragment.linkIndex)
|
|
21
|
+
putString("url", fragment.url)
|
|
22
|
+
putString("text", fragment.text)
|
|
23
|
+
putDouble("x", fragment.x.toDouble())
|
|
24
|
+
putDouble("y", fragment.y.toDouble())
|
|
25
|
+
putDouble("width", fragment.width.toDouble())
|
|
26
|
+
putDouble("height", fragment.height.toDouble())
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
return Arguments.createMap().apply { putArray("fragments", array) }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
companion object {
|
|
34
|
+
const val EVENT_NAME: String = "onLinkLayout"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
import com.swmansion.enriched.markdown.utils.text.view.SelectionRect
|
|
7
|
+
import com.swmansion.enriched.markdown.utils.text.view.SelectionSnapshot
|
|
8
|
+
|
|
9
|
+
class SelectionChangeEvent(
|
|
10
|
+
surfaceId: Int,
|
|
11
|
+
viewId: Int,
|
|
12
|
+
private val snapshot: SelectionSnapshot,
|
|
13
|
+
) : Event<SelectionChangeEvent>(surfaceId, viewId) {
|
|
14
|
+
override fun getEventName(): String = EVENT_NAME
|
|
15
|
+
|
|
16
|
+
// Every state is distinct (active/inactive flips must all arrive), so never merge.
|
|
17
|
+
override fun canCoalesce(): Boolean = false
|
|
18
|
+
|
|
19
|
+
override fun getEventData(): WritableMap =
|
|
20
|
+
Arguments.createMap().apply {
|
|
21
|
+
putBoolean("active", snapshot.active)
|
|
22
|
+
putString("source", snapshot.source)
|
|
23
|
+
putString("text", snapshot.text)
|
|
24
|
+
putString("markdown", snapshot.markdown)
|
|
25
|
+
putBoolean("whole", snapshot.whole)
|
|
26
|
+
putMap("rect", snapshot.rect.toWritableMap())
|
|
27
|
+
putMap("startRect", snapshot.startRect.toWritableMap())
|
|
28
|
+
putMap("endRect", snapshot.endRect.toWritableMap())
|
|
29
|
+
putBoolean("adjusting", snapshot.adjusting)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private fun SelectionRect.toWritableMap(): WritableMap =
|
|
33
|
+
Arguments.createMap().apply {
|
|
34
|
+
putDouble("x", x.toDouble())
|
|
35
|
+
putDouble("y", y.toDouble())
|
|
36
|
+
putDouble("width", width.toDouble())
|
|
37
|
+
putDouble("height", height.toDouble())
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
companion object {
|
|
41
|
+
const val EVENT_NAME: String = "onSelectionChange"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt
CHANGED
|
@@ -796,11 +796,13 @@ class EnrichedMarkdownTextInputView(
|
|
|
796
796
|
// and block ranges survive the round trip — mirrors iOS's copy:/cut:/paste:
|
|
797
797
|
// overrides. External clips keep default handling.
|
|
798
798
|
//
|
|
799
|
-
// Paste arrives by
|
|
799
|
+
// Paste arrives by three different routes and only the first one is a menu.
|
|
800
800
|
// This override covers the context menu and the hardware Ctrl+V /
|
|
801
801
|
// KEYCODE_PASTE shortcuts; drag-and-drop, autofill and IME `commitContent`
|
|
802
|
-
// reach the view through `onReceiveContent` instead
|
|
803
|
-
//
|
|
802
|
+
// reach the view through `onReceiveContent` instead, and a keyboard
|
|
803
|
+
// clipboard chip commits the text through the input connection (see
|
|
804
|
+
// interceptImePaste). Hooking only some of them leaves the others pasting
|
|
805
|
+
// straight into the buffer with nobody intercepting.
|
|
804
806
|
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
805
807
|
// Checked before the markdown paste below, which would otherwise insert first.
|
|
806
808
|
if (interceptPaste && (id == android.R.id.paste || id == android.R.id.pasteAsPlainText)) {
|
|
@@ -976,6 +978,26 @@ class EnrichedMarkdownTextInputView(
|
|
|
976
978
|
parts.second
|
|
977
979
|
}
|
|
978
980
|
|
|
981
|
+
/**
|
|
982
|
+
* Keyboard clipboard chips (Gboard, Samsung Keyboard) paste through
|
|
983
|
+
* `InputConnection.commitText`, a route neither [onTextContextMenuItem] nor
|
|
984
|
+
* the receive-content listener sees, so the text landed in the buffer with no
|
|
985
|
+
* `onPaste`. A commit that equals the primary clip is treated as that paste
|
|
986
|
+
* and reported like the other two routes; the IME commit is then dropped and
|
|
987
|
+
* JS completes it through [resolvePaste].
|
|
988
|
+
*
|
|
989
|
+
* Short typing-sized commits are not compared, so the clipboard is not read
|
|
990
|
+
* (and Android 12's clipboard toast not shown) while the user types; see
|
|
991
|
+
* [PasteTextUtils.mayBeImePaste] and [PasteTextUtils.isImePaste].
|
|
992
|
+
*/
|
|
993
|
+
internal fun interceptImePaste(committed: String): Boolean {
|
|
994
|
+
if (!interceptPaste || !PasteTextUtils.mayBeImePaste(committed)) return false
|
|
995
|
+
val clip = readClipboardText()
|
|
996
|
+
if (!PasteTextUtils.isImePaste(committed, clip)) return false
|
|
997
|
+
eventEmitter.emitPaste(clip, "")
|
|
998
|
+
return true
|
|
999
|
+
}
|
|
1000
|
+
|
|
979
1001
|
private fun readClipboardText(): String {
|
|
980
1002
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return ""
|
|
981
1003
|
val clip = clipboard.primaryClip ?: return ""
|
|
@@ -106,6 +106,13 @@ class InputConnectionWrapper(
|
|
|
106
106
|
if (key.length <= 2) {
|
|
107
107
|
dispatchKeyPressOrEnqueue(key.ifEmpty { BACKSPACE_KEY_VALUE })
|
|
108
108
|
}
|
|
109
|
+
// A keyboard clipboard chip pastes here. When the view claims it as a
|
|
110
|
+
// paste, the commit is dropped; the composing region is closed first so the
|
|
111
|
+
// word being typed stays in the buffer instead of being replaced.
|
|
112
|
+
if (editText.interceptImePaste(key)) {
|
|
113
|
+
super.finishComposingText()
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
109
116
|
return super.commitText(text, newCursorPosition)
|
|
110
117
|
}
|
|
111
118
|
|
|
@@ -2,6 +2,9 @@ package com.swmansion.enriched.markdown.renderer
|
|
|
2
2
|
|
|
3
3
|
import android.text.SpannableStringBuilder
|
|
4
4
|
import com.swmansion.enriched.markdown.parser.MarkdownASTNode
|
|
5
|
+
import com.swmansion.enriched.markdown.spans.LinkChipBackgroundSpan
|
|
6
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacerSpan
|
|
7
|
+
import com.swmansion.enriched.markdown.spans.LinkSpacers
|
|
5
8
|
import com.swmansion.enriched.markdown.spans.LinkSpan
|
|
6
9
|
import com.swmansion.enriched.markdown.utils.text.span.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
|
|
7
10
|
|
|
@@ -16,14 +19,41 @@ class LinkRenderer(
|
|
|
16
19
|
factory: RendererFactory,
|
|
17
20
|
) {
|
|
18
21
|
val url = node.getAttribute("url") ?: return
|
|
22
|
+
// Chip ("box") properties never fall back to the base link style.
|
|
23
|
+
val chip = factory.styleCache.linkChipForUrl(url)
|
|
24
|
+
val padding = chip.paddingHorizontal
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
val start = builder.length
|
|
27
|
+
// The leading spacer goes in before the children so ranges they register stay valid.
|
|
28
|
+
if (padding > 0f) appendSpacer(builder, padding)
|
|
29
|
+
val contentStart = builder.length
|
|
30
|
+
|
|
31
|
+
factory.renderChildren(node, builder, onLinkPress, onLinkLongPress)
|
|
32
|
+
|
|
33
|
+
if (builder.length == contentStart) {
|
|
34
|
+
if (contentStart > start) builder.delete(start, contentStart)
|
|
35
|
+
return
|
|
27
36
|
}
|
|
37
|
+
if (padding > 0f) appendSpacer(builder, padding)
|
|
38
|
+
val end = builder.length
|
|
39
|
+
|
|
40
|
+
val blockStyle = factory.blockStyleContext.requireBlockStyle()
|
|
41
|
+
builder.setSpan(
|
|
42
|
+
LinkSpan(url, onLinkPress, onLinkLongPress, factory.styleCache, blockStyle, factory.context),
|
|
43
|
+
start,
|
|
44
|
+
end,
|
|
45
|
+
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
|
|
46
|
+
)
|
|
47
|
+
// One instance per link: also identifies the link for link-layout reporting.
|
|
48
|
+
builder.setSpan(LinkChipBackgroundSpan(chip), start, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private fun appendSpacer(
|
|
52
|
+
builder: SpannableStringBuilder,
|
|
53
|
+
width: Float,
|
|
54
|
+
) {
|
|
55
|
+
val position = builder.length
|
|
56
|
+
builder.append(LinkSpacers.CHAR)
|
|
57
|
+
builder.setSpan(LinkSpacerSpan(width), position, position + 1, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE)
|
|
28
58
|
}
|
|
29
59
|
}
|
|
@@ -4,6 +4,7 @@ import android.content.res.AssetManager
|
|
|
4
4
|
import android.graphics.Typeface
|
|
5
5
|
import com.facebook.react.common.ReactConstants
|
|
6
6
|
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
|
|
7
|
+
import com.swmansion.enriched.markdown.styles.LinkChipStyle
|
|
7
8
|
import com.swmansion.enriched.markdown.styles.LinkVariantEntry
|
|
8
9
|
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
9
10
|
|
|
@@ -26,7 +27,7 @@ class SpanStyleCache(
|
|
|
26
27
|
val linkFontFamily: String = style.linkStyle.fontFamily
|
|
27
28
|
val linkColor: Int = style.linkStyle.color
|
|
28
29
|
val linkUnderline: Boolean = style.linkStyle.underline
|
|
29
|
-
val
|
|
30
|
+
val linkChip: LinkChipStyle = style.linkStyle.chip
|
|
30
31
|
val linkVariants: List<LinkVariantEntry> = style.linkVariants
|
|
31
32
|
|
|
32
33
|
private val compiledVariantPatterns: List<Pair<Regex, LinkVariantEntry>> =
|
|
@@ -100,6 +101,9 @@ class SpanStyleCache(
|
|
|
100
101
|
return compiledVariantPatterns.firstOrNull { (regex, _) -> regex.containsMatchIn(url) }?.second
|
|
101
102
|
}
|
|
102
103
|
|
|
104
|
+
/** Chip style for [url]: the matching variant's own, else the base link's (never merged). */
|
|
105
|
+
fun linkChipForUrl(url: String): LinkChipStyle = resolvedVariantForUrl(url)?.chip ?: linkChip
|
|
106
|
+
|
|
103
107
|
/**
|
|
104
108
|
* Cached typeface for font family + style (BOLD, ITALIC, BOLD_ITALIC).
|
|
105
109
|
*
|
|
@@ -5,9 +5,6 @@ import android.graphics.Paint
|
|
|
5
5
|
import android.graphics.Path
|
|
6
6
|
import android.graphics.RectF
|
|
7
7
|
import android.text.Spanned
|
|
8
|
-
import android.text.StaticLayout
|
|
9
|
-
import android.text.TextPaint
|
|
10
|
-
import android.text.style.LeadingMarginSpan
|
|
11
8
|
import android.text.style.LineBackgroundSpan
|
|
12
9
|
import com.swmansion.enriched.markdown.styles.StyleConfig
|
|
13
10
|
import kotlin.math.max
|
|
@@ -73,14 +70,6 @@ class CodeBackgroundSpan(
|
|
|
73
70
|
drawShapes(canvas, isFirst, isLast)
|
|
74
71
|
}
|
|
75
72
|
|
|
76
|
-
/**
|
|
77
|
-
* Returns the x position of [index] relative to the line's left edge, including any
|
|
78
|
-
* leading margin. The measuring StaticLayout is built from a subSequence that keeps
|
|
79
|
-
* all spans, so LeadingMarginSpans (lists, blockquotes) are already applied to
|
|
80
|
-
* getPrimaryHorizontal; adding the margin again on top would shift the background
|
|
81
|
-
* right by the indent. The margin is only added explicitly in the early-return case,
|
|
82
|
-
* where no layout is built.
|
|
83
|
-
*/
|
|
84
73
|
private fun getHorizontalOffset(
|
|
85
74
|
text: CharSequence,
|
|
86
75
|
lineStart: Int,
|
|
@@ -88,105 +77,30 @@ class CodeBackgroundSpan(
|
|
|
88
77
|
index: Int,
|
|
89
78
|
paint: Paint,
|
|
90
79
|
leadingMargin: Int,
|
|
91
|
-
): Float
|
|
92
|
-
if (index <= lineStart) return leadingMargin.toFloat()
|
|
93
|
-
val lineText = text.subSequence(lineStart, lineEnd)
|
|
94
|
-
val textPaint = paint as? TextPaint ?: TextPaint(paint)
|
|
95
|
-
val layout = StaticLayout.Builder.obtain(lineText, 0, lineText.length, textPaint, 10000).build()
|
|
96
|
-
return layout.getPrimaryHorizontal(index - lineStart)
|
|
97
|
-
}
|
|
80
|
+
): Float = InlineChipPainter.horizontalOffset(text, lineStart, lineEnd, index, paint, leadingMargin)
|
|
98
81
|
|
|
99
82
|
private fun drawShapes(
|
|
100
83
|
canvas: Canvas,
|
|
101
84
|
isFirst: Boolean,
|
|
102
85
|
isLast: Boolean,
|
|
103
86
|
) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
private fun drawOpenBorders(
|
|
118
|
-
canvas: Canvas,
|
|
119
|
-
isFirst: Boolean,
|
|
120
|
-
isLast: Boolean,
|
|
121
|
-
) {
|
|
122
|
-
val r = CORNER_RADIUS
|
|
123
|
-
path.reset()
|
|
124
|
-
|
|
125
|
-
if (isFirst) {
|
|
126
|
-
path.moveTo(rect.right, rect.top)
|
|
127
|
-
path.lineTo(rect.left + r, rect.top)
|
|
128
|
-
path.quadTo(rect.left, rect.top, rect.left, rect.top + r)
|
|
129
|
-
path.lineTo(rect.left, rect.bottom - r)
|
|
130
|
-
path.quadTo(rect.left, rect.bottom, rect.left + r, rect.bottom)
|
|
131
|
-
path.lineTo(rect.right, rect.bottom)
|
|
132
|
-
} else if (isLast) {
|
|
133
|
-
path.moveTo(rect.left, rect.top)
|
|
134
|
-
path.lineTo(rect.right - r, rect.top)
|
|
135
|
-
path.quadTo(rect.right, rect.top, rect.right, rect.top + r)
|
|
136
|
-
path.lineTo(rect.right, rect.bottom - r)
|
|
137
|
-
path.quadTo(rect.right, rect.bottom, rect.right - r, rect.bottom)
|
|
138
|
-
path.lineTo(rect.left, rect.bottom)
|
|
139
|
-
} else {
|
|
140
|
-
path.moveTo(rect.left, rect.top)
|
|
141
|
-
path.lineTo(rect.right, rect.top)
|
|
142
|
-
path.moveTo(rect.left, rect.bottom)
|
|
143
|
-
path.lineTo(rect.right, rect.bottom)
|
|
144
|
-
}
|
|
145
|
-
canvas.drawPath(path, sharedBorderPaint)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private fun createRadii(
|
|
149
|
-
isFirst: Boolean,
|
|
150
|
-
isLast: Boolean,
|
|
151
|
-
) = when {
|
|
152
|
-
isFirst && isLast -> {
|
|
153
|
-
floatArrayOf(
|
|
154
|
-
CORNER_RADIUS,
|
|
155
|
-
CORNER_RADIUS,
|
|
156
|
-
CORNER_RADIUS,
|
|
157
|
-
CORNER_RADIUS,
|
|
158
|
-
CORNER_RADIUS,
|
|
159
|
-
CORNER_RADIUS,
|
|
160
|
-
CORNER_RADIUS,
|
|
161
|
-
CORNER_RADIUS,
|
|
162
|
-
)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
isFirst -> {
|
|
166
|
-
floatArrayOf(CORNER_RADIUS, CORNER_RADIUS, 0f, 0f, 0f, 0f, CORNER_RADIUS, CORNER_RADIUS)
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
isLast -> {
|
|
170
|
-
floatArrayOf(0f, 0f, CORNER_RADIUS, CORNER_RADIUS, CORNER_RADIUS, CORNER_RADIUS, 0f, 0f)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
else -> {
|
|
174
|
-
floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f)
|
|
175
|
-
}
|
|
87
|
+
InlineChipPainter.draw(
|
|
88
|
+
canvas,
|
|
89
|
+
rect,
|
|
90
|
+
path,
|
|
91
|
+
CORNER_RADIUS,
|
|
92
|
+
sharedBackgroundPaint,
|
|
93
|
+
sharedBorderPaint,
|
|
94
|
+
isFirst,
|
|
95
|
+
isLast,
|
|
96
|
+
strokeInset = false,
|
|
97
|
+
)
|
|
176
98
|
}
|
|
177
99
|
|
|
178
100
|
private fun leadingMarginAt(
|
|
179
101
|
text: Spanned,
|
|
180
102
|
lineStart: Int,
|
|
181
|
-
): Int
|
|
182
|
-
if (lineStart >= text.length) return 0
|
|
183
|
-
val spans = text.getSpans(lineStart, lineStart + 1, LeadingMarginSpan::class.java)
|
|
184
|
-
var margin = 0
|
|
185
|
-
for (span in spans) {
|
|
186
|
-
margin += span.getLeadingMargin(false)
|
|
187
|
-
}
|
|
188
|
-
return margin
|
|
189
|
-
}
|
|
103
|
+
): Int = InlineChipPainter.leadingMarginAt(text, lineStart)
|
|
190
104
|
|
|
191
105
|
private fun adjustBottomForMargin(
|
|
192
106
|
text: Spanned,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.spans
|
|
2
|
+
|
|
3
|
+
import android.graphics.Canvas
|
|
4
|
+
import android.graphics.Paint
|
|
5
|
+
import android.graphics.Path
|
|
6
|
+
import android.graphics.RectF
|
|
7
|
+
import android.text.Spanned
|
|
8
|
+
import android.text.StaticLayout
|
|
9
|
+
import android.text.TextPaint
|
|
10
|
+
import android.text.style.LeadingMarginSpan
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shared geometry and drawing for inline "chips" painted from a [android.text.style.LineBackgroundSpan]
|
|
14
|
+
* (inline code, link chips). For a chip that wraps, only the outer ends are rounded and the border
|
|
15
|
+
* stays open where a fragment is cut.
|
|
16
|
+
*/
|
|
17
|
+
internal object InlineChipPainter {
|
|
18
|
+
/**
|
|
19
|
+
* Returns the x position of [index] relative to the line's left edge, including any
|
|
20
|
+
* leading margin. The measuring StaticLayout is built from a subSequence that keeps
|
|
21
|
+
* all spans, so LeadingMarginSpans (lists, blockquotes) are already applied to
|
|
22
|
+
* getPrimaryHorizontal; adding the margin again on top would shift the background
|
|
23
|
+
* right by the indent. The margin is only added explicitly in the early-return case,
|
|
24
|
+
* where no layout is built.
|
|
25
|
+
*/
|
|
26
|
+
fun horizontalOffset(
|
|
27
|
+
text: CharSequence,
|
|
28
|
+
lineStart: Int,
|
|
29
|
+
lineEnd: Int,
|
|
30
|
+
index: Int,
|
|
31
|
+
paint: Paint,
|
|
32
|
+
leadingMargin: Int,
|
|
33
|
+
): Float {
|
|
34
|
+
if (index <= lineStart) return leadingMargin.toFloat()
|
|
35
|
+
val lineText = text.subSequence(lineStart, lineEnd)
|
|
36
|
+
val textPaint = paint as? TextPaint ?: TextPaint(paint)
|
|
37
|
+
val layout = StaticLayout.Builder.obtain(lineText, 0, lineText.length, textPaint, 10000).build()
|
|
38
|
+
return layout.getPrimaryHorizontal(index - lineStart)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fun leadingMarginAt(
|
|
42
|
+
text: Spanned,
|
|
43
|
+
lineStart: Int,
|
|
44
|
+
): Int {
|
|
45
|
+
if (lineStart >= text.length) return 0
|
|
46
|
+
val spans = text.getSpans(lineStart, lineStart + 1, LeadingMarginSpan::class.java)
|
|
47
|
+
var margin = 0
|
|
48
|
+
for (span in spans) {
|
|
49
|
+
margin += span.getLeadingMargin(false)
|
|
50
|
+
}
|
|
51
|
+
return margin
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fills [rect] and strokes its border. [strokeInset] keeps a closed border inside [rect]
|
|
56
|
+
* (link chips); otherwise it is centered on the edge (inline code, kept for pixel parity).
|
|
57
|
+
* [path] is caller-owned scratch space to avoid per-draw allocations.
|
|
58
|
+
*/
|
|
59
|
+
fun draw(
|
|
60
|
+
canvas: Canvas,
|
|
61
|
+
rect: RectF,
|
|
62
|
+
path: Path,
|
|
63
|
+
cornerRadius: Float,
|
|
64
|
+
backgroundPaint: Paint?,
|
|
65
|
+
borderPaint: Paint?,
|
|
66
|
+
isFirst: Boolean,
|
|
67
|
+
isLast: Boolean,
|
|
68
|
+
strokeInset: Boolean,
|
|
69
|
+
) {
|
|
70
|
+
val r = minOf(maxOf(cornerRadius, 0f), minOf(rect.width(), rect.height()) / 2f)
|
|
71
|
+
|
|
72
|
+
if (backgroundPaint != null) {
|
|
73
|
+
path.reset()
|
|
74
|
+
path.addRoundRect(rect, radii(r, isFirst, isLast), Path.Direction.CW)
|
|
75
|
+
canvas.drawPath(path, backgroundPaint)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (borderPaint == null || borderPaint.strokeWidth <= 0f) return
|
|
79
|
+
|
|
80
|
+
path.reset()
|
|
81
|
+
if (isFirst && isLast) {
|
|
82
|
+
if (strokeInset) {
|
|
83
|
+
val inset = borderPaint.strokeWidth / 2f
|
|
84
|
+
val insetRect = RectF(rect.left + inset, rect.top + inset, rect.right - inset, rect.bottom - inset)
|
|
85
|
+
path.addRoundRect(insetRect, radii(maxOf(r - inset, 0f), true, true), Path.Direction.CW)
|
|
86
|
+
} else {
|
|
87
|
+
path.addRoundRect(rect, radii(r, true, true), Path.Direction.CW)
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
val inset = if (strokeInset) borderPaint.strokeWidth / 2f else 0f
|
|
91
|
+
addOpenBorder(path, rect, r, inset, isFirst, isLast)
|
|
92
|
+
}
|
|
93
|
+
canvas.drawPath(path, borderPaint)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private fun addOpenBorder(
|
|
97
|
+
path: Path,
|
|
98
|
+
rect: RectF,
|
|
99
|
+
r: Float,
|
|
100
|
+
inset: Float,
|
|
101
|
+
isFirst: Boolean,
|
|
102
|
+
isLast: Boolean,
|
|
103
|
+
) {
|
|
104
|
+
val left = rect.left + inset
|
|
105
|
+
val top = rect.top + inset
|
|
106
|
+
val right = rect.right - inset
|
|
107
|
+
val bottom = rect.bottom - inset
|
|
108
|
+
|
|
109
|
+
if (isFirst) {
|
|
110
|
+
path.moveTo(rect.right, top)
|
|
111
|
+
path.lineTo(left + r, top)
|
|
112
|
+
path.quadTo(left, top, left, top + r)
|
|
113
|
+
path.lineTo(left, bottom - r)
|
|
114
|
+
path.quadTo(left, bottom, left + r, bottom)
|
|
115
|
+
path.lineTo(rect.right, bottom)
|
|
116
|
+
} else if (isLast) {
|
|
117
|
+
path.moveTo(rect.left, top)
|
|
118
|
+
path.lineTo(right - r, top)
|
|
119
|
+
path.quadTo(right, top, right, top + r)
|
|
120
|
+
path.lineTo(right, bottom - r)
|
|
121
|
+
path.quadTo(right, bottom, right - r, bottom)
|
|
122
|
+
path.lineTo(rect.left, bottom)
|
|
123
|
+
} else {
|
|
124
|
+
path.moveTo(rect.left, top)
|
|
125
|
+
path.lineTo(rect.right, top)
|
|
126
|
+
path.moveTo(rect.left, bottom)
|
|
127
|
+
path.lineTo(rect.right, bottom)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun radii(
|
|
132
|
+
r: Float,
|
|
133
|
+
isFirst: Boolean,
|
|
134
|
+
isLast: Boolean,
|
|
135
|
+
): FloatArray {
|
|
136
|
+
val left = if (isFirst) r else 0f
|
|
137
|
+
val right = if (isLast) r else 0f
|
|
138
|
+
return floatArrayOf(left, left, right, right, right, right, left, left)
|
|
139
|
+
}
|
|
140
|
+
}
|