How to animate TextStyle in Jetpack Compose? - android

The text in one of my Composables is struck-through when a certain boolean variable is true. How can I animate this change in TextStyle on recomposition so that the line fades in rather than appearing and disappearing abruptly?
#Composable
fun MyComposable(
completed: Boolean
) {
val textStyle = TextStyle(textDecoration = if (completed) TextDecoration.LineThrough else null)
Text(
text = title,
color = textColor,
style = textStyle,
modifier = Modifier.align(Alignment.CenterVertically)
)

Not sure if it exists a way to animate a TextStyle.
Not a great solution, but just a workaround:
Box() {
AnimatedVisibility(
visible = !completed,
enter = fadeIn(
animationSpec = tween(durationMillis = duration)
),
exit = fadeOut(
animationSpec = tween(durationMillis = duration)
)) {
Text(
text = title,
style = TextStyle(textDecoration=null)
)
}
AnimatedVisibility(
visible = completed,
enter = fadeIn(
animationSpec = tween(durationMillis = duration)
),
exit = fadeOut(
animationSpec = tween(durationMillis = duration)
)) {
Text(
text = title,
style = TextStyle(textDecoration = TextDecoration.LineThrough),
)
}
}

Gabriele's answer is also a decent workaround, but perhaps a simpler one would be to put the Text and the "Stroke", in a box, overlapping. Say,
#Composable
fun MyComposable(
completed: Boolean
) {
Box{
Text(
text = title,
color = textColor,
style = textStyle,
modifier = Modifier.align(Alignment.CenterVertically)
)
AnimatedVisibility (completed) {
Box(Modifier.width(1.dp)){
//Empty Box of unit width to serve as the stroke
// Add background modifiers to match the font color
}
}
}

Related

Jetpack Compose: How to create multi-item FAB with animation?

I'm trying to create a multi-item floating action button with the following animation:
I created a multi-item floating action button but I could not implement the intended animation.
I have FilterFabMenuButton composable that I show as a menu item :
FilterFabMenuButton
#Composable
fun FilterFabMenuButton(
item: FilterFabMenuItem,
onClick: (FilterFabMenuItem) -> Unit,
modifier: Modifier = Modifier
) {
FloatingActionButton(
modifier = modifier,
onClick = {
onClick(item)
},
backgroundColor = colorResource(
id = R.color.primary_color
)
) {
Icon(
painter = painterResource(item.icon), contentDescription = null, tint = colorResource(
id = R.color.white
)
)
}
}
I have FilterFabMenuLabel composable which is a label for FilterFabMenuButton:
FilterFabMenuLabel
#Composable
fun FilterFabMenuLabel(
label: String,
modifier: Modifier = Modifier
) {
Surface(
modifier = modifier,
shape = RoundedCornerShape(6.dp),
color = Color.Black.copy(alpha = 0.8f)
) {
Text(
text = label, color = Color.White,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 2.dp),
fontSize = 14.sp,
maxLines = 1
)
}
}
I have FilterFabMenuItem composable which is a Row that contains FilterFabMenuLabel and FilterFabMenuButton composables:
FilterFabMenuItem
#Composable
fun FilterFabMenuItem(
menuItem: FilterFabMenuItem,
onMenuItemClick: (FilterFabMenuItem) -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
//label
FilterFabMenuLabel(label = menuItem.label)
//fab
FilterFabMenuButton(item = menuItem, onClick = onMenuItemClick)
}
}
I have FilterFabMenu composable which is a Column that shows menu items:
FilterFabMenu
#Composable
fun FilterFabMenu(
visible: Boolean,
items: List<FilterFabMenuItem>,
modifier: Modifier = Modifier
) {
val enterTransition = remember {
expandVertically(
expandFrom = Alignment.Bottom,
animationSpec = tween(150, easing = FastOutSlowInEasing)
) + fadeIn(
initialAlpha = 0.3f,
animationSpec = tween(150, easing = FastOutSlowInEasing)
)
}
val exitTransition = remember {
shrinkVertically(
shrinkTowards = Alignment.Bottom,
animationSpec = tween(150, easing = FastOutSlowInEasing)
) + fadeOut(
animationSpec = tween(150, easing = FastOutSlowInEasing)
)
}
AnimatedVisibility(visible = visible, enter = enterTransition, exit = exitTransition) {
Column(
modifier = modifier.fillMaxWidth(), horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
items.forEach { menuItem ->
FilterFabMenuItem(
menuItem = menuItem,
onMenuItemClick = {}
)
}
}
}
}
I have FilterFab composable that expands/collapses FilterMenu:
FilterFab
#Composable
fun FilterFab(
state: FilterFabState,
rotation:Float,
onClick: (FilterFabState) -> Unit,
modifier: Modifier = Modifier
) {
FloatingActionButton(
modifier = modifier
.rotate(rotation),
elevation = FloatingActionButtonDefaults.elevation(defaultElevation = 0.dp),
onClick = {
onClick(
if (state == FilterFabState.EXPANDED) {
FilterFabState.COLLAPSED
} else {
FilterFabState.EXPANDED
}
)
},
backgroundColor = colorResource(
R.color.primary_color
),
shape = CircleShape
) {
Icon(
painter = painterResource(R.drawable.fab_add),
contentDescription = null,
tint = Color.White
)
}
}
Last but not least, I have a FilterView composable which is a Column that contains FilterFabMenu and FilterFab composables:
FilterView
#SuppressLint("UnusedTransitionTargetStateParameter")
#Composable
fun FilterView(
items: List<FilterFabMenuItem>,
modifier: Modifier = Modifier
) {
var filterFabState by rememberSaveable() {
mutableStateOf(FilterFabState.COLLAPSED)
}
val transitionState = remember {
MutableTransitionState(filterFabState).apply {
targetState = FilterFabState.COLLAPSED
}
}
val transition = updateTransition(targetState = transitionState, label = "transition")
val iconRotationDegree by transition.animateFloat({
tween(durationMillis = 150, easing = FastOutSlowInEasing)
}, label = "rotation") {
if (filterFabState == FilterFabState.EXPANDED) 230f else 0f
}
Column(
modifier = modifier.fillMaxSize().padding(16.dp), horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(16.dp,Alignment.Bottom)
) {
FilterFabMenu(items = items, visible = filterFabState == FilterFabState.EXPANDED)
FilterFab(
state = filterFabState,
rotation = iconRotationDegree, onClick = { state ->
filterFabState = state
})
}
}
This produces the following result:
expandVertically in your enterTransition is not the correct approach for this kind of animation. Per documentation, it animates a clip revealing the content of the animated item from top to bottom, or vice versa. You apply this animation to the entire column (so all items at once), resulting in the gif you have shown us.
Instead, you should use a different enter/exit animation type, maybe a custom animation where you work with the item scaling to emulate the "pop in" effect like such:
scaleFactor.animateTo(2f, tween(easing = FastOutSlowInEasing, durationMillis = 50))
scaleFactor.animateTo(1f, tween(easing = FastOutSlowInEasing, durationMillis = 70))
(the scaleFactor is an animatabale of type Animatable<Float, AnimationVector1D> in this instance).
Then you create such an animatable for each of the column items, i.e. your menu items. After that, just run the animations in a for loop for each menu item inside a coroutine scope (since compose animations are suspend by default, they will run in sequence, use async/awaitAll if you want to do it in parallel).
Also, don't forget to put your animatabales in remember {}, then just use the values you are animating like scaleFactor inside modifiers of your column items, and trigger them inside a LaunchedEffect when you click to expand/close the menu.

Cannot calculate specific dimensions for composable TextField

I am trying to create multiple items to encapsulate the specific behavior of every component but I cannot specify the dimensions for every view.
I want a Textfield with an X icon on its right
setContent {
Surface(
modifier = Modifier
.fillMaxSize()
.background(color = white)
.padding(horizontal = 15.dp)
) {
Row(horizontalArrangement = Arrangement.spacedBy(15.dp)) {
Searcher(
modifier = Modifier.weight(1f),
onTextChanged = { },
onSearchAction = { }
)
Image(
painter = painterResource(id = R.drawable.ic_close),
contentDescription = null,
colorFilter = ColorFilter.tint(blue)
)
}
}
}
The component is the following
#Composable
fun Searcher(
modifier: Modifier = Modifier,
onTextChanged: (String) -> Unit,
onSearchAction: () -> Unit
) {
Row {
SearcherField(
onTextChanged = onTextChanged,
onSearchAction = onSearchAction,
modifier = Modifier.weight(1f)
)
CircularSearch(
modifier = Modifier
.padding(horizontal = 10.dp)
.align(CenterVertically)
)
}
}
and the SearcherField:
#Composable
fun SearcherField(
modifier: Modifier = Modifier,
onTextChanged: (String) -> Unit,
onSearchAction: () -> Unit
) {
var fieldText by remember { mutableStateOf(emptyText) }
TextField(
value = fieldText,
onValueChange = { value ->
fieldText = value
if (value.length > 2)
onTextChanged(value)
},
singleLine = true,
textStyle = Typography.h5.copy(color = White),
colors = TextFieldDefaults.textFieldColors(
cursorColor = White,
focusedIndicatorColor = Transparent,
unfocusedIndicatorColor = Transparent,
backgroundColor = Transparent
),
trailingIcon = {
if (fieldText.isNotEmpty()) {
IconButton(onClick = {
fieldText = emptyText
}) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = emptyText
)
}
}
},
placeholder = {
Text(
text = stringResource(id = R.string.dondebuscas),
style = Typography.h5.copy(color = White)
)
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
onSearchAction()
}
),
modifier = modifier.fillMaxWidth()
)
}
But I don´t know why, but the component Searcher with the placeholder is rendered in two lines.
It´s all about the placeholder that seems to be resized for not having enough space because if I remove the placeholder, the component looks perfect.
Everything is in one line, not having a placeholder of two lines. I m trying to modify the size of every item but I am not able to get the expected result and I don´t know if the problem is just about the placeholder.
How can I solve it? UPDATE -> I found the error
Thanks in advance!
Add maxlines = 1 to the placeholder Text's parameters.
Your field is single -lune but your text is multi-line. I think it creates conflict in implementation.
Okay... I find that the problem is about the trailing icon. Is not visible when there is no text in the TextField but is still occupying some space in the view, that´s why the placeholder cannot occupy the entire space. The solution is the following.
val trailingIconView = #Composable {
IconButton(onClick = {
fieldText = emptyText
}) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = emptyText
)
}
}
Create a variable with the icon and set it to the TextField only when is required
trailingIcon = if (fieldText.isNotEmpty()) trailingIconView else null,
With that, the trailing icon will be "gone" instead of "invisible" (the old way).
Still have a lot to learn.

Jetpack Compose Button padding outside of border: why?

Using Button, I want to make its diminensions such that its border hugs its width, with a minimum width and height of 40dp. In the sample below, I like the looks of the BigNumber preview. It does not have any outside horizontal padding. The Default preview does have padding outside the border. How do I fix this without setting an absolute width? Consider this sample:
#Composable
fun BasketQuantityStepper(
quantityControlsViewState: QuantityControlsViewState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Button(
onClick = onClick,
colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(id = R.color.basketQuantityStepperBackground)),
border = BorderStroke(dimensionResource(id = R.dimen.buttonBorderWidth), colorResource(id = R.color.basketQuantityStepperBorderColor)),
modifier = modifier
.heightIn(min = 40.dp)
.widthIn(min = 40.dp),
) {
Text(
text = "${quantityControlsViewState.currentQuantity}",
)
}
}
#Preview
#Composable
private fun PreviewDefault() {
BasketQuantityStepper(quantityControlsViewState = QuantityControlsViewState(
currentQuantity = 1,
minOrderQuantity = 1,
maxOrderQuantity = 10,
stepQuantity = 1
), onClick = {})
}
#Preview
#Composable
private fun PreviewBigNumber() {
BasketQuantityStepper(quantityControlsViewState = QuantityControlsViewState(
currentQuantity = 100,
minOrderQuantity = 1,
maxOrderQuantity = 1000,
stepQuantity = 1
), onClick = {})
}
Minimum dimension of Composables' touch area is 48.dp by default for accessibility.
You can remove it by wrapping your button with
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
}
but it's not advised to have Composable's smaller than accebility size. Icons, CheckBox, even Slider uses 48.dp by default.
CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
Button(
onClick = {},
border = BorderStroke(2.dp, Color.LightGray),
modifier = Modifier
.border(2.dp, Color.Green)
.heightIn(min = 40.dp)
.widthIn(min = 40.dp),
) {
Text(
text = "$counter",
)
}
}
https://developer.android.com/jetpack/compose/accessibility
Remove Default padding around checkboxes in Jetpack Compose new update

How to remove space below text baseline in jetpack compose?

Currently I get this:
But I want something like this:
But also the text from "50" and "min" should be aligned to the top.
My code:
Row(verticalAlignment = Alignment.Bottom) {
Text(
text = "18",
color = MaterialTheme.colors.primaryVariant,
fontSize = 60.sp,
modifier = Modifier
.weight(1f).height(62.dp),
textAlign = TextAlign.End,
)
Text(
text = "hrs",
modifier = Modifier.weight(1f).height(16.dp),
)
}
Row(verticalAlignment = Alignment.Top) {
Text(
text = "50",
color = MaterialTheme.colors.primaryVariant,
fontSize = 28.sp,
modifier = Modifier.weight(1f).height(30.dp).align(Alignment.Top),
textAlign = TextAlign.End,
)
Text("min", modifier = Modifier.weight(1f))
}
As you see in my code I solved that by using the height property currently. But it doesn't work for the align top and it feels a bit wrong. is there a better way to make it work?
You can use the AnnotatedString to display the text with multiple styles.
Something like:
Row() {
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(
color = MaterialTheme.colors.primaryVariant,
fontSize = 60.sp)) {
append("18")
}
append(" hrs ")
})
}
For the second case you can apply a BaselineShift to the min text:
Row() {
Text(
text = buildAnnotatedString {
withStyle(style = SpanStyle(
color = MaterialTheme.colors.primaryVariant,
fontSize = 28.sp)) {
append("50")
}
withStyle(style = SpanStyle(
baselineShift = BaselineShift(+0.65f))) {
append(" min ")
}
})
}
You can use AlignmentLine.LastBaseLine to properly size and position you're Text. You can do something like this:
Modifier.layout { measurable, constraints ->
// Measure the composable
val placeable = measurable.measure(constraints)
// Check the composable has a LastBaseline
check(placeable[LastBaseline] != AlignmentLine.Unspecified)
val lastBaseline = placeable[LastBaseline]
val placeableY = placeable.height - lastBaseline
val height = placeable.height - placeableY
layout(placeable.width, height) {
placeable.placeRelative(0, -placeableY)
}
}
If you want to completely remove even the bottom FirstBaseLine just subtract it to the height and that should do it.
From version 1.2.0-alpha05, includeFontPadding is turned off as they announced: (https://developer.android.com/jetpack/androidx/releases/compose-ui#1.2.0-alpha05)
Text: includeFontPadding is now turned off by default. The clipping issues as a result of includeFontPadding=false is handled and no clipping should occur for tall scripts.
In case you haven't found the right solution. You can try this:
Text(text = buildAnnotatedString{
withStyle(style =
ParagraphStyle(
platformStyle = PlatformParagraphStyle(includeFontPadding = false),
lineHeightStyle = LineHeightStyle(
LineHeightStyle.Alignment.Bottom,
LineHeightStyle.Trim.Both
)
)
){
append("18")
}
})

How to pass android compose material icons to textField

I want to use material icons as argument passing it to the textField.
#Composable
fun NormalTextField(
icon: () -> Unit, // how to pass material icon to textField
label: String
) {
val (text, setText) = mutableStateOf("")
TextField(
leadingIcon = icon,
value = text,
onValueChange = setText,
label = label
)
}
The leadingIcon argument of texfield is a composable function (the label too), so one way to do it is:
#Composable
fun Example() {
NormalTextField(label = "Email") {
Icon(
imageVector = Icons.Outlined.Email,
contentDescription = null
)
}
}
#Composable
fun NormalTextField(
label: String,
Icon: #Composable (() -> Unit)
) {
val (text, setText) = mutableStateOf("")
TextField(
leadingIcon = Icon,
value = text,
onValueChange = setText,
label = { Text(text = label) }
)
}
This can be done using InlineTextContent. Here is an example how to insert the icon at the start of the text. You can wrap this into another composable if you just want to pass the icon as a parameter.
Text(text = buildAnnotatedString {
appendInlineContent("photoIcon", "photoIcon")
append("very long breaking text very long breaking text very long breaking text very long breaking text very long breaking text")
}, inlineContent = mapOf(
Pair("photoIcon", InlineTextContent(
Placeholder(width = 1.7.em, height = 23.sp, placeholderVerticalAlign = PlaceholderVerticalAlign.TextTop)
) {
Image(
painterResource(R.drawable.ic_cameraicon),"play",
modifier = Modifier.fillMaxWidth().padding(end = 10.dp),
alignment = Alignment.Center,
contentScale = ContentScale.FillWidth)
}
)), lineHeight = 23.sp, color = Color.White, fontFamily = HelveticaNeue, fontSize = 18.sp, fontWeight = FontWeight.Medium)
The result would look like this:

Categories

Resources