11package com.swmansion.enriched.markdown.utils
22
3+ import android.text.Spannable
4+ import android.text.SpannableStringBuilder
35import android.text.Spanned
6+ import android.text.style.ForegroundColorSpan
7+ import android.text.style.StrikethroughSpan
48import android.widget.TextView
9+ import com.swmansion.enriched.markdown.renderer.SpanStyleCache
10+ import com.swmansion.enriched.markdown.spans.BaseListSpan
511import com.swmansion.enriched.markdown.spans.TaskListSpan
12+ import com.swmansion.enriched.markdown.styles.StyleConfig
13+ import com.swmansion.enriched.markdown.utils.SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE
614
715data class TaskListHitTestResult (
816 val taskIndex : Int ,
@@ -23,7 +31,8 @@ object TaskListToggleUtils {
2331
2432 val match = matches[index]
2533 val prefix = match.groupValues[1 ]
26- val replacement = " $prefix [${if (checked) " " else " x" } ]"
34+
35+ val replacement = " $prefix [${if (checked) " x" else " " } ]"
2736
2837 return markdown.replaceRange(match.range, replacement)
2938 }
@@ -79,4 +88,162 @@ object TaskListTapUtils {
7988 itemText = itemText,
8089 )
8190 }
91+
92+ fun updateTaskListItemCheckedState (
93+ textView : TextView ,
94+ targetIndex : Int ,
95+ newChecked : Boolean ,
96+ styleConfig : StyleConfig ,
97+ ): Boolean {
98+ val text = textView.text
99+ if (text !is Spannable ) {
100+ return false
101+ }
102+
103+ val spannable = SpannableStringBuilder (text)
104+ val taskSpans = spannable.getSpans(0 , spannable.length, TaskListSpan ::class .java)
105+
106+ val targetSpan = taskSpans.firstOrNull { it.taskIndex == targetIndex }
107+ if (targetSpan == null ) {
108+ return false
109+ }
110+
111+ if (targetSpan.isChecked == newChecked) {
112+ return true
113+ }
114+
115+ val spanStart = spannable.getSpanStart(targetSpan)
116+ val spanEnd = spannable.getSpanEnd(targetSpan)
117+ val itemDepth = targetSpan.depth
118+
119+ val styleCache = SpanStyleCache (styleConfig)
120+ val newTaskSpan =
121+ TaskListSpan (
122+ taskStyle = styleConfig.taskListStyle,
123+ listStyle = styleConfig.listStyle,
124+ depth = itemDepth,
125+ context = textView.context,
126+ styleCache = styleCache,
127+ taskIndex = targetIndex,
128+ isChecked = newChecked,
129+ )
130+
131+ spannable.removeSpan(targetSpan)
132+ spannable.setSpan(newTaskSpan, spanStart, spanEnd, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE )
133+
134+ val verifySpans = spannable.getSpans(spanStart, spanEnd, TaskListSpan ::class .java)
135+ val verifySpan = verifySpans.firstOrNull { it.taskIndex == targetIndex }
136+ if (verifySpan == null || verifySpan.isChecked != newChecked) {
137+ return false
138+ }
139+
140+ val taskStyle = styleConfig.taskListStyle
141+ val checkedTextColor = taskStyle.checkedTextColor
142+ val strikethrough = taskStyle.checkedStrikethrough
143+
144+ val excludedRanges =
145+ spannable
146+ .getSpans(spanStart, spanEnd, BaseListSpan ::class .java)
147+ .filter { it.depth > itemDepth }
148+ .map { spannable.getSpanStart(it) to spannable.getSpanEnd(it) }
149+ .sortedBy { it.first }
150+
151+ applyDecorationsToRanges(
152+ spannable = spannable,
153+ spanStart = spanStart,
154+ spanEnd = spanEnd,
155+ excludedRanges = excludedRanges,
156+ isChecked = newChecked,
157+ checkedTextColor = checkedTextColor,
158+ strikethrough = strikethrough,
159+ styleConfig = styleConfig,
160+ )
161+
162+ val imageSpans = text.getSpans(0 , text.length, com.swmansion.enriched.markdown.spans.ImageSpan ::class .java).toList()
163+ val originalSpanStarts = imageSpans.associateWith { text.getSpanStart(it) }
164+
165+ textView.text = spannable
166+
167+ val newImageSpans = spannable.getSpans(0 , spannable.length, com.swmansion.enriched.markdown.spans.ImageSpan ::class .java)
168+ imageSpans.forEach { originalSpan ->
169+ val matchingSpan =
170+ newImageSpans.firstOrNull {
171+ it.imageUrl == originalSpan.imageUrl &&
172+ spannable.getSpanStart(it) == originalSpanStarts[originalSpan]
173+ }
174+ (matchingSpan ? : originalSpan).registerTextView(textView)
175+ }
176+
177+ textView.invalidate()
178+
179+ return true
180+ }
181+
182+ private fun applyDecorationsToRanges (
183+ spannable : Spannable ,
184+ spanStart : Int ,
185+ spanEnd : Int ,
186+ excludedRanges : List <Pair <Int , Int >>,
187+ isChecked : Boolean ,
188+ checkedTextColor : Int ,
189+ strikethrough : Boolean ,
190+ styleConfig : StyleConfig ,
191+ ) {
192+ var currentPos = spanStart
193+ for ((start, end) in excludedRanges) {
194+ if (start > currentPos) {
195+ if (isChecked) {
196+ applyCheckedSpans(spannable, currentPos, start, checkedTextColor, strikethrough)
197+ } else {
198+ removeCheckedSpans(spannable, currentPos, start, styleConfig)
199+ }
200+ }
201+ currentPos = maxOf(currentPos, end)
202+ }
203+ if (currentPos < spanEnd) {
204+ if (isChecked) {
205+ applyCheckedSpans(spannable, currentPos, spanEnd, checkedTextColor, strikethrough)
206+ } else {
207+ removeCheckedSpans(spannable, currentPos, spanEnd, styleConfig)
208+ }
209+ }
210+ }
211+
212+ private fun applyCheckedSpans (
213+ spannable : Spannable ,
214+ start : Int ,
215+ end : Int ,
216+ color : Int ,
217+ strikethrough : Boolean ,
218+ ) {
219+ if (color != 0 ) {
220+ spannable.setSpan(ForegroundColorSpan (color), start, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE )
221+ }
222+ if (strikethrough) {
223+ spannable.setSpan(StrikethroughSpan (), start, end, SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE )
224+ }
225+ }
226+
227+ private fun removeCheckedSpans (
228+ spannable : Spannable ,
229+ start : Int ,
230+ end : Int ,
231+ styleConfig : StyleConfig ,
232+ ) {
233+ val strikethroughSpans = spannable.getSpans(start, end, StrikethroughSpan ::class .java)
234+ strikethroughSpans.forEach { spannable.removeSpan(it) }
235+
236+ val colorSpans = spannable.getSpans(start, end, ForegroundColorSpan ::class .java)
237+ colorSpans.forEach { spannable.removeSpan(it) }
238+
239+ val listStyleColor = styleConfig.listStyle.color
240+ if (listStyleColor != 0 ) {
241+ spannable.setSpan(
242+ ForegroundColorSpan (listStyleColor),
243+ start,
244+ end,
245+ Spannable .SPAN_EXCLUSIVE_EXCLUSIVE ,
246+ )
247+ }
248+ }
82249}
0 commit comments