Simple type to help implement expand and collapse behaviour for a textview.
Created
December 16, 2022 13:49
-
-
Save felipeslongo/100b51aaf8672de11969a479ed1b8ee1 to your computer and use it in GitHub Desktop.
ExpandableTextViewDelegate
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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