Skip to content

Commit 032a06b

Browse files
authored
Merge pull request #2 from software-mansion-labs/@hryhoriiK97/android/implement-basic-android-markdown-component
[android] Implement basic Android markdown rendering
2 parents 86f7b44 + f1184f3 commit 032a06b

14 files changed

Lines changed: 557 additions & 20 deletions

android/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,5 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
7474
dependencies {
7575
implementation "com.facebook.react:react-android"
7676
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77+
implementation "org.commonmark:commonmark:0.27.0"
7778
}
Lines changed: 129 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,138 @@
11
package com.richtext
22

33
import android.content.Context
4+
import android.graphics.Color
5+
import android.text.method.LinkMovementMethod
46
import android.util.AttributeSet
5-
import android.view.View
7+
import androidx.appcompat.widget.AppCompatTextView
8+
import com.facebook.react.common.ReactConstants
9+
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
10+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
11+
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
12+
import com.richtext.parser.Parser
13+
import com.richtext.renderer.Renderer
614

7-
class RichTextView : View {
8-
constructor(context: Context?) : super(context)
9-
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
10-
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(
15+
class RichTextView : AppCompatTextView {
16+
17+
private val parser = Parser()
18+
private val renderer = Renderer()
19+
private var onLinkPressCallback: ((String) -> Unit)? = null
20+
21+
private var typefaceDirty = false
22+
private var didAttachToWindow = false
23+
24+
var fontSize: Float? = null
25+
private var fontFamily: String? = null
26+
private var fontStyle: Int = ReactConstants.UNSET
27+
private var fontWeight: Int = ReactConstants.UNSET
28+
29+
constructor(context: Context) : super(context) {
30+
prepareComponent()
31+
}
32+
33+
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
34+
prepareComponent()
35+
}
36+
37+
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(
1138
context,
1239
attrs,
1340
defStyleAttr
14-
)
41+
) {
42+
prepareComponent()
43+
}
44+
45+
private fun prepareComponent() {
46+
movementMethod = LinkMovementMethod.getInstance()
47+
setPadding(0, 0, 0, 0)
48+
setBackgroundColor(Color.TRANSPARENT)
49+
}
50+
51+
fun setMarkdownContent(markdown: String) {
52+
try {
53+
val document = parser.parseMarkdown(markdown)
54+
if (document != null) {
55+
val styledText = renderer.renderDocument(document, onLinkPressCallback)
56+
text = styledText
57+
movementMethod = LinkMovementMethod.getInstance()
58+
} else {
59+
android.util.Log.e("RichTextView", "Failed to parse markdown - Document is null")
60+
text = ""
61+
}
62+
} catch (e: Exception) {
63+
android.util.Log.e("RichTextView", "Error parsing markdown: ${e.message}")
64+
text = ""
65+
}
66+
}
67+
68+
69+
fun setOnLinkPressCallback(callback: (String) -> Unit) {
70+
onLinkPressCallback = callback
71+
}
72+
73+
fun emitOnLinkPress(url: String) {
74+
val context = this.context as? com.facebook.react.bridge.ReactContext ?: return
75+
val surfaceId = com.facebook.react.uimanager.UIManagerHelper.getSurfaceId(context)
76+
val dispatcher =
77+
com.facebook.react.uimanager.UIManagerHelper.getEventDispatcherForReactTag(context, id)
78+
79+
dispatcher?.dispatchEvent(
80+
com.richtext.events.LinkPressEvent(
81+
surfaceId,
82+
id,
83+
url
84+
)
85+
)
86+
}
87+
88+
fun setFontSize(size: Float) {
89+
fontSize = size
90+
textSize = size
91+
typefaceDirty = true
92+
updateTypeface()
93+
}
94+
95+
fun setFontFamily(family: String?) {
96+
fontFamily = family
97+
typefaceDirty = true
98+
updateTypeface()
99+
}
100+
101+
fun setFontWeight(weight: String?) {
102+
val parsedWeight = parseFontWeight(weight)
103+
if (parsedWeight != fontWeight) {
104+
fontWeight = parsedWeight
105+
typefaceDirty = true
106+
updateTypeface()
107+
}
108+
}
109+
110+
fun setFontStyle(style: String?) {
111+
val parsedStyle = parseFontStyle(style)
112+
if (parsedStyle != fontStyle) {
113+
fontStyle = parsedStyle
114+
typefaceDirty = true
115+
updateTypeface()
116+
}
117+
}
118+
119+
fun setColor(color: Int?) {
120+
if (color != null) {
121+
setTextColor(color)
122+
}
123+
}
124+
125+
fun updateTypeface() {
126+
if (!typefaceDirty) return
127+
typefaceDirty = false
128+
129+
val newTypeface = applyStyles(typeface, fontStyle, fontWeight, fontFamily, context.assets)
130+
setTypeface(newTypeface)
131+
}
132+
133+
override fun onAttachedToWindow() {
134+
super.onAttachedToWindow()
135+
didAttachToWindow = true
136+
updateTypeface()
137+
}
15138
}

android/src/main/java/com/richtext/RichTextViewManager.kt

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
package com.richtext
22

3-
import android.graphics.Color
43
import com.facebook.react.module.annotations.ReactModule
54
import com.facebook.react.uimanager.SimpleViewManager
65
import com.facebook.react.uimanager.ThemedReactContext
7-
import com.facebook.react.uimanager.ViewManagerDelegate
86
import com.facebook.react.uimanager.annotations.ReactProp
9-
import com.facebook.react.viewmanagers.RichTextViewManagerInterface
7+
import com.facebook.react.uimanager.UIManagerHelper
8+
import com.facebook.react.uimanager.ViewProps
9+
import com.facebook.react.uimanager.ViewDefaults
10+
import com.facebook.react.uimanager.ViewManagerDelegate
1011
import com.facebook.react.viewmanagers.RichTextViewManagerDelegate
12+
import com.facebook.react.viewmanagers.RichTextViewManagerInterface
13+
import com.richtext.events.LinkPressEvent
1114

1215
@ReactModule(name = RichTextViewManager.NAME)
1316
class RichTextViewManager : SimpleViewManager<RichTextView>(),
1417
RichTextViewManagerInterface<RichTextView> {
15-
private val mDelegate: ViewManagerDelegate<RichTextView>
1618

17-
init {
18-
mDelegate = RichTextViewManagerDelegate(this)
19-
}
19+
private val mDelegate: ViewManagerDelegate<RichTextView> = RichTextViewManagerDelegate(this)
2020

2121
override fun getDelegate(): ViewManagerDelegate<RichTextView>? {
2222
return mDelegate
@@ -26,13 +26,63 @@ class RichTextViewManager : SimpleViewManager<RichTextView>(),
2626
return NAME
2727
}
2828

29-
public override fun createViewInstance(context: ThemedReactContext): RichTextView {
30-
return RichTextView(context)
29+
override fun createViewInstance(reactContext: ThemedReactContext): RichTextView {
30+
return RichTextView(reactContext)
31+
}
32+
33+
override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Any> {
34+
val map = mutableMapOf<String, Any>()
35+
map.put(LinkPressEvent.EVENT_NAME, mapOf("registrationName" to LinkPressEvent.EVENT_NAME))
36+
return map
37+
}
38+
39+
@ReactProp(name = "markdown")
40+
override fun setMarkdown(view: RichTextView?, markdown: String?) {
41+
view?.setOnLinkPressCallback { url ->
42+
emitOnLinkPress(view, url)
43+
}
44+
45+
view?.setMarkdownContent(markdown ?: "No markdown content")
46+
}
47+
48+
49+
@ReactProp(name = "fontSize", defaultInt = ViewDefaults.FONT_SIZE_SP.toInt())
50+
override fun setFontSize(view: RichTextView?, fontSize: Int) {
51+
view?.setFontSize(fontSize.toFloat())
52+
}
53+
54+
@ReactProp(name = "fontFamily")
55+
override fun setFontFamily(view: RichTextView?, family: String?) {
56+
view?.setFontFamily(family)
57+
}
58+
59+
@ReactProp(name = ViewProps.COLOR, customType = "Color")
60+
override fun setColor(view: RichTextView?, color: Int?) {
61+
view?.setColor(color)
62+
}
63+
64+
@ReactProp(name = "fontWeight")
65+
override fun setFontWeight(view: RichTextView?, weight: String?) {
66+
view?.setFontWeight(weight)
67+
}
68+
69+
@ReactProp(name = "fontStyle")
70+
override fun setFontStyle(view: RichTextView?, style: String?) {
71+
view?.setFontStyle(style)
3172
}
3273

33-
@ReactProp(name = "color")
34-
override fun setColor(view: RichTextView?, color: String?) {
35-
view?.setBackgroundColor(Color.parseColor(color))
74+
override fun onAfterUpdateTransaction(view: RichTextView) {
75+
super.onAfterUpdateTransaction(view)
76+
view.updateTypeface()
77+
}
78+
79+
private fun emitOnLinkPress(view: RichTextView, url: String) {
80+
val context = view.context as com.facebook.react.bridge.ReactContext
81+
val surfaceId = UIManagerHelper.getSurfaceId(context)
82+
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, view.id)
83+
val event = LinkPressEvent(surfaceId, view.id, url)
84+
85+
eventDispatcher?.dispatchEvent(event)
3686
}
3787

3888
companion object {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.richtext.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 LinkPressEvent(surfaceId: Int, viewId: Int, private val url: String) :
8+
Event<LinkPressEvent>(surfaceId, viewId) {
9+
10+
override fun getEventName(): String {
11+
return EVENT_NAME
12+
}
13+
14+
override fun getEventData(): WritableMap {
15+
val eventData: WritableMap = Arguments.createMap()
16+
eventData.putString("url", url)
17+
return eventData
18+
}
19+
20+
companion object {
21+
const val EVENT_NAME: String = "onLinkPress"
22+
}
23+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.richtext.parser
2+
3+
import android.util.Log
4+
import org.commonmark.node.Document
5+
import org.commonmark.parser.Parser
6+
7+
class Parser {
8+
9+
private val parser = Parser.builder().build()
10+
11+
fun parseMarkdown(markdown: String): Document? {
12+
if (markdown.isBlank()) {
13+
return null
14+
}
15+
16+
try {
17+
val document = parser.parse(markdown) as? Document
18+
19+
if (document != null) {
20+
return document
21+
} else {
22+
Log.w("MarkdownParser", "Failed to cast parsed result to Document")
23+
return null
24+
}
25+
} catch (e: Exception) {
26+
Log.e("MarkdownParser", "CommonMark parsing failed: ${e.message}")
27+
return null
28+
}
29+
}
30+
}

0 commit comments

Comments
 (0)