I try to create a tappable surface in Jetpack Compose where the elevation changes when the user taps the surface. The following code already works:
var tapped by remember { mutableStateOf(false) }
val elevation by animateDpAsState(
targetValue = if (tapped) 0.dp else 5.dp,
animationSpec = tween(50)
)
Surface(
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.padding(16.dp)
.requiredSize(150.dp)
.pointerInput(Unit) {
detectTapGestures(onPress = {
tapped = true
tryAwaitRelease()
tapped = false
})
},
elevation = elevation
) {
...
}
However I would like to have a ripple effect during the tap. How could I achieve this?
The default button/surface onClick and clickable is not suitable because it only handles press inputs but no taps.
You use Modifier.indication to add ripple effect, and pass events with interactionSource to update it state like this:
var tapped by remember { mutableStateOf(false) }
val interactionSource = remember { MutableInteractionSource() }
Surface(
modifier = Modifier
.indication(interactionSource, LocalIndication.current)
.pointerInput(Unit) {
detectTapGestures(onPress = { offset ->
tapped = true
val press = PressInteraction.Press(offset)
interactionSource.emit(press)
tryAwaitRelease()
interactionSource.emit(PressInteraction.Release(press))
tapped = false
})
}
) {
}
Related
How to set Background Color for Material3 Card in Android Compose?
Piggy backing fro this question. The answers tells how to set a background color.
When material3 card is pressed, it changes color with a ripple effect.
But how can I change the effect color when it is pressed?
CardDefaults.cardColors(....) doesn't do it
The Card with the onClick variant uses internally an indication = rememberRipple(). This creates and remembers a Ripple using values provided by RippleTheme.
You can provide a custom LocalRippleTheme to override the default behaviour:
CompositionLocalProvider(LocalRippleTheme provides GreenRippleTheme) {
Card(
onClick = { /* Do something */ },
modifier = Modifier.size(width = 180.dp, height = 100.dp)
) {
//Card content
}
}
with:
private object GreenRippleTheme : RippleTheme {
#Composable
override fun defaultColor() = Color.Green
#Composable
override fun rippleAlpha(): RippleAlpha = RippleTheme.defaultRippleAlpha(
Color.Green,
lightTheme = true
)
}
Otherwise you can use the clickable modifier:
val interactionSource = remember { MutableInteractionSource() }
Card(
modifier = Modifier
.size(width = 180.dp, height = 100.dp)
.clickable (
onClick = { /* Do something */ },
interactionSource = interactionSource,
indication = rememberRipple(color = Green )
)
) {
//Card content
}
Finally if you want to modify the background color when the Card is pressed (not the ripple effect) you can use:
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
val backgroundColor = if (isPressed) Yellow else MaterialTheme.colorScheme.surfaceVariant
Card(
interactionSource = interactionSource,
onClick = { /* Do something */ },
modifier = Modifier
.size(width = 180.dp, height = 100.dp),
colors = CardDefaults.cardColors(
containerColor = backgroundColor
)
) {
//Card content
}
You can use the "onClick" property of the Card component to change the color when it is pressed. To do this, you can define a state variable to track the current color of the card and toggle it on click. For example:
var cardColor by remember { mutableStateOf(Color.White) }
Card(
color = cardColor,
onClick = {
cardColor = if (cardColor == Color.White) Color.Green else Color.White
}
...
)
Alternatively, you can define the ripple color in the Modifier property of the Card component. For example:
Card(
color = Color.White,
modifier = Modifier.clickable(onClick = {
// logic to change color
}).ripple(color = Color.Green),
...
)
This question already has answers here:
Why don't Indication work for Button or Icons?
(2 answers)
Closed 6 months ago.
I have seen that we can disable the ripple effect of a view with the clickable(interactionSource, indication) inside for example a row or column but my question is that if we can disable it from a Button or FloatingActionButton
I see that FloatingActionButton has an interactionSource attribute and I have tried this
FloatingActionButton(
modifier = Modifier
.size(40.dp),
onClick = {
buttonState = when (buttonState) {
ButtonState.PRESSED -> ButtonState.UNPRESSED
ButtonState.UNPRESSED -> ButtonState.PRESSED
}
},
interactionSource = remember {
MutableInteractionSource()
})
this is not working to disable the ripple effect.
Then I have tried with the indication modifier like this
FloatingActionButton(
modifier = Modifier
.size(40.dp)
.indication(
interactionSource = remember {
MutableInteractionSource()
},
indication = null
),
onClick = {
buttonState = when (buttonState) {
ButtonState.PRESSED -> ButtonState.UNPRESSED
ButtonState.UNPRESSED -> ButtonState.PRESSED
}
})
also is not working, and then last thing I tried is adding the .clickable(...) in the modifier of the Fab button but I think that is pointless since the button has its own onClick event.
All the cases above yields to this
Is there anyway from any Button to disable its ripple effect without adding a Column or Box with a clickable attribute into its modifier ?
You can change ripple o by providing RippleTheme
private class CustomRippleTheme : RippleTheme {
#Composable
override fun defaultColor(): Color = Color.Unspecified
#Composable
override fun rippleAlpha(): RippleAlpha = RippleAlpha(
draggedAlpha = 0f,
focusedAlpha = 0f,
hoveredAlpha = 0f,
pressedAlpha = 0f,
)
}
Demo
#Composable
private fun RippleDemo() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(50.dp)
) {
Button(onClick = { /*TODO*/ }) {
Text("Button with ripple", fontSize = 20.sp)
}
Spacer(Modifier.height(20.dp))
FloatingActionButton(onClick = { /*TODO*/ }) {
Icon(imageVector = Icons.Filled.Add, contentDescription = null)
}
Spacer(Modifier.height(20.dp))
CompositionLocalProvider(LocalRippleTheme provides CustomRippleTheme()) {
Button(onClick = { /*TODO*/ }) {
Text("Button with No ripple", fontSize = 20.sp)
}
Spacer(Modifier.height(20.dp))
FloatingActionButton(onClick = { /*TODO*/ }) {
Icon(imageVector = Icons.Filled.Add, contentDescription = null)
}
}
}
}
Result
A Button, internally, is just a surface with modifications applied to it to make it clickable. It has a default indication set within the implementation, hence cannot be "turned off" at the calling site.
Just pull up the source code and remove the indication, storing the resultant inside a new Composable.
Just do a quick Ctrl+Left Click on the text "Button" in Studio, and it'll take you there.
I want to display a ripple affect after click on a view and also change it alpha after click on it.
However, the ripple effect only work well if alpha change from 0.5->1, when alpha change from 1->0.5, the ripple effect don't display fully.
fun Greeting2(name: String) {
val isProcessing = remember { mutableStateOf(false) }
Column(
modifier = Modifier
.padding(36.dp)
.alpha(if (isProcessing.value) 0.5f else 1f)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) {
isProcessing.value = !isProcessing.value
}
) {
Image(
painter = painterResource(R.drawable.ic_btn_speak_now),
contentDescription = "",
modifier = Modifier
.width(80.dp)
.height(80.dp)
)
Text(text = "Hello $name!")
}
}
Here is the demo. Any way to achieve both alpha and ripple effect together?
I guess it might be because of the mechanism how the ripple is displayed internally. Maybe it's a sort of clash between the recompositions occuring because of both the changing alpha and the propagating ripple. To fix that, you can just wrap your column in another composable, like so.
#Preview
#Composable
fun Greeting2() {
val name = "Android!" // I used preview so had to remove the parameter
var isProcessing by remember { mutableStateOf(false) }
val alpha by animateFloatAsState(targetValue = if (isProcessing) 0.5f else 1f, animationSpec = keyframes { durationMillis = 1 })
Box(
Modifier
.alpha(alpha)
){
Column(
modifier = Modifier
.padding(36.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
) {
isProcessing = !isProcessing
}
) {
Image(
painter = painterResource(R.drawable.ic_launcher_foreground),
contentDescription = "",
modifier = Modifier
.width(80.dp)
.height(80.dp)
)
Text(text = "Hello $name!")
}
}
}
I might have made some modifications but you get the idea. Also, I wrapped in a Box instead of a Surface since it defaults to a background.
I have button to record voice so I want it to start record when user press it and stop when he leave it
#Composable
fun Screen(){
Button(){
Text("record")
}
}
If you're asking just about the press/release actions, I don't know how to do this with a button, but you can achieve the same result using a Box (for instance) and use some modifiers to design it the way you want...
Here is a possible solution.
#Composable
fun TestButton() {
var isPressed by remember {
mutableStateOf(false)
}
Column {
Box(
Modifier
.pointerInput(Unit) {
detectTapGestures(
onPress = {
try {
isPressed = true
// Start recording here
awaitRelease()
} finally {
isPressed = false
// Stop recording here
}
},
)
}
.background(
MaterialTheme.colors.primary.copy(alpha = if (isPressed) .88f else 1f),
MaterialTheme.shapes.small
)
.padding(vertical = 8.dp, horizontal = 16.dp)
) {
Text(
text = "Press me!",
Modifier.align(Alignment.Center),
color = MaterialTheme.colors.onPrimary
)
}
Text(text = if (isPressed) "Pressed" else "Unpressed")
}
}
Notice that I'm using a Box with a similar design of a Button.
Here is the result:
To get the press/release actions in a Button you can use the InteractionSource.collectIsPressedAsState to know if the Button is pressed.
You can add a side effect to know when the Button is released.
Something like:
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
if (isPressed){
println("Pressed")
//Use if + DisposableEffect to wait for the press action is completed
DisposableEffect(Unit) {
onDispose {
println("released")
}
}
}
Button(onClick={},
interactionSource = interactionSource
){
Text("record")
}
Building a simple keyboard is fairly simple and straightforward in Jetpack Compose.
I built a really simple KeyRow by using this:
Key.kt
#Composable
fun Key(modifier: Modifier = Modifier, label: String, onClick: () -> Unit) {
val shape = RoundedCornerShape(4.dp)
//TODO: make clickable outside but don't show ripple
Box(modifier = modifier
.padding(2.dp)
.clip(shape)
.clickable(onClick = onClick)
.background(Color.White)
.padding(vertical = 12.dp, horizontal = 4.dp), contentAlignment = Alignment.Center) {
Text(text = label, fontSize = 20.sp)
}
}
KeyRow.kt
#Composable
fun KeyRow(keys: List<String>) {
Row(modifier = Modifier.fillMaxWidth().background(color = grey200)) {
keys.forEach {
Key(modifier = Modifier.weight(1f), label = it, onClick = { })
}
}
}
That's what it looks like:
I want to achieve this animation:
However, I'm currently stuck with this
![4]
Hierachy
-Keyboard
--KeyRow
---KeyLayout
----Key
----KeyPressedOverlay (only visible when pressed)
My main problem is that I don't know how to show the KeyPressedOverlay Composale (which is larger than the Key Composable) without making the parent Layout larger. As a result, I need to overflow the parent layout in some way.
Not sure if it's the best way (probably not), but I found a solution using ConstraintLayout...
val keys = listOf("A", "B", "C", "D")
ConstraintLayout(
modifier = Modifier.graphicsLayer(clip = false)
) {
val refs = keys.map { createRef() }
refs.forEachIndexed { index, ref ->
val modifier = when (index) {
0 -> Modifier.constrainAs(ref) {
start.linkTo(parent.start)
}
refs.lastIndex -> Modifier.constrainAs(ref) {
start.linkTo(refs[index - 1].end)
end.linkTo(parent.end)
}
else -> Modifier.constrainAs(ref) {
start.linkTo(refs[index - 1].end)
end.linkTo(refs[index + 1].start)
}
}
val modifierPressed = Modifier.constrainAs(createRef()) {
start.linkTo(ref.start)
end.linkTo(ref.end)
bottom.linkTo(ref.bottom)
}
KeyboardKey(
keyboardKey = keys[index],
modifier = modifier,
modifierPressed = modifierPressed,
pressed = { s -> /* Do something with the key */}
)
}
}
One important detail here is graphicLayer(clip = false) (which is similar to the clipChildren in View Toolkit). Then, I'm creating a modifier to each key and to the pressed key. Noticed that the modifierPressed is aligned to the center/bottom of the other modifier.
Finally the KeyboardKey is described below.
#Composable
fun KeyboardKey(
keyboardKey: String,
modifier: Modifier,
modifierPressed: Modifier,
pressed: (String) -> Unit
) {
var isKeyPressed by remember { mutableStateOf(false) }
Text(keyboardKey, Modifier
.then(modifier)
.pointerInput(Unit) {
detectTapGestures(onPress = {
isKeyPressed = true
val success = tryAwaitRelease()
if (success) {
isKeyPressed = false
pressed(keyboardKey)
} else {
isKeyPressed = false
}
})
}
.background(Color.White)
.padding(
start = 12.dp,
end = 12.dp,
top = 16.dp,
bottom = 16.dp
),
color = Color.Black
)
if (isKeyPressed) {
Text(
keyboardKey, Modifier
.then(modifierPressed)
.background(Color.White)
.padding(
start = 16.dp,
end = 16.dp,
top = 16.dp,
bottom = 48.dp
),
color = Color.Black
)
}
}
This is the result I got:
Edit:
Adding some more logic, I was able to get this...
I hope it helps this time ;)
Here's the gist just in case...
https://gist.github.com/nglauber/4cb1573efba9024c008ea71f3320b4d8
I guess you're looking for the pressIndicatorGestureFilter modifier...
I tried this and worked for me...
var pressed by remember { mutableStateOf(false) }
val padding = if (pressed) 32.dp else 16.dp
Text("A", Modifier
.pressIndicatorGestureFilter(
onStart = {
pressed = true
},
onStop = {
pressed = false
},
onCancel = {
pressed = false
}
)
.background(Color.White)
.padding(start = 16.dp, end = 16.dp, top = padding, bottom = padding)
)