-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathBlockquoteSpan.kt
More file actions
152 lines (132 loc) · 4.65 KB
/
BlockquoteSpan.kt
File metadata and controls
152 lines (132 loc) · 4.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package com.swmansion.enriched.markdown.spans
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Typeface
import android.text.Layout
import android.text.Spanned
import android.text.TextPaint
import android.text.style.LeadingMarginSpan
import android.text.style.LineBackgroundSpan
import android.text.style.MetricAffectingSpan
import com.swmansion.enriched.markdown.renderer.BlockStyle
import com.swmansion.enriched.markdown.renderer.SpanStyleCache
import com.swmansion.enriched.markdown.styles.BlockquoteStyle
import com.swmansion.enriched.markdown.utils.text.extensions.applyBlockStyleFont
import com.swmansion.enriched.markdown.utils.text.extensions.applyColorPreserving
class BlockquoteSpan(
private val blockquoteStyle: BlockquoteStyle,
val depth: Int,
private val context: Context,
private val styleCache: SpanStyleCache,
) : MetricAffectingSpan(),
LeadingMarginSpan,
LineBackgroundSpan {
private val levelSpacing: Float = blockquoteStyle.borderWidth + blockquoteStyle.gapWidth
private val blockStyle =
BlockStyle(
fontSize = blockquoteStyle.fontSize,
fontFamily = blockquoteStyle.fontFamily,
fontWeight = blockquoteStyle.fontWeight,
color = blockquoteStyle.color,
)
// Cache for shouldSkipDrawing to avoid repeated getSpans() calls during draw passes
private var cachedText: CharSequence? = null
private var cachedMaxDepthByPosition = mutableMapOf<Int, Int>()
override fun updateMeasureState(tp: TextPaint) = applyTextStyle(tp)
override fun updateDrawState(tp: TextPaint) = applyTextStyle(tp)
override fun getLeadingMargin(first: Boolean): Int = levelSpacing.toInt()
override fun drawLeadingMargin(
c: Canvas,
p: Paint,
x: Int,
dir: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence?,
start: Int,
end: Int,
first: Boolean,
layout: Layout?,
) {
// Essential check from original: only the deepest span draws to prevent over-rendering background
if (shouldSkipDrawing(text, start)) return
val borderPaint = configureBorderPaint()
val borderTop = top.toFloat()
val borderBottom = bottom.toFloat()
for (level in 0..depth) {
val borderX = x + (levelSpacing * level * dir)
val borderRight = borderX + (blockquoteStyle.borderWidth * dir)
c.drawRect(minOf(borderX, borderRight), borderTop, maxOf(borderX, borderRight), borderBottom, borderPaint)
}
}
override fun drawBackground(
canvas: Canvas,
paint: Paint,
left: Int,
right: Int,
top: Int,
baseline: Int,
bottom: Int,
text: CharSequence,
start: Int,
end: Int,
lineNum: Int,
) {
if (shouldSkipDrawing(text, start)) return
drawBackground(canvas, left, top, bottom, right)
}
@SuppressLint("WrongConstant") // Result of mask is always valid: 0, 1, 2, or 3
private fun applyTextStyle(tp: TextPaint) {
tp.textSize = blockStyle.fontSize
val preserved = (tp.typeface?.style ?: 0) and BOLD_ITALIC_MASK
tp.applyBlockStyleFont(blockStyle, context)
if (preserved != 0) {
tp.typeface = Typeface.create(tp.typeface ?: Typeface.DEFAULT, preserved)
}
tp.applyColorPreserving(blockStyle.color, *styleCache.colorsToPreserve)
}
companion object {
private const val BOLD_ITALIC_MASK = Typeface.BOLD or Typeface.ITALIC
private val sharedBorderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
private val sharedBackgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
}
private fun configureBorderPaint(): Paint =
sharedBorderPaint.apply {
color = blockquoteStyle.borderColor
}
private fun configureBackgroundPaint(bgColor: Int): Paint =
sharedBackgroundPaint.apply {
color = bgColor
}
private fun shouldSkipDrawing(
text: CharSequence?,
start: Int,
): Boolean {
if (text !is Spanned) return false
if (cachedText !== text) {
cachedText = text
cachedMaxDepthByPosition.clear()
}
val maxDepth =
cachedMaxDepthByPosition.getOrPut(start) {
val spans = text.getSpans(start, start + 1, BlockquoteSpan::class.java)
spans.maxOfOrNull { it.depth } ?: -1
}
return maxDepth > depth
}
private fun drawBackground(
c: Canvas,
left: Int,
top: Int,
bottom: Int,
right: Int,
) {
val bgColor = blockquoteStyle.backgroundColor?.takeIf { it != Color.TRANSPARENT } ?: return
val backgroundPaint = configureBackgroundPaint(bgColor)
c.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), backgroundPaint)
}
}