Skip to content

Commit 8c6bf12

Browse files
authored
Merge pull request #7 from software-mansion-labs/@hryhoriiK97/implement-styles-for-elements
Implement styles for all elements
2 parents b3ffcdd + 171712d commit 8c6bf12

24 files changed

Lines changed: 896 additions & 124 deletions

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import com.facebook.react.common.ReactConstants
99
import com.facebook.react.views.text.ReactTypefaceUtils.applyStyles
1010
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontStyle
1111
import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
12+
import com.facebook.react.bridge.ReadableMap
1213
import com.richtext.parser.Parser
1314
import com.richtext.renderer.Renderer
15+
import com.richtext.styles.RichTextStyle
1416

1517
class RichTextView : AppCompatTextView {
1618

@@ -26,6 +28,9 @@ class RichTextView : AppCompatTextView {
2628
private var fontStyle: Int = ReactConstants.UNSET
2729
private var fontWeight: Int = ReactConstants.UNSET
2830

31+
var richTextStyle: RichTextStyle? = null
32+
private var currentMarkdown: String = ""
33+
2934
constructor(context: Context) : super(context) {
3035
prepareComponent()
3136
}
@@ -49,9 +54,18 @@ class RichTextView : AppCompatTextView {
4954
}
5055

5156
fun setMarkdownContent(markdown: String) {
57+
currentMarkdown = markdown
58+
renderMarkdown()
59+
}
60+
61+
fun renderMarkdown() {
5262
try {
53-
val document = parser.parseMarkdown(markdown)
63+
val document = parser.parseMarkdown(currentMarkdown)
5464
if (document != null) {
65+
val currentStyle = requireNotNull(richTextStyle) {
66+
"richTextStyle should always be provided from JS side with defaults."
67+
}
68+
renderer.setStyle(currentStyle)
5569
val styledText = renderer.renderDocument(document, onLinkPressCallback)
5670
text = styledText
5771
movementMethod = LinkMovementMethod.getInstance()
@@ -65,6 +79,15 @@ class RichTextView : AppCompatTextView {
6579
}
6680
}
6781

82+
fun setRichTextStyle(style: ReadableMap?) {
83+
val newStyle = style?.let { RichTextStyle(it) }
84+
val styleChanged = richTextStyle != newStyle
85+
richTextStyle = newStyle
86+
if (styleChanged && currentMarkdown.isNotEmpty()) {
87+
renderMarkdown()
88+
}
89+
}
90+
6891

6992
fun setOnLinkPressCallback(callback: (String) -> Unit) {
7093
onLinkPressCallback = callback

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ class RichTextViewManager : SimpleViewManager<RichTextView>(),
7171
view?.setFontStyle(style)
7272
}
7373

74+
@ReactProp(name = "richTextStyle")
75+
override fun setRichTextStyle(view: RichTextView?, style: com.facebook.react.bridge.ReadableMap?) {
76+
view?.setRichTextStyle(style)
77+
}
78+
7479
override fun onAfterUpdateTransaction(view: RichTextView) {
7580
super.onAfterUpdateTransaction(view)
7681
view.updateTypeface()

android/src/main/java/com/richtext/renderer/NodeRenderer.kt

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

33
import android.text.SpannableStringBuilder
4-
import android.text.style.UnderlineSpan
54
import com.richtext.spans.RichTextLinkSpan
65
import com.richtext.spans.RichTextHeadingSpan
76
import com.richtext.spans.RichTextParagraphSpan
87
import com.richtext.spans.RichTextTextSpan
8+
import com.richtext.styles.RichTextStyle
99
import com.richtext.utils.addSpacing
1010
import org.commonmark.node.*
1111

@@ -17,7 +17,13 @@ interface NodeRenderer {
1717
)
1818
}
1919

20-
class DocumentRenderer : NodeRenderer {
20+
data class RendererConfig(
21+
val style: RichTextStyle
22+
)
23+
24+
class DocumentRenderer(
25+
private val config: RendererConfig? = null
26+
) : NodeRenderer {
2127
override fun render(
2228
node: Node,
2329
builder: SpannableStringBuilder,
@@ -26,13 +32,15 @@ class DocumentRenderer : NodeRenderer {
2632
val document = node as Document
2733
var child = document.firstChild
2834
while (child != null) {
29-
NodeRendererFactory.getRenderer(child).render(child, builder, onLinkPress)
35+
NodeRendererFactory.getRenderer(child, config).render(child, builder, onLinkPress)
3036
child = child.next
3137
}
3238
}
3339
}
3440

35-
class ParagraphRenderer : NodeRenderer {
41+
class ParagraphRenderer(
42+
private val config: RendererConfig? = null
43+
) : NodeRenderer {
3644
override fun render(
3745
node: Node,
3846
builder: SpannableStringBuilder,
@@ -43,7 +51,7 @@ class ParagraphRenderer : NodeRenderer {
4351

4452
var child = paragraph.firstChild
4553
while (child != null) {
46-
NodeRendererFactory.getRenderer(child).render(child, builder, onLinkPress)
54+
NodeRendererFactory.getRenderer(child, config).render(child, builder, onLinkPress)
4755
child = child.next
4856
}
4957

@@ -61,7 +69,9 @@ class ParagraphRenderer : NodeRenderer {
6169
}
6270
}
6371

64-
class HeadingRenderer : NodeRenderer {
72+
class HeadingRenderer(
73+
private val config: RendererConfig? = null
74+
) : NodeRenderer {
6575
override fun render(
6676
node: Node,
6777
builder: SpannableStringBuilder,
@@ -72,14 +82,17 @@ class HeadingRenderer : NodeRenderer {
7282

7383
var child = heading.firstChild
7484
while (child != null) {
75-
NodeRendererFactory.getRenderer(child).render(child, builder, onLinkPress)
85+
NodeRendererFactory.getRenderer(child, config).render(child, builder, onLinkPress)
7686
child = child.next
7787
}
7888

7989
val contentLength = builder.length - start
80-
if (contentLength > 0) {
90+
if (contentLength > 0 && config != null) {
8191
builder.setSpan(
82-
RichTextHeadingSpan(heading.level),
92+
RichTextHeadingSpan(
93+
heading.level,
94+
config.style
95+
),
8396
start,
8497
start + contentLength,
8598
android.text.SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
@@ -114,7 +127,9 @@ class TextRenderer : NodeRenderer {
114127
}
115128
}
116129

117-
class LinkRenderer : NodeRenderer {
130+
class LinkRenderer(
131+
private val config: RendererConfig? = null
132+
) : NodeRenderer {
118133
override fun render(
119134
node: Node,
120135
builder: SpannableStringBuilder,
@@ -126,21 +141,14 @@ class LinkRenderer : NodeRenderer {
126141

127142
var child = link.firstChild
128143
while (child != null) {
129-
NodeRendererFactory.getRenderer(child).render(child, builder, onLinkPress)
144+
NodeRendererFactory.getRenderer(child, config).render(child, builder, onLinkPress)
130145
child = child.next
131146
}
132147

133148
val contentLength = builder.length - start
134-
if (contentLength > 0) {
135-
builder.setSpan(
136-
RichTextLinkSpan(url, onLinkPress),
137-
start,
138-
start + contentLength,
139-
android.text.SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
140-
)
141-
149+
if (contentLength > 0 && config != null) {
142150
builder.setSpan(
143-
UnderlineSpan(),
151+
RichTextLinkSpan(url, onLinkPress, config.style),
144152
start,
145153
start + contentLength,
146154
android.text.SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE
@@ -160,13 +168,13 @@ class LineBreakRenderer : NodeRenderer {
160168
}
161169

162170
object NodeRendererFactory {
163-
fun getRenderer(node: Node): NodeRenderer {
171+
fun getRenderer(node: Node, config: RendererConfig? = null): NodeRenderer {
164172
return when (node) {
165-
is Document -> DocumentRenderer()
166-
is Paragraph -> ParagraphRenderer()
167-
is Heading -> HeadingRenderer()
173+
is Document -> DocumentRenderer(config)
174+
is Paragraph -> ParagraphRenderer(config)
175+
is Heading -> HeadingRenderer(config)
168176
is Text -> TextRenderer()
169-
is Link -> LinkRenderer()
177+
is Link -> LinkRenderer(config)
170178
is HardLineBreak, is SoftLineBreak -> LineBreakRenderer()
171179
else -> {
172180
android.util.Log.w(

android/src/main/java/com/richtext/renderer/Renderer.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,35 @@ package com.richtext.renderer
22

33
import android.text.SpannableString
44
import android.text.SpannableStringBuilder
5+
import com.richtext.styles.RichTextStyle
56
import org.commonmark.node.*
67

78
class Renderer {
9+
private var style: RichTextStyle? = null
10+
11+
fun setStyle(style: RichTextStyle) {
12+
this.style = style
13+
}
14+
815
fun renderDocument(document: Document, onLinkPress: ((String) -> Unit)? = null): SpannableString {
916
val builder = SpannableStringBuilder()
17+
val currentStyle = requireNotNull(style) {
18+
"richTextStyle should always be provided from JS side with defaults."
19+
}
20+
val config = RendererConfig(currentStyle)
1021

11-
renderNode(document, builder, onLinkPress)
22+
renderNode(document, builder, onLinkPress, config)
1223

1324
return SpannableString(builder)
1425
}
1526

1627
private fun renderNode(
1728
node: Node,
1829
builder: SpannableStringBuilder,
19-
onLinkPress: ((String) -> Unit)? = null
30+
onLinkPress: ((String) -> Unit)? = null,
31+
config: RendererConfig
2032
) {
21-
val renderer = NodeRendererFactory.getRenderer(node)
33+
val renderer = NodeRendererFactory.getRenderer(node, config)
2234
renderer.render(node, builder, onLinkPress)
2335
}
2436
}

android/src/main/java/com/richtext/spans/RichTextHeadingSpan.kt

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,27 @@ package com.richtext.spans
22

33
import android.graphics.Typeface
44
import android.text.TextPaint
5-
import android.text.style.MetricAffectingSpan
5+
import android.text.style.AbsoluteSizeSpan
6+
import com.richtext.styles.RichTextStyle
67

78
class RichTextHeadingSpan(
8-
private val level: Int
9-
) : MetricAffectingSpan() {
9+
private val level: Int,
10+
private val style: RichTextStyle
11+
) : AbsoluteSizeSpan(style.getHeadingFontSize(level).toInt()) {
12+
13+
private val cachedTypeface: Typeface? by lazy {
14+
style.getHeadingFontFamily(level)?.let { fontFamily ->
15+
Typeface.create(fontFamily, Typeface.NORMAL)
16+
}
17+
}
1018

1119
override fun updateDrawState(tp: TextPaint) {
12-
tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
20+
super.updateDrawState(tp)
21+
cachedTypeface?.let { tp.typeface = it }
1322
}
1423

1524
override fun updateMeasureState(tp: TextPaint) {
16-
tp.typeface = Typeface.create(tp.typeface, Typeface.BOLD)
25+
super.updateMeasureState(tp)
26+
cachedTypeface?.let { tp.typeface = it }
1727
}
1828
}

android/src/main/java/com/richtext/spans/RichTextLinkSpan.kt

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@ import android.text.TextPaint
44
import android.text.style.ClickableSpan
55
import android.view.View
66
import com.richtext.RichTextView
7+
import com.richtext.styles.RichTextStyle
78

89
class RichTextLinkSpan(
910
private val url: String,
10-
private val onLinkPress: ((String) -> Unit)?
11+
private val onLinkPress: ((String) -> Unit)?,
12+
private val style: RichTextStyle
1113
) : ClickableSpan() {
1214

1315
override fun onClick(widget: View) {
@@ -21,7 +23,7 @@ class RichTextLinkSpan(
2123

2224
override fun updateDrawState(textPaint: TextPaint) {
2325
super.updateDrawState(textPaint)
24-
textPaint.isUnderlineText = true
25-
textPaint.color = textPaint.linkColor
26+
textPaint.color = style.getLinkColor()
27+
textPaint.isUnderlineText = style.getLinkUnderline()
2628
}
2729
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.richtext.styles
2+
3+
import com.facebook.react.bridge.ReadableMap
4+
import com.facebook.react.uimanager.PixelUtil
5+
6+
data class HeadingStyle(
7+
val fontSize: Float,
8+
val fontFamily: String?
9+
)
10+
11+
data class LinkStyle(
12+
val color: Int,
13+
val underline: Boolean
14+
)
15+
16+
class RichTextStyle(style: ReadableMap) {
17+
private val headingStyles = arrayOfNulls<HeadingStyle>(7)
18+
private lateinit var linkStyle: LinkStyle
19+
20+
init {
21+
parseStyles(style)
22+
}
23+
24+
fun getHeadingFontSize(level: Int): Float {
25+
return headingStyles[level]?.fontSize
26+
?: error("Heading style for level $level not found. JS should always provide defaults.")
27+
}
28+
29+
fun getHeadingFontFamily(level: Int): String? {
30+
return headingStyles[level]?.fontFamily
31+
}
32+
33+
fun getLinkColor(): Int {
34+
return linkStyle.color
35+
}
36+
37+
fun getLinkUnderline(): Boolean {
38+
return linkStyle.underline
39+
}
40+
41+
private fun parseStyles(style: ReadableMap) {
42+
(1..6).forEach { level ->
43+
val levelKey = "h$level"
44+
val levelStyle = requireNotNull(style.getMap(levelKey)) {
45+
"Style for $levelKey not found. JS should always provide defaults."
46+
}
47+
48+
val fontSize = PixelUtil.toPixelFromSP(levelStyle.getDouble("fontSize").toFloat())
49+
val fontFamily = levelStyle.getString("fontFamily")
50+
51+
headingStyles[level] = HeadingStyle(fontSize, fontFamily)
52+
}
53+
54+
val linkStyleMap = requireNotNull(style.getMap("link")) {
55+
"Link style not found. JS should always provide defaults."
56+
}
57+
58+
val color = linkStyleMap.getInt("color")
59+
val underline = linkStyleMap.getBoolean("underline")
60+
61+
linkStyle = LinkStyle(color, underline)
62+
}
63+
}
64+

0 commit comments

Comments
 (0)