Skip to content

Instantly share code, notes, and snippets.

@nikmax42
Created May 1, 2025 14:56
Show Gist options
  • Save nikmax42/d58bd3853da446031f6360ce8aa407ac to your computer and use it in GitHub Desktop.
Save nikmax42/d58bd3853da446031f6360ce8aa407ac to your computer and use it in GitHub Desktop.
package attracktorui.core.components
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.VisibilityThreshold
import androidx.compose.animation.core.spring
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.TextUnit
/*
* Credits:
* https://medium.com/@munbonecci/how-to-implement-expandable-text-in-jetpack-compose-ca9ba35b645c
* */
const val DEFAULT_MAX_LINES = 5
/**
* An expandable text component that provides access to truncated text with a dynamic ... Show More/ Show Less button.
*
* @param modifier Modifier for the Box containing the text.
* @param modifier Modifier for the Text composable.
* @param style The TextStyle to apply to the text.
* @param fontStyle The FontStyle to apply to the text.
* @param text The text to be displayed.
* @param collapsedMaxLines The maximum number of lines to display when collapsed.
* @param showMoreText The text to display for "... Show More" button.
* @param showMoreStyle The SpanStyle for "... Show More" button.
* @param showLessText The text to display for "Show Less" button.
* @param showLessStyle The SpanStyle for "Show Less" button.
* @param textAlign The alignment of the text.
* @param fontSize The font size of the text.
*/
@Composable
fun ExpandableText(
text: String,
showMoreText: String,
showLessText: String,
modifier: Modifier = Modifier,
collapsedMaxLines: Int = DEFAULT_MAX_LINES,
showMoreStyle: SpanStyle = SpanStyle(fontWeight = FontWeight.W500),
showLessStyle: SpanStyle = showMoreStyle,
style: TextStyle = LocalTextStyle.current,
fontStyle: FontStyle? = null,
fontSize: TextUnit = TextUnit.Unspecified,
textAlign: TextAlign? = null,
animationSpec: FiniteAnimationSpec<IntSize> = spring(
stiffness = Spring.StiffnessMediumLow,
visibilityThreshold = IntSize.VisibilityThreshold
)
) {
// State variables to track the expanded state, clickable state, and last character index.
var isExpanded by remember { mutableStateOf(false) }
var isClickable by remember { mutableStateOf(false) }
var lastCharIndex by remember { mutableIntStateOf(0) }
// Box composable containing the Text composable.
Box(
modifier = Modifier
.clickable(
enabled = isClickable,
indication = null,
interactionSource = null
) {
isExpanded = !isExpanded
}
) {
// Text composable with buildAnnotatedString to handle "Show More" and "Show Less" buttons.
Text(
modifier = modifier
.fillMaxWidth()
.animateContentSize(animationSpec),
text = buildAnnotatedString {
if (isClickable) {
if (isExpanded) {
// Display the full text and "Show Less" button when expanded.
append(text)
withStyle(style = showLessStyle) { append(showLessText) }
}
else {
// Display truncated text and "Show More" button when collapsed.
val adjustText = text.substring(startIndex = 0, endIndex = lastCharIndex)
.dropLast(showMoreText.length)
.dropLastWhile { Character.isWhitespace(it) || it == '.' }
append(adjustText)
withStyle(style = showMoreStyle) { append(showMoreText) }
}
}
else {
// Display the full text when not clickable.
append(text)
}
},
// Set max lines based on the expanded state.
maxLines = if (isExpanded) Int.MAX_VALUE else collapsedMaxLines,
fontStyle = fontStyle,
// Callback to determine visual overflow and enable click ability.
onTextLayout = { textLayoutResult ->
if (!isExpanded && textLayoutResult.hasVisualOverflow) {
isClickable = true
lastCharIndex = textLayoutResult.getLineEnd(collapsedMaxLines - 1)
}
},
style = style,
textAlign = textAlign,
fontSize = fontSize
)
}
}
@Preview
@Composable
private fun Preview() {
val text = remember {
"Text text text text text text text text text text text text " +
"text text text text text text text text text text text text " +
"text text text text text text text text text text text text " +
"text text text text text text text text text text text text " +
"text text text text text text text text text text text text " +
"text text text text text text text text text text text text " +
"text text text text text text text text text text text text " +
"text text text text text text text text text text text text " +
"text text text text text text text text text text text text"
}
Surface(Modifier.fillMaxSize()) {
ExpandableText(
text = text,
showMoreText = "... Show More",
showLessText = " Show less"
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment