Skip to content

Instantly share code, notes, and snippets.

@felipeslongo
Created December 16, 2022 13:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save felipeslongo/100b51aaf8672de11969a479ed1b8ee1 to your computer and use it in GitHub Desktop.
Save felipeslongo/100b51aaf8672de11969a479ed1b8ee1 to your computer and use it in GitHub Desktop.
ExpandableTextViewDelegate

Simple type to help implement expand and collapse behaviour for a textview.

package myapp
import android.text.Layout
import android.util.Log
import android.widget.TextView
import myapp.ExpandableTextViewDelegate.State.*
import kotlin.properties.Delegates
private const val TAG = "ExpandableTextViewDel"
//https://stackoverflow.com/questions/31668697/android-expandable-text-view-with-view-more-button-displaying-at-center-after
/**
* Type responsible for handling a [TextView] that can be expanded and collapsed.
* Asserting the state and behaviour can be delegated to this type.
* This pattern is know as "Read More" to show all text.
* @param textView [TextView] that implements read more behaviour.
* @param collapsedStateMaxLines threshold for the collapsed,
* after this a read more action button should be displayed so user can trigger expand.
* @param onStateChanged callback when the state of the textview behaviour
*/
class ExpandableTextViewDelegate(
private val textView: TextView,
private val collapsedStateMaxLines: Int = textView.maxLines,
private val onStateChanged: (State) -> Unit
) {
private val expandedStateMaxLine = Int.MAX_VALUE
var state: State by Delegates.observable(AWAITING_LAYOUT_PASS) { _, _, newState ->
Log.v(TAG,"state=$state")
onStateChanged(newState)
}
fun setText(value: CharSequence) {
textView.text = value
awaitLayoutPassAndUpdateState()
}
fun toggle() {
when (state) {
AWAITING_LAYOUT_PASS -> { }
WRAPPED -> { }
COLLAPSED -> { expand() }
EXPANDED -> { collapse() }
}
}
@Suppress("MemberVisibilityCanBePrivate")
fun expand() {
textView.maxLines = expandedStateMaxLine
awaitLayoutPassAndUpdateState()
}
fun collapse() {
textView.maxLines = collapsedStateMaxLines
awaitLayoutPassAndUpdateState()
}
private fun awaitLayoutPassAndUpdateState() {
onStateChanged(AWAITING_LAYOUT_PASS)
textView.post {
state = getTextViewState()
}
}
private fun getTextViewState(): State = with(textView) {
val layout = textView.layout ?: return AWAITING_LAYOUT_PASS
val lines = layout.lineCount
Log.d(TAG,"lines=$lines")
return when {
lines > collapsedStateMaxLines -> EXPANDED
lines < collapsedStateMaxLines -> WRAPPED
layout.isEllipsized() -> COLLAPSED
else -> WRAPPED
}
}
// https://stackoverflow.com/questions/4005933/how-do-i-tell-if-my-textview-has-been-ellipsized
// https://stackoverflow.com/questions/15567235/check-if-textview-is-ellipsized-in-android?noredirect=1&lq=1
private fun Layout.isEllipsized() = getEllipsisCount(lineCount - 1) > 0
/**
* State of the [TextView] regarding expand and collapse behaviour.
*/
enum class State {
/**
* Awaiting a new Layout Pass for the [TextView] before asserting the next state.
* This state is triggered when text is changed.
*/
AWAITING_LAYOUT_PASS,
/**
* Text is completely wrapped by the [TextView].
* Don't need to show Expand Action.
*/
WRAPPED,
/**
* Text is truncated/ellipsized.
* Need to show Expand Action.
*/
COLLAPSED,
/**
* Text is expanded an completely visible.
* Need to show Collapse Action.
*/
EXPANDED
}
}
inner class ViewHolder(
override val containerView: View
) : RecyclerView.ViewHolder(containerView), LayoutContainer {
private val readMoreDelegate = ExpandableTextViewDelegate(
textView = tv_review_body,
onStateChanged = ::onReadMoreStateChanged
)
init {
tv_read_toggle.setOnClickListener {
readMoreDelegate.toggle()
}
}
fun bind(model: Model) {
readMoreDelegate.collapse()
readMoreDelegate.setText(model.text)
}
private fun onReadMoreStateChanged(state: ExpandableTextViewDelegate.State) {
when (state) {
ExpandableTextViewDelegate.State.AWAITING_LAYOUT_PASS -> {
tv_read_toggle.isGone = true
}
ExpandableTextViewDelegate.State.WRAPPED -> {
tv_read_toggle.isGone = true
}
ExpandableTextViewDelegate.State.COLLAPSED -> {
tv_read_toggle.text = "Read More"
tv_read_toggle.isVisible = true
}
ExpandableTextViewDelegate.State.EXPANDED -> {
tv_read_toggle.text = "Read Less"
tv_read_toggle.isVisible = true
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment