<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[MarkNjung'e]]></title><description><![CDATA[Android and backend developer from Nairobi, Kenya.]]></description><link>https://blog.marknjunge.com</link><generator>RSS for Node</generator><lastBuildDate>Sun, 24 May 2026 02:48:39 GMT</lastBuildDate><atom:link href="https://blog.marknjunge.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Spoiler text formatting on Android]]></title><description><![CDATA[Spoiler text formatting on Android


Inspired by Discord now having spoiler tags, I decided to look into how this can be done. I was able to achieve a similar effect using SpannableString.
You can find the final source code on my Github.
So let’s get...]]></description><link>https://blog.marknjunge.com/spoiler-text-formatting-on-android</link><guid isPermaLink="true">https://blog.marknjunge.com/spoiler-text-formatting-on-android</guid><category><![CDATA[Android]]></category><category><![CDATA[discord]]></category><dc:creator><![CDATA[MarkNjunge]]></dc:creator><pubDate>Tue, 05 Feb 2019 08:54:16 GMT</pubDate><content:encoded><![CDATA[<p><span class="w"></span></p>
<h1 id="spoiler-text-formatting-on-android">Spoiler text formatting on Android</h1>
<img alt="Image for post" src="https://miro.medium.com/max/2160/1*PGHo4a9r158C9xn3ZolofA.jpeg" />

<p>Inspired by <a target="_blank" href="https://youtu.be/teCVXzKRVFc">Discord</a> now having spoiler tags, I decided to look into how this can be done. I was able to achieve a similar effect using SpannableString.</p>
<p>You can find the final source code on my <a target="_blank" href="https://github.com/MarkNjunge/SpoilerText/blob/master/app/src/main/java/com/marknjunge/spoilertext/MainActivity.kt">Github</a>.</p>
<p>So let’s get started.</p>
<p><span class="hv fi bk hw hx hy"></span><span class="hv fi bk hw hx hy"></span><span class="hv fi bk hw hx"></span></p>
<p>The UI for the demo is pretty simple. An EditText, a Button and a TextView. When the Button is clicked, the text in the EditText is formatted and set in the TextView.</p>
<p><img src="https://miro.medium.com/max/60/1*T1XzQNcCXr_ascK7cpLdzg.jpeg?q=20" alt="Image for post" /></p>
<img alt="Image for post" src="https://miro.medium.com/max/2160/1*T1XzQNcCXr_ascK7cpLdzg.jpeg" />

<p>You will then need to choose what you want the spoiler tag to be. In my case, I chose the same as Discord; two pipe characters.</p>
<pre><code><span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> spoilerTag = <span class="hljs-string">"||"</span>&lt;/span&gt;
</code></pre><h1 id="detecting-the-spoiler-text">Detecting the spoiler text</h1>
<p>Code first, in case you want to copy-paste</p>
<p>F<span id="rmm">i</span>rst we remove the spoiler tags from the text and save it for later. This is the text we will apply the formatting to.</p>
<p>Since we want to support multiple spolier blocks, we will store the ranges in a list of pairs. The first int in the pair represents the start and the second represents the end.</p>
<p>Getting the start and end indices of the tags is pretty straightforward using <code>indexOf</code>.
For the end tag, we start from the index if the starting tag plus the length of the spoiler tag. Then we offset it by the length of the spoiler tag. This is because the index will be at the end of the tag, instead of the beginning.
Since we now have the start and the end, we can add them to the list.</p>
<p>We then need to remove the spoiler tags. We use <code>replaceRange</code> since <code>replace</code> will remove all instances while we only want to remove the ones we’ve accounted for.</p>
<p>Before removing trying to remove the end tag, we need to ensure that there actually is an end tag. This can occur if the text is badly formatted.
To do that, we check if the end index is less than the start.</p>
<p>Usually, the value returned when <code>indexOf</code> doesn’t find any occurrence is -1 but in our case it is less than that because we deducted the length of the spoiler tag.</p>
<h1 id="formatting-the-text">Formatting the text</h1>
<p>In order for ClickableSpan to work, we need to first add this</p>
<pre><code><span class="hljs-attr">textView._movementMethod_</span> = LinkMovementMethod.getInstance()&lt;/span&gt;
</code></pre><p>Then we call a function to create the SpannableString and update the text view. Into this function we pass the original text (the one without the tags) and the list of ranges.</p>
<pre><code>updateTextView(original, ranges)&lt;/span&gt;
</code></pre><p>We are wrapping this in a function because we will need to call it again when the user clicks on a block.</p>
<p>Again, code first</p>
<p>When setting the spans, we loop through the ranges so that each block is separate. This is so that the user can click each block to reveal it’s contents, instead of all at once.</p>
<p>For the hidden effect, we use a <code>BackgroundColorSpan</code>. You also need to set a <code>ForegroundColorSpan</code> so that the text color is the same as the background, otherwise the text will still be visible.</p>
<p>For the text to be clickable, we use a <code>ClickableSpan</code>.
Within the <code>onClick</code>, we remove the current range from the list then call the function again with the “new” list and the original text.</p>
<p><span class="hv fi bk hw hx hy"></span><span class="hv fi bk hw hx hy"></span><span class="hv fi bk hw hx"></span></p>
<p>That’s it. Now we have a working spoiler textview.</p>
<p><img src="https://miro.medium.com/freeze/max/34/1*2K25oX8QC0a75QmRaKxVCA.gif?q=20" alt="Image for post" /></p>
<img alt="Image for post" src="https://miro.medium.com/max/720/1*2K25oX8QC0a75QmRaKxVCA.gif" />]]></content:encoded></item></channel></rss>