Jetpack Compose - how to make SwipeToDismiss work on LazyRow? - android

I saw many examples about SwipeToDismiss working on LazyColumn but I did not see one implementing it on LazyRow. Here is my current implementation -
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun DashboardDataState(dashboardCardModels: List<DashboardCardModel>) {
Column(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
backgroundStartColor,
backgroundEndColor
)
)
),
verticalArrangement = Arrangement.Center
) {
LazyRow(modifier = Modifier.wrapContentSize()) {
items(dashboardCardModels) { model ->
val dismissState = rememberDismissState()
when {
dismissState.isDismissed(DismissDirection.EndToStart) ->{
}
dismissState.isDismissed(DismissDirection.StartToEnd) ->{
}
}
SwipeToDismiss(
state = rememberDismissState(),
background = {
val color by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.Default -> Color.White
DismissValue.DismissedToEnd -> Color.Blue
DismissValue.DismissedToStart -> Color.Red
}
)
val alignment = Alignment.CenterEnd
val icon = Icons.Default.Delete
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = Dp(20f)),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Delete Icon",
modifier = Modifier.scale(scale)
)
}
},
dismissContent = {
DashboardCard(model = model)
}
)
}
}
}
}
What happens is that I am able to swipe the LazyRow horizontally, but obviously I want to swipe it vertically. How can I achieve such behavior?

Going deeper into the SwipeToDismiss code and learning it's behavior, I made some simple modifications, changing the height variable to width, the swipe orientation to Orientation.Vertical and the offset of the dismissContent variable to go on the y axis instead of the x axis. I named my modified composable SwipeToDismissVertical and it works perfectly 🤩
#Composable
#ExperimentalMaterialApi
fun SwipeToDismissVertical(
state: DismissState,
modifier: Modifier = Modifier,
directions: Set<DismissDirection> = setOf(DismissDirection.EndToStart, DismissDirection.StartToEnd),
dismissThresholds: (DismissDirection) -> ThresholdConfig = {
FixedThreshold(DISMISS_THRESHOLD)
},
background: #Composable RowScope.() -> Unit,
dismissContent: #Composable RowScope.() -> Unit
) = BoxWithConstraints(modifier) {
val width = constraints.maxWidth.toFloat()
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
val anchors = mutableMapOf(0f to DismissValue.Default)
if (DismissDirection.StartToEnd in directions) anchors += width to DismissValue.DismissedToEnd
if (DismissDirection.EndToStart in directions) anchors += -width to DismissValue.DismissedToStart
val thresholds = { from: DismissValue, to: DismissValue ->
dismissThresholds(getDismissDirection(from, to)!!)
}
val minFactor =
if (DismissDirection.EndToStart in directions) SwipeableDefaults.StandardResistanceFactor else SwipeableDefaults.StiffResistanceFactor
val maxFactor =
if (DismissDirection.StartToEnd in directions) SwipeableDefaults.StandardResistanceFactor else SwipeableDefaults.StiffResistanceFactor
Box(
Modifier.swipeable(
state = state,
anchors = anchors,
thresholds = thresholds,
orientation = Orientation.Vertical,
enabled = state.currentValue == DismissValue.Default,
reverseDirection = isRtl,
resistance = ResistanceConfig(
basis = width,
factorAtMin = minFactor,
factorAtMax = maxFactor
)
)
) {
Row(
content = background,
modifier = Modifier.matchParentSize()
)
Row(
content = dismissContent,
modifier = Modifier.offset { IntOffset(0, state.offset.value.roundToInt(), ) }
)
}
}
private val DISMISS_THRESHOLD = 56.dp
private fun getDismissDirection(from: DismissValue, to: DismissValue): DismissDirection? {
return when {
// settled at the default state
from == to && from == DismissValue.Default -> null
// has been dismissed to the end
from == to && from == DismissValue.DismissedToEnd -> DismissDirection.StartToEnd
// has been dismissed to the start
from == to && from == DismissValue.DismissedToStart -> DismissDirection.EndToStart
// is currently being dismissed to the end
from == DismissValue.Default && to == DismissValue.DismissedToEnd -> DismissDirection.StartToEnd
// is currently being dismissed to the start
from == DismissValue.Default && to == DismissValue.DismissedToStart -> DismissDirection.EndToStart
// has been dismissed to the end but is now animated back to default
from == DismissValue.DismissedToEnd && to == DismissValue.Default -> DismissDirection.StartToEnd
// has been dismissed to the start but is now animated back to default
from == DismissValue.DismissedToStart && to == DismissValue.Default -> DismissDirection.EndToStart
else -> null
}
}
The usage is exactly the same as before, but now I use my new composable instead of the material one -
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun DashboardDataState(dashboardCardModels: List<DashboardCardModel>) {
Column(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.verticalGradient(
colors = listOf(
backgroundStartColor,
backgroundEndColor
)
)
),
verticalArrangement = Arrangement.Center
) {
LazyRow(modifier = Modifier.wrapContentSize()) {
items(dashboardCardModels) { model ->
val dismissState = rememberDismissState()
when {
dismissState.isDismissed(DismissDirection.EndToStart) ->{
}
dismissState.isDismissed(DismissDirection.StartToEnd) ->{
}
}
SwipeToDismissVertical( //Here is the new one
state = dismissState,
background = {
val color by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.Default -> Color.Transparent
DismissValue.DismissedToEnd -> Color.Blue
DismissValue.DismissedToStart -> Color.Red
}
)
val alignment = Alignment.CenterEnd
val icon = Icons.Default.Delete
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = Dp(20f)),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Delete Icon",
modifier = Modifier.scale(scale)
)
}
},
dismissContent = {
DashboardCard(model = model)
}
)
}
}
}
}

Related

How to scale item in row while hovering over in Jetpack compose

I am working on recreating facebook reactions with jetpack compose. Have now an issue with how I can detect when hovering over an item in a row and scale it? To be more clear I am creating THIS. I reused popup from jetpack compose that part is fine. But dragging/hovering over the row not sure how to scale images. Any advice or help?
I did this initial implementation for you. I hope you can continue your implementation from here:
#ExperimentalComposeUiApi
#Composable
fun ReactionsComponent() {
val density = LocalDensity.current
var selectedIndex by remember {
mutableStateOf(-1)
}
val iconSize = 48.dp
val boxPadding = 8.dp
val iconSizePx = with(density) { iconSize.toPx() }
val boxPaddingPx = with(density) { boxPadding.toPx() }
val increaseSize = iconSize.times(2f)
val icons = listOf(
Icons.Default.Favorite,
Icons.Default.Star,
Icons.Default.Call,
Icons.Default.AccountBox,
Icons.Default.ThumbUp
)
Box(
Modifier
.height(increaseSize)
.width(IntrinsicSize.Min)
.pointerInteropFilter {
val selection = ((it.x - boxPaddingPx) / iconSizePx).toInt()
if (selection >= icons.size || selection < 0 || it.x < boxPaddingPx) {
selectedIndex = -1
} else if (it.action == MotionEvent.ACTION_UP) {
selectedIndex = -1 // finger released
} else {
selectedIndex = selection
}
true
}
) {
Box(
Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
.height(iconSize + boxPadding.times(2))
.background(Color.LightGray, CircleShape)
)
Row(
Modifier
.align(Alignment.BottomStart)
.width(IntrinsicSize.Min)
.padding(boxPadding),
verticalAlignment = Alignment.Bottom
) {
icons.forEachIndexed { index, icon ->
val size = if (selectedIndex == index) increaseSize else iconSize
Box(
Modifier
.border(1.dp, Color.LightGray, CircleShape)
.background(Color.White, CircleShape)
.height(animateDpAsState(size).value)
.width(animateDpAsState(size).value)
) {
Icon(
icon,
contentDescription = null,
tint = Color.Magenta,
modifier = Modifier.fillMaxSize().padding(8.dp)
)
}
}
}
}
}
Here's the result:
You can find the full code here (which includes item selection).

Material Swipe To Dismiss in Jetpack Compose with a Column instead of a LazyColumn

In my Jetpack Compose project I want to use Material SwipeToDismiss to delete an item with a swipe. In my composable I use a Column in a list instead of a LazyColumn. The problem is that when the item is deleted a recomposition starts and all following items are also deleted (because the dismissState seems to be the same for the next item). For example if the Column has 7 items and I delete item #5 then also item #6 and item #7 gets deleted.
It works if I use a LazyColumn (probably because of the keys) with a fixed height but I just want a list of items without scrolling or a fixed height. Any suggestions how to get this running?
#OptIn(ExperimentalMaterialApi::class)
#Composable
private fun DocumentationsList(
modifier: Modifier = Modifier,
task: DBTask,
documentations: List<DBDocumentation>,
onDocumentationDelete: (DBDocumentation) -> Unit,
) {
Column(
modifier = modifier
) {
for ((index, documentation) in documentations.withIndex()) {
var dismissState = rememberDismissState(initialValue = DismissValue.Default)
if (dismissState.isDismissed(DismissDirection.EndToStart)) {
onDocumentationDelete(documentation)
}
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 4.dp),
directions = setOf(DismissDirection.EndToStart),
dismissThresholds = { direction ->
FractionalThreshold(if (direction == DismissDirection.StartToEnd) 0.25f else 0.5f)
},
background = {
val direction = dismissState.dismissDirection ?: return#SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.Default -> Color.LightGray
DismissValue.DismissedToEnd -> Color.Green
DismissValue.DismissedToStart -> Color.Red
}
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.Done
DismissDirection.EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f
)
Box(
Modifier.fillMaxSize().background(color).padding(horizontal = 20.dp),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier.scale(scale)
)
}
},
dismissContent = {
Card(
elevation = animateDpAsState(
if (dismissState.dismissDirection != null) 4.dp else 0.dp
).value
) {
// Layout here
}
}
)
}
}
You can use key which will work the same LazyColumn key parameter:
Column(
modifier = modifier
) {
for ((index, documentation) in documentations.withIndex()) {
key(uniqueKey) {
// your view
}
}
}
This should not be your index, as it will change for all items below the deleted one after deletion, it should be some identifier unique to each item in your collection.

SwipeToDismiss inside LazyColumn with animation

I am trying to achieve something like this but with Jetpack Compose. In other words swipe to delete like we could do in RecyclerView with ItemTouchHelper and class DiffCallBack : DiffUtil.ItemCallback<RvModel>() where we could see enter - exit animations and then the list moving gracefully up or down where the item has been inserted or removed.
This is what I have tried:
LazyColumn(state = listState) {
items(products, {listItem:InventoryEntity -> listItem.inventoryId}) { item ->
var unread by remember { mutableStateOf(false) }
val dismissState = rememberDismissState(
confirmStateChange = {
if (it == DismissValue.DismissedToEnd) unread = !unread
it != DismissValue.DismissedToEnd
}
)
val isDismissed = dismissState.isDismissed(DismissDirection.EndToStart)
if (dismissState.isDismissed(DismissDirection.EndToStart)){
LaunchedEffect(Unit) {
delay(300)
viewModel.deleteProduct(item.inventoryId)
}
}
var itemAppeared by remember { mutableStateOf(!columnAppeared) }
LaunchedEffect(Unit) {
itemAppeared = true
}
AnimatedVisibility(
visible = itemAppeared && !isDismissed,
exit = shrinkVertically(
animationSpec = tween(
durationMillis = 300,
)
),
enter = expandVertically(
animationSpec = tween(
durationMillis = 300
)
)
) {
SwipeToDismiss(
state = dismissState,
modifier = Modifier.padding(vertical = 4.dp),
directions = setOf(
DismissDirection.StartToEnd,
DismissDirection.EndToStart
),
dismissThresholds = { direction ->
FractionalThreshold(if (direction == DismissDirection.StartToEnd) 0.25f else 0.5f)
},
background = {
val direction =
dismissState.dismissDirection ?: return#SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.Default -> Color.LightGray
DismissValue.DismissedToEnd -> Color.Green
DismissValue.DismissedToStart -> Color.Red
}
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.Done
DismissDirection.EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier.scale(scale)
)
}
},
dismissContent = {
Card(
elevation = animateDpAsState(
if (dismissState.dismissDirection != null) 4.dp else 0.dp
).value
) {
ProductRow(product = item, number = item.inventoryId)
}
}
)
}
}
}
Even though it works. Scrolling is not smooth, and when I scroll up it jumps to the top. What is the right way to implement this function?
Recently google anounced compose Version 1.1.0-beta03. Now we have a new way on how we can animate items. They have introduced a new modifier: Modifier.animateItemPlacement(). You may find latest compose version in this link.
I will try to post an example with minimum code so that you can reproduce it and see how you could achieve a SwipeToDismiss inside LazyColumn with animation.
Data Class To store information:
data class DataSet(
val itemId: Int,
val itemName: String,
val itemQty: String
)
Comparator to compare list items:
private val ListComparator = Comparator<DataSet> { left, right ->
left.itemId.compareTo(right.itemId)
}
The row of each of our items:
#Composable
fun ItemRow(
modifier: Modifier = Modifier,
product: DataSet,
number: Int
) {
Card(
shape = RoundedCornerShape(4.dp),
modifier = modifier
.padding(8.dp)
.fillMaxWidth(),
backgroundColor = Color.LightGray
) {
Row(modifier = modifier) {
Text(
text = "$number.", modifier = Modifier
.weight(2f)
.padding(start = 8.dp, end = 4.dp)
)
Text(
text = product.itemName, modifier = Modifier
.weight(10f)
.padding(end = 4.dp)
)
Text(
text = product.itemQty, modifier = Modifier
.weight(2f)
.padding(end = 4.dp)
)
}
}
}
Putting all together to our composable:
#ExperimentalMaterialApi
#ExperimentalFoundationApi
#Composable
fun helloWorld() {
var list by remember { mutableStateOf(listOf<DataSet>()) }
val comparator by remember { mutableStateOf(ListComparator) }
LazyColumn {
item {
Button(onClick = {
list = list + listOf(DataSet((0..1111).random(), "A random item", "100"))
}) {
Text("Add an item to the list")
}
}
val sortedList = list.sortedWith(comparator)
items(sortedList, key = { it.itemId }) { item ->
val dismissState = rememberDismissState()
if (dismissState.isDismissed(DismissDirection.EndToStart)) {
list = list.toMutableList().also { it.remove(item) } // remove
}
SwipeToDismiss(
state = dismissState,
modifier = Modifier
.padding(vertical = 1.dp)
.animateItemPlacement(),
directions = setOf(DismissDirection.StartToEnd, DismissDirection.EndToStart),
dismissThresholds = { direction ->
FractionalThreshold(if (direction == DismissDirection.StartToEnd) 0.25f else 0.5f)
},
background = {
val direction = dismissState.dismissDirection ?: return#SwipeToDismiss
val color by animateColorAsState(
when (dismissState.targetValue) {
DismissValue.Default -> Color.LightGray
DismissValue.DismissedToEnd -> Color.Green
DismissValue.DismissedToStart -> Color.Red
}
)
val alignment = when (direction) {
DismissDirection.StartToEnd -> Alignment.CenterStart
DismissDirection.EndToStart -> Alignment.CenterEnd
}
val icon = when (direction) {
DismissDirection.StartToEnd -> Icons.Default.Done
DismissDirection.EndToStart -> Icons.Default.Delete
}
val scale by animateFloatAsState(
if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f
)
Box(
Modifier
.fillMaxSize()
.background(color)
.padding(horizontal = 20.dp),
contentAlignment = alignment
) {
Icon(
icon,
contentDescription = "Localized description",
modifier = Modifier.scale(scale)
)
}
},
dismissContent = {
Card(
elevation = animateDpAsState(
if (dismissState.dismissDirection != null) 4.dp else 0.dp
).value
) {
ItemRow(
product = item,
number = item.itemId
)
}
}
)
}
}
}
For reference and see also other ways on how you could use Modifier.animateItemPlacement() you may check this example posted from Google.

How to make FlipCard animation in Jetpack compose

I have an existing app where I have implemented FlipCard animation like below using Objectanimator in XML. If I click on a card it flips horizontally. But now I want to migrate it to jetpack compose. So is it possible to make flip card animation in jetpack compose?
Update
Finally, I have ended up with this. Though I don't know if it is the right way or not but I got exactly what I wanted. If there is any better alternative you can suggest. Thank you.
Method 1: Using animate*AsState
#Composable
fun FlipCard() {
var rotated by remember { mutableStateOf(false) }
val rotation by animateFloatAsState(
targetValue = if (rotated) 180f else 0f,
animationSpec = tween(500)
)
val animateFront by animateFloatAsState(
targetValue = if (!rotated) 1f else 0f,
animationSpec = tween(500)
)
val animateBack by animateFloatAsState(
targetValue = if (rotated) 1f else 0f,
animationSpec = tween(500)
)
val animateColor by animateColorAsState(
targetValue = if (rotated) Color.Red else Color.Blue,
animationSpec = tween(500)
)
Box(
Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Card(
Modifier
.fillMaxSize(.5f)
.graphicsLayer {
rotationY = rotation
cameraDistance = 8 * density
}
.clickable {
rotated = !rotated
},
backgroundColor = animateColor
)
{
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = if (rotated) "Back" else "Front",
modifier = Modifier
.graphicsLayer {
alpha = if (rotated) animateBack else animateFront
rotationY = rotation
})
}
}
}
}
Method 2: Encapsulate a Transition and make it reusable.
You will get the same output as method 1. But it is reusable and for the complex case.
enum class BoxState { Front, Back }
#Composable
fun AnimatingBox(
rotated: Boolean,
onRotate: (Boolean) -> Unit
) {
val transitionData = updateTransitionData(
if (rotated) BoxState.Back else BoxState.Front
)
Card(
Modifier
.fillMaxSize(.5f)
.graphicsLayer {
rotationY = transitionData.rotation
cameraDistance = 8 * density
}
.clickable { onRotate(!rotated) },
backgroundColor = transitionData.color
)
{
Column(
Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = if (rotated) "Back" else "Front",
modifier = Modifier
.graphicsLayer {
alpha =
if (rotated) transitionData.animateBack else transitionData.animateFront
rotationY = transitionData.rotation
})
}
}
}
private class TransitionData(
color: State<Color>,
rotation: State<Float>,
animateFront: State<Float>,
animateBack: State<Float>
) {
val color by color
val rotation by rotation
val animateFront by animateFront
val animateBack by animateBack
}
#Composable
private fun updateTransitionData(boxState: BoxState): TransitionData {
val transition = updateTransition(boxState, label = "")
val color = transition.animateColor(
transitionSpec = {
tween(500)
},
label = ""
) { state ->
when (state) {
BoxState.Front -> Color.Blue
BoxState.Back -> Color.Red
}
}
val rotation = transition.animateFloat(
transitionSpec = {
tween(500)
},
label = ""
) { state ->
when (state) {
BoxState.Front -> 0f
BoxState.Back -> 180f
}
}
val animateFront = transition.animateFloat(
transitionSpec = {
tween(500)
},
label = ""
) { state ->
when (state) {
BoxState.Front -> 1f
BoxState.Back -> 0f
}
}
val animateBack = transition.animateFloat(
transitionSpec = {
tween(500)
},
label = ""
) { state ->
when (state) {
BoxState.Front -> 0f
BoxState.Back -> 1f
}
}
return remember(transition) { TransitionData(color, rotation, animateFront, animateBack) }
}
Output
setContent {
ComposeAnimationTheme {
Surface(color = MaterialTheme.colors.background) {
var state by remember {
mutableStateOf(CardFace.Front)
}
FlipCard(
cardFace = state,
onClick = {
state = it.next
},
axis = RotationAxis.AxisY,
back = {
Text(text = "Front", Modifier
.fillMaxSize()
.background(Color.Red))
},
front = {
Text(text = "Back", Modifier
.fillMaxSize()
.background(Color.Green))
}
)
}
}
}
enum class CardFace(val angle: Float) {
Front(0f) {
override val next: CardFace
get() = Back
},
Back(180f) {
override val next: CardFace
get() = Front
};
abstract val next: CardFace
}
enum class RotationAxis {
AxisX,
AxisY,
}
#ExperimentalMaterialApi
#Composable
fun FlipCard(
cardFace: CardFace,
onClick: (CardFace) -> Unit,
modifier: Modifier = Modifier,
axis: RotationAxis = RotationAxis.AxisY,
back: #Composable () -> Unit = {},
front: #Composable () -> Unit = {},
) {
val rotation = animateFloatAsState(
targetValue = cardFace.angle,
animationSpec = tween(
durationMillis = 400,
easing = FastOutSlowInEasing,
)
)
Card(
onClick = { onClick(cardFace) },
modifier = modifier
.graphicsLayer {
if (axis == RotationAxis.AxisX) {
rotationX = rotation.value
} else {
rotationY = rotation.value
}
cameraDistance = 12f * density
},
) {
if (rotation.value <= 90f) {
Box(
Modifier.fillMaxSize()
) {
front()
}
} else {
Box(
Modifier
.fillMaxSize()
.graphicsLayer {
if (axis == RotationAxis.AxisX) {
rotationX = 180f
} else {
rotationY = 180f
}
},
) {
back()
}
}
}
}
Check this article. https://fvilarino.medium.com/creating-a-rotating-card-in-jetpack-compose-ba94c7dd76fb.

Scaling Button Animation in Jetpack Compose

I want to build this awesome button animation pressed from the AirBnB App with Jetpack Compose
Unfortunately, the Animation/Transition API was changed recently and there's almost no documentation for it. Can someone help me get the right approach to implement this button press animation?
Edit
Based on #Amirhosein answer I have developed a button that looks almost exactly like the Airbnb example
Code:
#Composable
fun AnimatedButton() {
val boxHeight = animatedFloat(initVal = 50f)
val relBoxWidth = animatedFloat(initVal = 1.0f)
val fontSize = animatedFloat(initVal = 16f)
fun animateDimensions() {
boxHeight.animateTo(45f)
relBoxWidth.animateTo(0.95f)
// fontSize.animateTo(14f)
}
fun reverseAnimation() {
boxHeight.animateTo(50f)
relBoxWidth.animateTo(1.0f)
//fontSize.animateTo(16f)
}
Box(
modifier = Modifier
.height(boxHeight.value.dp)
.fillMaxWidth(fraction = relBoxWidth.value)
.clip(RoundedCornerShape(8.dp))
.background(Color.Black)
.clickable { }
.pressIndicatorGestureFilter(
onStart = {
animateDimensions()
},
onStop = {
reverseAnimation()
},
onCancel = {
reverseAnimation()
}
),
contentAlignment = Alignment.Center
) {
Text(text = "Explore Airbnb", fontSize = fontSize.value.sp, color = Color.White)
}
}
Video:
Unfortunately, I cannot figure out how to animate the text correctly as It looks very bad currently
Are you looking for something like this?
#Composable
fun AnimatedButton() {
val selected = remember { mutableStateOf(false) }
val scale = animateFloatAsState(if (selected.value) 2f else 1f)
Column(
Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Button(
onClick = { },
modifier = Modifier
.scale(scale.value)
.height(40.dp)
.width(200.dp)
.pointerInteropFilter {
when (it.action) {
MotionEvent.ACTION_DOWN -> {
selected.value = true }
MotionEvent.ACTION_UP -> {
selected.value = false }
}
true
}
) {
Text(text = "Explore Airbnb", fontSize = 15.sp, color = Color.White)
}
}
}
Here's the implementation I used in my project. Seems most concise to me.
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val sizeScale by animateFloatAsState(if (isPressed) 0.5f else 1f)
Button(
onClick = { },
modifier = Modifier
.wrapContentSize()
.graphicsLayer(
scaleX = sizeScale,
scaleY = sizeScale
),
interactionSource = interactionSource
) { Text(text = "Open the reward") }
Use pressIndicatorGestureFilter to achieve this behavior.
Here is my workaround:
#Preview
#Composable
fun MyFancyButton() {
val boxHeight = animatedFloat(initVal = 60f)
val boxWidth = animatedFloat(initVal = 200f)
Box(modifier = Modifier
.height(boxHeight.value.dp)
.width(boxWidth.value.dp)
.clip(RoundedCornerShape(4.dp))
.background(Color.Black)
.clickable { }
.pressIndicatorGestureFilter(
onStart = {
boxHeight.animateTo(55f)
boxWidth.animateTo(180f)
},
onStop = {
boxHeight.animateTo(60f)
boxWidth.animateTo(200f)
},
onCancel = {
boxHeight.animateTo(60f)
boxWidth.animateTo(200f)
}
), contentAlignment = Alignment.Center) {
Text(text = "Utforska Airbnb", color = Color.White)
}
}
The default jetpack compose Button consumes tap gestures in its onClick event and pressIndicatorGestureFilter doesn't receive taps. That's why I created this custom button
You can use the Modifier.pointerInput to detect the tapGesture.
Define an enum:
enum class ComponentState { Pressed, Released }
Then:
var toState by remember { mutableStateOf(ComponentState.Released) }
val modifier = Modifier.pointerInput(Unit) {
detectTapGestures(
onPress = {
toState = ComponentState.Pressed
tryAwaitRelease()
toState = ComponentState.Released
}
)
}
// Defines a transition of `ComponentState`, and updates the transition when the provided [targetState] changes
val transition: Transition<ComponentState> = updateTransition(targetState = toState, label = "")
// Defines a float animation to scale x,y
val scalex: Float by transition.animateFloat(
transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
if (state == ComponentState.Pressed) 1.25f else 1f
}
val scaley: Float by transition.animateFloat(
transitionSpec = { spring(stiffness = 50f) }, label = ""
) { state ->
if (state == ComponentState.Pressed) 1.05f else 1f
}
Apply the modifier and use the Modifier.graphicsLayer to change also the text dimension.
Box(
modifier
.padding(16.dp)
.width((100 * scalex).dp)
.height((50 * scaley).dp)
.background(Color.Black, shape = RoundedCornerShape(8.dp)),
contentAlignment = Alignment.Center) {
Text("BUTTON", color = Color.White,
modifier = Modifier.graphicsLayer{
scaleX = scalex;
scaleY = scaley
})
}
Here is the ScalingButton, the onClick callback is fired when users click the button and state is reset when users move their finger out of the button area after pressing the button and not releasing it. I'm using Modifier.pointerInput function to detect user inputs:
#Composable
fun ScalingButton(onClick: () -> Unit, content: #Composable RowScope.() -> Unit) {
var selected by remember { mutableStateOf(false) }
val scale by animateFloatAsState(if (selected) 0.7f else 1f)
Button(
onClick = onClick,
modifier = Modifier
.scale(scale)
.pointerInput(Unit) {
while (true) {
awaitPointerEventScope {
awaitFirstDown(false)
selected = true
waitForUpOrCancellation()
selected = false
}
}
}
) {
content()
}
}
OR
Another approach without using an infinite loop:
#Composable
fun ScalingButton(onClick: () -> Unit, content: #Composable RowScope.() -> Unit) {
var selected by remember { mutableStateOf(false) }
val scale by animateFloatAsState(if (selected) 0.75f else 1f)
Button(
onClick = onClick,
modifier = Modifier
.scale(scale)
.pointerInput(selected) {
awaitPointerEventScope {
selected = if (selected) {
waitForUpOrCancellation()
false
} else {
awaitFirstDown(false)
true
}
}
}
) {
content()
}
}
If you want to animated button with different types of animation like scaling, rotating and many different kind of animation then you can use this library in jetpack compose. Check Here

Categories

Resources