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
+
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
+ }
0 commit comments