Skip to content

Commit 4a99613

Browse files
committed
update: add Read More Text View
1 parent 28cb4e9 commit 4a99613

File tree

7 files changed

+306
-5
lines changed

7 files changed

+306
-5
lines changed

app/src/main/java/com/qomunal/opensource/androidresearch/ui/main/MainActivity.kt

+7-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.qomunal.opensource.androidresearch.ui.main
22

33
import android.os.Bundle
44
import androidx.activity.viewModels
5+
import com.qomunal.opensource.androidresearch.R
56
import com.qomunal.opensource.androidresearch.common.base.BaseActivity
67
import com.qomunal.opensource.androidresearch.common.ext.showToast
78
import com.qomunal.opensource.androidresearch.databinding.ActivityMainBinding
@@ -23,8 +24,12 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
2324

2425
override fun initUI() {
2526
binding.apply {
26-
btnTest.setOnClickListener {
27-
showToast("Yes u click on me")
27+
tvReadMore.apply {
28+
setCollapsedText("More")
29+
setExpandedText("Less")
30+
setCollapsedTextColor(R.color.more)
31+
setExpandedTextColor(R.color.less)
32+
setTrimLines(4)
2833
}
2934
}
3035
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
package com.qomunal.opensource.androidresearch.widget
2+
3+
import android.content.Context
4+
import android.graphics.Color
5+
import android.text.SpannableStringBuilder
6+
import android.text.Spanned
7+
import android.text.TextPaint
8+
import android.text.method.LinkMovementMethod
9+
import android.text.style.ClickableSpan
10+
import android.util.AttributeSet
11+
import android.util.Log
12+
import android.view.View
13+
import android.view.ViewTreeObserver
14+
import androidx.appcompat.widget.AppCompatTextView
15+
import androidx.core.content.ContextCompat
16+
import com.qomunal.opensource.androidresearch.R
17+
18+
/**
19+
* Created by faisalamircs on 06/03/2025
20+
* -----------------------------------------
21+
* Name : Muhammad Faisal Amir
22+
* E-mail : [email protected]
23+
* Github : github.com/amirisback
24+
* -----------------------------------------
25+
*/
26+
27+
28+
class ReadMoreTextView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
29+
AppCompatTextView(context, attrs) {
30+
private var text: CharSequence? = null
31+
private var bufferType: BufferType? = null
32+
private var readMore = true
33+
private var trimLength: Int
34+
private var trimCollapsedText: CharSequence
35+
private var trimExpandedText: CharSequence
36+
private val viewMoreSpan: ReadMoreClickableSpan
37+
private var colorClickableText: Int
38+
private var lessTextColor: Int? = null
39+
private var moreTextColor: Int? = null
40+
private val showTrimExpandedText: Boolean
41+
private var trimMode: Int
42+
private var lineEndIndex = 0
43+
private var trimLines: Int
44+
45+
init {
46+
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ReadMoreTextView)
47+
trimLength = typedArray.getInt(R.styleable.ReadMoreTextView_trimLength, DEFAULT_TRIM_LENGTH)
48+
val resourceIdTrimCollapsedText = typedArray.getResourceId(
49+
R.styleable.ReadMoreTextView_trimCollapsedText,
50+
R.string.read_more
51+
)
52+
val resourceIdTrimExpandedText = typedArray.getResourceId(
53+
R.styleable.ReadMoreTextView_trimExpandedText,
54+
R.string.read_less
55+
)
56+
trimCollapsedText = resources.getString(resourceIdTrimCollapsedText)
57+
trimExpandedText = resources.getString(resourceIdTrimExpandedText)
58+
trimLines = typedArray.getInt(R.styleable.ReadMoreTextView_trimLines, DEFAULT_TRIM_LINES)
59+
colorClickableText = typedArray.getColor(
60+
R.styleable.ReadMoreTextView_colorClickableText,
61+
ContextCompat.getColor(context, R.color.show_more)
62+
)
63+
showTrimExpandedText = typedArray.getBoolean(
64+
R.styleable.ReadMoreTextView_showTrimExpandedText,
65+
DEFAULT_SHOW_TRIM_EXPANDED_TEXT
66+
)
67+
trimMode = typedArray.getInt(R.styleable.ReadMoreTextView_trimMode, TRIM_MODE_LINES)
68+
typedArray.recycle()
69+
viewMoreSpan = ReadMoreClickableSpan()
70+
onGlobalLayoutLineEndIndex()
71+
setText()
72+
}
73+
74+
private fun setText() {
75+
super.setText(displayableText, bufferType)
76+
movementMethod = LinkMovementMethod.getInstance()
77+
highlightColor = Color.TRANSPARENT
78+
}
79+
80+
private val displayableText: CharSequence?
81+
get() = getTrimmedText(text)
82+
83+
override fun setText(text: CharSequence, type: BufferType) {
84+
this.text = text
85+
bufferType = type
86+
setText()
87+
}
88+
89+
private fun getTrimmedText(text: CharSequence?): CharSequence? {
90+
if (trimMode == TRIM_MODE_LENGTH) {
91+
if (text != null && text.length > trimLength) {
92+
return if (readMore) {
93+
updateCollapsedText()
94+
} else {
95+
updateExpandedText()
96+
}
97+
}
98+
}
99+
if (trimMode == TRIM_MODE_LINES) {
100+
if (text != null && lineEndIndex > 0) {
101+
if (readMore) {
102+
if (layout.lineCount > trimLines) {
103+
lessTextColor?.let {
104+
Log.e("getTrimmedText", ": $it ${R.color.show_less}")
105+
setColorClickableText(it)
106+
}
107+
return updateCollapsedText()
108+
}
109+
} else {
110+
moreTextColor?.let {
111+
setColorClickableText(it)
112+
}
113+
return updateExpandedText()
114+
}
115+
}
116+
}
117+
return text
118+
}
119+
120+
private fun updateCollapsedText(): CharSequence {
121+
var trimEndIndex = text!!.length
122+
when (trimMode) {
123+
TRIM_MODE_LINES -> {
124+
trimEndIndex = lineEndIndex - (ELLIPSIZE.length + trimCollapsedText.length + 1)
125+
if (trimEndIndex < 0) {
126+
trimEndIndex = trimLength + 1
127+
}
128+
}
129+
130+
TRIM_MODE_LENGTH -> trimEndIndex = trimLength + 1
131+
}
132+
val s = SpannableStringBuilder(text, 0, trimEndIndex)
133+
.append(ELLIPSIZE)
134+
.append(trimCollapsedText)
135+
return addClickableSpan(s, trimCollapsedText)
136+
}
137+
138+
private fun updateExpandedText(): CharSequence? {
139+
if (showTrimExpandedText) {
140+
val s = SpannableStringBuilder(
141+
text,
142+
0,
143+
text!!.length
144+
).append(context.getString(R.string.space)).append(trimExpandedText)
145+
return addClickableSpan(s, trimExpandedText)
146+
}
147+
return text
148+
}
149+
150+
private fun addClickableSpan(s: SpannableStringBuilder, trimText: CharSequence): CharSequence {
151+
s.setSpan(
152+
viewMoreSpan,
153+
s.length - trimText.length,
154+
s.length,
155+
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
156+
)
157+
return s
158+
}
159+
160+
fun setTrimLength(trimLength: Int) {
161+
this.trimLength = trimLength
162+
setText()
163+
}
164+
165+
private fun setColorClickableText(colorClickableText: Int) {
166+
try {
167+
this.colorClickableText = ContextCompat.getColor(context, colorClickableText)
168+
} catch (e: Exception) {
169+
e.printStackTrace()
170+
}
171+
}
172+
173+
fun setExpandedTextColor(moreTextColor: Int) {
174+
this.moreTextColor = moreTextColor
175+
}
176+
177+
fun setCollapsedTextColor(lessTextColor: Int) {
178+
this.lessTextColor = lessTextColor
179+
}
180+
181+
fun setCollapsedText(trimCollapsedText: CharSequence) {
182+
this.trimCollapsedText = trimCollapsedText
183+
}
184+
185+
fun setExpandedText(trimExpandedText: CharSequence) {
186+
this.trimExpandedText = trimExpandedText
187+
}
188+
189+
fun setTrimMode(trimMode: Int) {
190+
this.trimMode = trimMode
191+
}
192+
193+
fun setTrimLines(trimLines: Int) {
194+
this.trimLines = trimLines
195+
}
196+
197+
private inner class ReadMoreClickableSpan : ClickableSpan() {
198+
override fun onClick(widget: View) {
199+
readMore = !readMore
200+
setText()
201+
}
202+
203+
override fun updateDrawState(ds: TextPaint) {
204+
ds.color = colorClickableText
205+
}
206+
}
207+
208+
private fun onGlobalLayoutLineEndIndex() {
209+
if (trimMode == TRIM_MODE_LINES) {
210+
viewTreeObserver.addOnGlobalLayoutListener(object :
211+
ViewTreeObserver.OnGlobalLayoutListener {
212+
override fun onGlobalLayout() {
213+
val obs = viewTreeObserver
214+
obs.removeOnGlobalLayoutListener(this)
215+
refreshLineEndIndex()
216+
setText()
217+
}
218+
})
219+
}
220+
}
221+
222+
private fun refreshLineEndIndex() {
223+
try {
224+
lineEndIndex = when (trimLines) {
225+
0 -> {
226+
layout.getLineEnd(0)
227+
}
228+
229+
in 1..lineCount -> {
230+
layout.getLineEnd(trimLines - 1)
231+
}
232+
233+
else -> {
234+
INVALID_END_INDEX
235+
}
236+
}
237+
} catch (e: Exception) {
238+
e.printStackTrace()
239+
}
240+
}
241+
242+
companion object {
243+
private const val TRIM_MODE_LINES = 0
244+
private const val TRIM_MODE_LENGTH = 1
245+
private const val DEFAULT_TRIM_LENGTH = 240
246+
private const val DEFAULT_TRIM_LINES = 2
247+
private const val INVALID_END_INDEX = -1
248+
private const val DEFAULT_SHOW_TRIM_EXPANDED_TEXT = true
249+
private const val ELLIPSIZE = "... "
250+
}
251+
252+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.qomunal.opensource.androidresearch.widget
2+
3+
/**
4+
* Created by faisalamircs on 06/03/2025
5+
* -----------------------------------------
6+
* Name : Muhammad Faisal Amir
7+
* E-mail : [email protected]
8+
* Github : github.com/amirisback
9+
* -----------------------------------------
10+
*/
11+
12+
13+
interface TextViewClickListener {
14+
fun delegate(expended:Boolean)
15+
}

app/src/main/res/layout/activity_main.xml

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
android:layout_width="match_parent"
55
android:layout_height="match_parent">
66

7-
<Button
8-
android:id="@+id/btn_test"
7+
<com.qomunal.opensource.androidresearch.widget.ReadMoreTextView
8+
android:id="@+id/tv_read_more"
99
android:layout_width="wrap_content"
1010
android:layout_height="wrap_content"
11-
android:text="Click On Me"
11+
android:text="@string/dumyy"
1212
app:layout_constraintBottom_toBottomOf="parent"
1313
app:layout_constraintEnd_toEndOf="parent"
1414
app:layout_constraintStart_toStartOf="parent"

app/src/main/res/values/attrs.xml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<resources>
3+
<!-- Read More TextView-->
4+
<declare-styleable name="ReadMoreTextView">
5+
<attr name="trimExpandedText" format="string" />
6+
<attr name="trimCollapsedText" format="string" />
7+
<attr name="trimLength" format="integer" />
8+
<attr name="showTrimExpandedText" format="boolean" />
9+
<attr name="colorClickableText" format="color" />
10+
<attr name="trimLines" format="integer" />
11+
<attr name="trimMode">
12+
<enum name="trimModeLine" value="0" />
13+
<enum name="trimModeLength" value="1" />
14+
</attr>
15+
</declare-styleable>
16+
<!-- Read More TextView-->
17+
</resources>

app/src/main/res/values/colors.xml

+6
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,10 @@
22
<resources>
33
<color name="black">#FF000000</color>
44
<color name="white">#FFFFFFFF</color>
5+
6+
<color name="show_more">#FF6200EE</color>
7+
<color name="show_less">#FFBB86FC</color>
8+
9+
<color name="more">#FFC107</color>
10+
<color name="less">#E91E63</color>
511
</resources>

app/src/main/res/values/strings.xml

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
<resources>
22
<string name="app_name">AndroidResearch</string>
3+
4+
<string name="read_more">show more</string>
5+
<string name="read_less">show less</string>
6+
<string name="space">" "</string>
7+
<string name="dumyy">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry\'s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum</string>
8+
39
</resources>

0 commit comments

Comments
 (0)