Jetpack Compose onClick ripple is not propagating with a circular motion? - android

As can be seen in gif
when Column that contains of Text, Spacer, and LazyRowForIndexed is touched ripple is not propagating with circular motion. And it gets touched effect even when horizontal list is touched.
#Composable
fun Chip(modifier: Modifier = Modifier, text: String) {
Card(
modifier = modifier,
border = BorderStroke(color = Color.Black, width = Dp.Hairline),
shape = RoundedCornerShape(8.dp)
) {
Row(
modifier = Modifier.padding(start = 8.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier.preferredSize(16.dp, 16.dp)
.background(color = MaterialTheme.colors.secondary)
)
Spacer(Modifier.preferredWidth(4.dp))
Text(text = text)
}
}
}
#Composable
fun TutorialSectionCard(model: TutorialSectionModel) {
Column(
modifier = Modifier
.padding(top = 8.dp)
.clickable(onClick = { /* Ignoring onClick */ })
.padding(16.dp)
) {
Text(text = model.title, fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h6)
Spacer(Modifier.preferredHeight(8.dp))
Providers(AmbientContentAlpha provides ContentAlpha.medium) {
Text(model.description, style = MaterialTheme.typography.body2)
}
Spacer(Modifier.preferredHeight(16.dp))
LazyRowForIndexed(items = model.tags) { _: Int, item: String ->
Chip(text = item)
Spacer(Modifier.preferredWidth(4.dp))
}
}
}
#Preview
#Composable
fun TutorialSectionCardPreview() {
val model = TutorialSectionModel(
clazz = MainActivity::class.java,
title = "1-1 Column/Row Basics",
description = "Create Rows and Columns that adds elements in vertical order",
tags = listOf("Jetpack", "Compose", "Rows", "Columns", "Layouts", "Text", "Modifier")
)
Column {
TutorialSectionCard(model)
TutorialSectionCard(model)
TutorialSectionCard(model)
}
}
What should be done to have circular effect, but not when list itself or an item from list is touched, or scrolled?

You have to apply a Theme to your composable, which in turn provides a default ripple factory, or you have to set the ripple explicitly:
#Preview
#Composable
fun TutorialSectionCardPreview() {
MaterialTheme() {
Column {
TutorialSectionCard
...
}
}
}
or
Column(
modifier = Modifier
.padding(top = 8.dp)
.clickable(
onClick = { /* Ignoring onClick */ },
indication = rememberRipple(bounded = true)
)
.padding(16.dp)
) {
// content
}
(As of compose version 1.0.0-alpha09 there seems to be no way to prevent the ripple from showing when content is scrolled)

I'm using this approach:
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(
color = Color.Black
),
onClick = {
}
)

I also figured out how to keep ripple only for the card not the scrollable list that contains tags. To prevent ripple only move through cards use a Box which places it's children as a stack, and add clickable to section that contains header and text.
#Composable
fun TutorialSectionCard(
model: TutorialSectionModel,
onClick: ((TutorialSectionModel) -> Unit)? = null
) {
Card(
modifier = Modifier.padding(vertical = 3.dp, horizontal = 8.dp),
elevation = 1.dp,
shape = RoundedCornerShape(8.dp)
) {
Box(
contentAlignment = Alignment.BottomStart
) {
TutorialContentComponent(onClick, model)
TutorialTagsComponent(model)
}
}
}
#Composable
private fun TutorialContentComponent(
onClick: ((TutorialSectionModel) -> Unit)?,
model: TutorialSectionModel
) {
Column(Modifier
.clickable(
onClick = { onClick?.invoke(model) }
)
.padding(16.dp)
) {
Text(
text = model.title,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h6
)
// Vertical spacing
Spacer(Modifier.height(8.dp))
// Description text
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text(model.description, style = MaterialTheme.typography.body2)
}
// Vertical spacing
Spacer(Modifier.height(36.dp))
}
}
#Composable
private fun TutorialTagsComponent(model: TutorialSectionModel) {
Column(Modifier.padding(12.dp)) {
// Horizontal list for tags
LazyRow(content = {
items(model.tags) { tag ->
TutorialChip(text = tag)
Spacer(Modifier.width(8.dp))
}
})
}
}

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.

Jetpack compose - Lazy Column crashes on Recompose

Hi I am using Jetpack Compose to create a Heterogenous list. I was successful in implementing it. My requirement is when I try to click an Item in the list, I need to recompose the list. The app crashes when tried to refresh the list with below exception:
java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a header before the list of items please add a header as a separate item() before the main items() inside the LazyColumn scope. There are could be other reasons for this to happen: your ComposeView was added into a LinearLayout with some weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a custom layout. Please try to remove the source of infinite constraints in the hierarchy above the scrolling container
My code is below:
#Composable fun PrepareOverViewScreen(overViewList: List<OverViewListItem>) {
Scaffold(topBar = { TopBar("OverView") },
content = { DisplayOverViewScreen(overViewList = overViewList) },
backgroundColor = Color(0xFFf2f2f2)
)
}
#Composable fun DisplayOverViewScreen(
modifier: Modifier = Modifier, overViewList: List<OverViewListItem>
) {
LazyColumn(modifier = modifier) {
items(overViewList) { data ->
when (data) {
is OverViewHeaderItem -> {
HeaderItem(data,overViewList)
}
}
}
}
}
HeaderItem Composable Function is below :
#Composable fun HeaderItem(overViewHeaderItem: OverViewHeaderItem,overViewList: List<OverViewListItem>) { <br>
var angle by remember {
mutableStateOf(0f)
}
var canDisplayChild by remember {
mutableStateOf(false)
}
**if(canDisplayChild){
HandleHistoryTodayChild(canDisplayChild = true,overViewList)
}**
when (overViewHeaderItem.listType) {
ItemType.IN_PROGRESS_HEADER -> {
Column(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffdc8633)),
verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start
) {
Text(
text = "In Progress", color = Color.White,
modifier = Modifier.padding(start = 16.dp)
)
}
}
ItemType.HISTORY_TODAY_HEADER -> {
Column(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffd7d7d7)),
verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start
) {
Text(
text = stringResource(R.string.history_label), color = Color.Black,
modifier = Modifier.padding(start = 16.dp)
)
}
}
else -> {
Row(modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffd7d7d7)),
horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.history_yesterday), color = Color.Black,
modifier = Modifier.padding(start = 16.dp)
)
Spacer(Modifier.weight(1f))
**Image(
painter = painterResource(id = R.drawable.ic_expand_more),
modifier = Modifier
.padding(end = 5.dp)
.rotate(angle)
.clickable {
angle = (angle + 180) % 360f
canDisplayChild = !canDisplayChild
},
contentDescription = "Expandable Image"
)**
}
}
}
}
Handle History info where recomposition is called
#Composable
fun HandleHistoryTodayChild(canDisplayChild:Boolean,overViewList: List<OverViewListItem>) {
if(canDisplayChild){
**PrepareOverViewScreen(overViewList = overViewList)**
}
}
Your problem should be:
Spacer(Modifier.weight(1f))
Try to specify a fixed height and work your solution from there.

Highlight only one Card in Compose through selection

i have a Compose LazyColumnList which shows the content (from a List) in single cards. I want to highlight the selected card only. Now when i select another card, the first card stays highlighted.
Here is the code:
...
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(
items = sql_quran,
itemContent = { SurahListItem(quran_einzeln = it, navController)
})
}
#Composable
fun SurahListItem(
quran_einzeln: ItemData_quran,
navController: NavController,
) {
var selectedCard by remember { mutableStateOf(false) }
var cardColor = if (selectedCard) Blue else White;
// var cardColor= if (selectedCard) LightGray else White
Card(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 8.dp).fillMaxWidth().fillMaxSize(),
elevation = 2.dp,
shape = RoundedCornerShape(corner = CornerSize(16.dp)),
backgroundColor = cardColor,
onClick = {
selectedCard=!selectedCard
}
)
All state changes are happening locally within your SurahListItem and nothing tells your LazyColumn to perform any updates to its item children every time you click one of them.
I can't compile your entire code, so I just assumed some parts of it resulting to the codes below. I hoisted the "selection state" that you want one level above from your SurahListItem and have the composable that contains the LazyColumn do the updates.
You can copy-and-paste all of these in a single .kt file and you'll be able to run it separately from some root composable screen/content.
data class ItemData_quran(val content: String)
#Composable
fun MyCards( sql_quran: List<ItemData_quran>) {
var selectedItem by remember {
mutableStateOf<ItemData_quran?>(null)
}
LazyColumn(
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp)
) {
items(
items = sql_quran,
itemContent = { surah ->
SurahListItem(
quran_einzeln = surah,
setSelected = selectedItem?.let {
surah.content == it.content
} ?: false
) {
selectedItem = it
}
}
)
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun SurahListItem(
quran_einzeln: ItemData_quran,
setSelected: Boolean,
isSelected: (ItemData_quran?) -> Unit
) {
val cardColor = if (setSelected) Blue else White
Card(
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 8.dp)
.wrapContentSize(),
elevation = 2.dp,
shape = RoundedCornerShape(corner = CornerSize(16.dp)),
backgroundColor = cardColor,
onClick = {
isSelected(if(!setSelected) quran_einzeln else null)
}
) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(text = quran_einzeln.content)
}
}
}
Usage:
MyCards(
listOf(
ItemData_quran("Item 1"),
ItemData_quran("Item 2"),
ItemData_quran("Item 3"),
ItemData_quran("Item 4"),
ItemData_quran("Item 5"),
ItemData_quran("Item 6"),
ItemData_quran("Item 7"),
)
)

How to remove padding between AlertDialog and title/text with Compose

Using compose, I want to create something like this :
Problem is using compose AlertDialog I only achieve to get this :
There is a padding between the AlertDialog border and the title that can not be removed. How to remove it using Compose ?
Using Modifier.padding(all = 0.dp) doesn't work. I tried it at every level.
Properties "title" and "text" are wrapped with AlertDialogBaselineLayout that is adding padding. You can't remove it.
I have made a custom Composable that is acting as an AlertDialog and doesn't use AlertDialogBaselineLayout.
You can copy/paste it and edit it according to your needs :
NoPaddingAlertDialog :
#Composable
fun NoPaddingAlertDialog(
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: #Composable (() -> Unit)? = null,
text: #Composable (() -> Unit)? = null,
confirmButton: #Composable () -> Unit,
dismissButton: #Composable (() -> Unit)? = null,
shape: Shape = MaterialTheme.shapes.medium,
backgroundColor: Color = MaterialTheme.colors.surface,
contentColor: Color = contentColorFor(backgroundColor),
properties: DialogProperties = DialogProperties()
) {
Dialog(
onDismissRequest = onDismissRequest,
properties = properties
) {
Surface(
modifier = modifier,
shape = shape,
color = backgroundColor,
contentColor = contentColor
) {
Column(
modifier = Modifier
.fillMaxWidth()
) {
title?.let {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
val textStyle = MaterialTheme.typography.subtitle1
ProvideTextStyle(textStyle, it)
}
}
text?.let {
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) {
val textStyle = MaterialTheme.typography.subtitle1
ProvideTextStyle(textStyle, it)
}
}
Box(
Modifier
.fillMaxWidth()
.padding(all = 8.dp)
) {
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
dismissButton?.invoke()
confirmButton()
}
}
}
}
}
}
You can then easily use it this way :
#Composable
fun MyCustomAlertDialog(openDialog: MutableState<Boolean> = mutableStateOf(true)) {
if (openDialog.value) {
NoPaddingAlertDialog(
title = {
Surface(
color = Color.Blue,
contentColor = Color.White,
modifier = Modifier
.fillMaxWidth()
) {
Text(
text = " Popup Title️",
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 16.dp),
)
}
},
text = {
Column(Modifier.fillMaxWidth()) {
OutlinedTextField(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = 8.dp, vertical = 16.dp)
.fillMaxWidth(),
value = "Alert Dialog content ...",
onValueChange = { },
label = { Text(text = "Content") },
)
}
},
onDismissRequest = { /*TODO*/ },
confirmButton = {
PopupButton(title = "Ok", setShow = {
openDialog.value = false
})
},
dismissButton = {
PopupButton(
title = "Cancel",
setShow = {
openDialog.value = false
}
)
}
)
}
}
And get this :
You can use a Dialog() instead of AlertDialog(). It allows you to pass in a composable as the content parameter, so you can set whatever padding you want. Just don't forget to set the background color in the root composable of the content, since it is transparent by default.

Jetpack Compose: Textfield and FAB not using full width

I am trying to put a TextField and a FAB inside a bottomBar using Jetpack Compose.
I wrapped the two with a box, which has the modifier "fillMaxWidth".
But the two controls dont use the full width.
Does anyone know, how to fix this issue?
Here is my Code:
#Composable
fun ChatView() {
Scaffold(
topBar= { ChannelButton() },
bottomBar = { ChatBox() },
modifier = Modifier
.padding(10.dp)
) {
ChatList()
}
}
#Composable
fun ChatBox() {
Box(modifier = Modifier
.background(DiscordDarkGray)
.fillMaxWidth()
){
Column(modifier = Modifier
.padding(10.dp)
.fillMaxWidth()) {
HorizontalCenteredRow(modifier = Modifier
.fillMaxWidth()) {
val textState = remember { mutableStateOf(TextFieldValue()) }
TextField(
value = textState.value,
onValueChange = { textState.value = it }
)
Spacer(modifier = Modifier.width(10.dp))
FloatingIconActionButton (
icon = Icons.Default.Send,
onClick = { /*TODO*/ },
backgroundColor = DiscordBlue
)
}
Spacer(modifier = Modifier.height(60.dp))
}
}
}
Here is the Code of the HorizontalCenteredRow:
#Composable
fun HorizontalCenteredRow(
modifier: Modifier = Modifier,
content: #Composable RowScope.() -> Unit
) {
Row(
modifier = modifier
.wrapContentSize(Alignment.Center),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
content = content
)
}
Here is the code of the FAB:
#Composable
fun FloatingIconActionButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
shape: Shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)),
backgroundColor: Color = MaterialTheme.colors.secondary,
contentColor: Color = contentColorFor(backgroundColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
icon: ImageVector = Icons.Default.QuestionAnswer,
iconContentDescription: String = "",
) {
Surface(
modifier = modifier.let {
if (enabled) {
it.clickable(
onClick = onClick,
role = Role.Button,
interactionSource = interactionSource,
indication = null
)
} else it
},
shape = shape,
color = backgroundColor,
contentColor = contentColor,
elevation = elevation.elevation(interactionSource).value
) {
CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) {
ProvideTextStyle(MaterialTheme.typography.button) {
Box(
modifier = Modifier
.defaultMinSize(minWidth = FabSize, minHeight = FabSize)
.indication(interactionSource, rememberRipple()),
contentAlignment = Alignment.Center
) {
Icon(
icon,
iconContentDescription,
tint = if (enabled) {
colors().onPrimary
} else {
colors().onPrimary.transparentize(.6f)
}
)
}
}
}
}
}
Using
HorizontalCenteredRow(
modifier = Modifier.fillMaxWidth()
the Row fills all the space, but it doesn't mean that the children occupy the entire space.
If you want to fill all the space with the children you have to change the default dimensions of them.
For example you can apply modifier = Modifier.weight(1f) to the TextField.
Something like:
HorizontalCenteredRow(modifier = Modifier
.fillMaxWidth()) {
val textState = remember { mutableStateOf(TextFieldValue()) }
TextField(
value = textState.value,
onValueChange = { textState.value = it },
modifier = Modifier.weight(1f)
)

Categories

Resources