Constraining a Composable to the bottom of a Column - android

Me and my team are new to Compose and we're trying to build a TopUp screen for a client. The screen consists of a Column that contains some Padding (in the form of a Composable) for the TopAppBar, two Card composables, a button, then a footer composable which I need to constraint to the bottom of the column, and finally, some padding (again in the form of a composable) to give us some space between the content and the bottom navbar.
Right now we're using a spacer with a fixed dp value but obviously, this won't scale so that it is constrained to the bottom on all devices. What would be the best way to go about achieving such a look?
screen composable:
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun TopUpScreen() {
Column(
horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(horizontal = 16.dp)
) {
val viewModel = getViewModel<TopUpViewModel>()
TopBarPadding()
TopUpCardView(title = stringResource(id = R.string.choose_topup_amount)) {
var selectedCardIndex by remember { mutableStateOf(-1) }
Row(
horizontalArrangement = Arrangement.SpaceAround,
modifier = Modifier
.fillMaxWidth()
) {
Card(
shape = RoundedCornerShape(11.dp),
elevation = if (selectedCardIndex == 0) 1.dp else 0.dp,
backgroundColor = if (selectedCardIndex == 0) colorResource(id = R.color.bottom_modal_drawer_background) else colorResource(
id = R.color.more_screen_item_background
),
modifier = Modifier
.width(71.dp)
.height(56.dp)
.shadow(
if (selectedCardIndex == 0) 8.dp else 0.dp,
shape = RoundedCornerShape(11.dp)
)
.clickable {
selectedCardIndex = 0
viewModel.topUpAmount = 20.0
}
) {
Text(
"€20",
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center,
modifier = Modifier.wrapContentHeight()
)
}
Card(
shape = RoundedCornerShape(11.dp),
elevation = if (selectedCardIndex == 1) 1.dp else 0.dp,
backgroundColor = if (selectedCardIndex == 1) colorResource(id = R.color.bottom_modal_drawer_background) else colorResource(
id = R.color.more_screen_item_background
),
modifier = Modifier
.width(71.dp)
.height(56.dp)
.shadow(
if (selectedCardIndex == 1) 8.dp else 0.dp,
shape = RoundedCornerShape(11.dp)
)
.clickable {
selectedCardIndex = 1
viewModel.topUpAmount = 40.0
}
) {
Text(
"€40",
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center,
modifier = Modifier.wrapContentHeight()
)
}
Card(
shape = RoundedCornerShape(11.dp),
elevation = if (selectedCardIndex == 2) 1.dp else 0.dp,
backgroundColor = if (selectedCardIndex == 2) colorResource(id = R.color.bottom_modal_drawer_background) else colorResource(
id = R.color.more_screen_item_background
),
modifier = Modifier
.width(71.dp)
.height(56.dp)
.shadow(
if (selectedCardIndex == 2) 8.dp else 0.dp,
shape = RoundedCornerShape(11.dp)
)
.clickable {
selectedCardIndex = 2
viewModel.topUpAmount = 70.0
}
) {
Text(
"€70",
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center,
modifier = Modifier.wrapContentHeight()
)
}
Card(
shape = RoundedCornerShape(11.dp),
elevation = if (selectedCardIndex == 3) 1.dp else 0.dp,
backgroundColor = if (selectedCardIndex == 3) colorResource(id = R.color.bottom_modal_drawer_background) else colorResource(
id = R.color.more_screen_item_background
),
modifier = Modifier
.width(71.dp)
.height(56.dp)
.shadow(
if (selectedCardIndex == 3) 8.dp else 0.dp,
shape = RoundedCornerShape(11.dp)
)
.clickable {
selectedCardIndex = 3
viewModel.topUpAmount = 100.0
}
) {
Text(
"€100",
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Center,
modifier = Modifier.wrapContentHeight()
)
}
}
}
Spacer(modifier = Modifier.padding(16.dp))
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
TopUpCardView(title = stringResource(id = R.string.enter_custom_topup_amount)) {
var customAmountTxt by remember { mutableStateOf(TextFieldValue()) }
TextField(
value = customAmountTxt,
onValueChange = {
customAmountTxt = it
},
maxLines = 1,
singleLine = true,
leadingIcon = {
Icon(
painter = painterResource(id = R.drawable.ic_euro),
contentDescription = stringResource(
R.string.euro_icon_desc
),
modifier = Modifier.padding(
start = 16.dp,
end = 16.dp,
top = 12.dp,
bottom = 12.dp
)
)
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = {
focusManager.clearFocus()
keyboardController?.hide()
}),
shape = RoundedCornerShape(6.dp),
colors = TextFieldDefaults.textFieldColors(
backgroundColor = colorResource(id = R.color.white),
textColor = colorResource(id = R.color.black),
focusedIndicatorColor = colorResource(id = R.color.white),
unfocusedIndicatorColor = colorResource(id = R.color.white),
disabledIndicatorColor = colorResource(id = R.color.white),
cursorColor = colorResource(id = R.color.black)
),
textStyle = TextStyle(
color = Color.Black,
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
textAlign = TextAlign.Start
),
modifier = Modifier
.height(50.dp)
.fillMaxWidth()
.shadow(8.dp, shape = RoundedCornerShape(6.dp))
)
}
Spacer(modifier = Modifier.padding(32.fixedDp()))
val context = LocalContext.current //todo:sp remove when you remove the toast
MyButton(
text = stringResource(id = R.string.continue_text),
buttonType = MyButtonType.PRIMARY,
onClick = {
Toast.makeText(context, "[TODO] Navigate to card screen", Toast.LENGTH_SHORT).show()
})
//todo:sp replace the spacer implementation with something that will constraint the
// footer to the bottom as it should
Spacer(modifier = Modifier.height(130.dp))
AcceptedCardsFooter()
BottomBarPadding()
}
}
TopUpCardView:
#Composable
fun TopUpCardView(
title: String,
modifier: Modifier = Modifier,
mainBody: #Composable () -> Unit
) {
Card(
shape = RoundedCornerShape(13.dp),
elevation = 10.dp,
modifier = modifier
.fillMaxWidth()
.height(131.dp)
) {
Column(modifier = Modifier.padding(vertical = 20.dp, horizontal = 16.dp)) {
Text(
text = title,
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.padding(9.dp))
mainBody()
}
}
}
Footer:
#Composable
fun AcceptedCardsFooter(isTransparent: Boolean = false) {
Row(modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.background(
if (isTransparent) {
Color.Transparent
} else {
colorResource(id = R.color.registration_note_background)
}
)
.padding(bottom = 12.dp, top = 12.dp, start = 16.dp, end = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = LocalContext.current.getString(R.string.accepted_cards),
fontSize = 12.sp,
color = colorResource(id = R.color.Black_100)
)
Row(verticalAlignment = Alignment.CenterVertically) {
Image(painter = painterResource(id = R.drawable.visa), contentDescription = "")
Spacer(modifier = Modifier.padding(17.fixedDp()))
Image(painter = painterResource(id = R.drawable.mastercard), contentDescription = "")
Spacer(modifier = Modifier.padding(10.fixedDp()))
Image(painter = painterResource(id = R.drawable.american_express), contentDescription = "")
}
}
}

The easiest way is to use weight modifier:
Spacer(modifier = Modifier.height(130.dp).weight(1f))

Add bottom padding to first Column for give space between content and bottombar. After that give weight to second Column for make footer constraint to the bottom.
For example,
Column(
modifier = Modifier.fillMaxSize().padding(bottom = 15.dp, start = 16.dp, end = 16.dp )
) {
Column(
modifier = Modifier.fillMaxSize().weight(1f).padding(horizontal = 16.dp)
) {
// top bar
// two cards
// button
}
// add footer here
}

I would use a ConstraintLayout and wrap the view you need to always be at the bottom of the screen.
ConstraintLayout {
// Create references for the composables to constrain
val id_of_your_footer = createRefs()
YourFooterComposable(
modifier = Modifier.constrainAs(id_of_your_footer) {
bottom.linkTo(parent.bottom)
}
)
}
As long as the parent is the root view, the Footer will be constrained to stay at the bottom of the screen.
Alternatively, you can use a scaffold that has these constraints set automatically:
Scaffold(topBar={//place a top bar composable here},
content={//all your content composables here},
bottomBar={//your footer composable here})

Related

How to dismiss a composable dialog?

I am a new in jetpack compose and I really wanted to know how I can dismiss a composable dialog. Is there any function like dismiss() for dialog in jetpack compose?
By using below code, I cannot dismiss the dialog either touching outside or pressing back button. The dialog just still is visible on the top of view hierarchy.
`
#Composable
fun InfoDialog() {
val shouldDismiss = remember {
mutableStateOf(false)
}
Dialog(onDismissRequest = {
shouldDismiss.value = false
}, properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)) {
Card(
shape = RoundedCornerShape(8.dp),
modifier = Modifier.padding(16.dp,8.dp,16.dp,8.dp),
elevation = 8.dp
) {
Column(
Modifier.background(c282534)) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Notice",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
style = TextStyle(fontWeight = FontWeight.Bold, color = Color.White, fontSize = 24.sp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Allow Permission to send you notifications when important update added.",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 8.dp, start = 24.dp, end = 24.dp)
.fillMaxWidth(),
style = TextStyle(color = Color.White, fontSize = 16.sp)
)
}
Row(
Modifier
.fillMaxWidth()
.padding(top = 8.dp),
horizontalArrangement = Arrangement.SpaceAround) {
TextButton(onClick = {
shouldDismiss.value = true
}, modifier = Modifier.weight(1f)) {
Text(
"Close",
fontWeight = FontWeight.Normal,
color = Color.White,
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
)
}
TextButton(
onClick = {
shouldDismiss.value = true
},
modifier = Modifier.weight(1f)
) {
Text(
"Allow",
fontWeight = FontWeight.ExtraBold,
color = Color.White,
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
)
}
}
}
}
}
}
`
First, you should setup onDismissRequest, I guess in your case it will be shouldDismiss.value = true. Then you should hide Dialog based on shouldDismiss value. In order to hide you should just stop invoking Dialog {... function in your code based on condition. E.g. by adding fast return if (shouldDismiss.value) return. Finally it will look like this:
#Composable
fun InfoDialog() {
val shouldDismiss = remember {
mutableStateOf(false)
}
if (shouldDismiss.value) return
Dialog(onDismissRequest = {
shouldDismiss.value = true
}, properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = true
)) {
Card(
shape = RoundedCornerShape(8.dp),
modifier = Modifier.padding(16.dp,8.dp,16.dp,8.dp),
elevation = 8.dp
) {
Column(
Modifier.background(c282534)) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Notice",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 8.dp)
.fillMaxWidth(),
style = TextStyle(fontWeight = FontWeight.Bold, color = Color.White, fontSize = 24.sp),
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
Text(
text = "Allow Permission to send you notifications when important update added.",
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 8.dp, start = 24.dp, end = 24.dp)
.fillMaxWidth(),
style = TextStyle(color = Color.White, fontSize = 16.sp)
)
}
Row(
Modifier
.fillMaxWidth()
.padding(top = 8.dp),
horizontalArrangement = Arrangement.SpaceAround) {
TextButton(onClick = {
shouldDismiss.value = true
}, modifier = Modifier.weight(1f)) {
Text(
"Close",
fontWeight = FontWeight.Normal,
color = Color.White,
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
)
}
TextButton(
onClick = {
shouldDismiss.value = true
},
modifier = Modifier.weight(1f)
) {
Text(
"Allow",
fontWeight = FontWeight.ExtraBold,
color = Color.White,
modifier = Modifier.padding(top = 8.dp, bottom = 8.dp)
)
}
}
}
}
}
}
The dialog is visible as long as it is part of the composition hierarchy.
You should use something like:
val shouldShowDialog = remember { mutableStateOf(true) }
if (shouldShowDialog.value) {
Dialog(onDismissRequest = { shouldShowDialog.value = false }) {
Button(onClick = {shouldShowDialog.value = false}){
Text("Close")
}
}
}
Setting shouldShowDialog to false dismisses the Dialog. And to show just set shouldShowDialog to true. Something like:
Button(onClick = {shouldShowDialog.value = true}){
Text("Open")
}

TextField overflow and softwrap not working with Compose Constraint Layout

I've been trying to build a list with a Card in it formatted like this:
The difficulty here is that the title e.g. "Bread" and ingredient name e.g. "Flour" can be very long and thus I want to have an ellipsis to keep things manageable i.e. "My Long Flour name" will be displayed as "My Long Flou..." or as much space as is allowed.
The picture size and the gram and percent widths are constant .dp values.
Ellipsis worked fine when it was in a Column but with ConstraintLayout it doesn't seem to work and I get this:
here's my code
#Composable
fun BakeItem(
modifier: Modifier = Modifier,
bake: Bake,
cardClicked: () -> Unit,
ingredeints: List<Ingredient>
) {
Card(
modifier = modifier
.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 8.dp)
.clickable { cardClicked() }
.fillMaxSize()
.wrapContentHeight(),
border = BorderStroke(4.dp, MaterialTheme.colorScheme.secondary),
shape = RoundedCornerShape(14.0.dp),
colors = cardColors(
containerColor = MaterialTheme.colorScheme.background
)
) {
val context = LocalContext.current
val uri = remember(bake.imagePath) { Uri.parse(bake.imagePath) }
// Card Content
ConstraintLayout(
modifier = modifier
.fillMaxSize()
.padding(start = 16.dp, top = 8.dp, end = 8.dp, bottom = 8.dp)
) {
val (titleRef, gramColRef, ingrColRef, percentColRef,
imageRef, dateRef, starsRef) = createRefs()
Text(
modifier = modifier
.padding(4.dp)
.constrainAs(titleRef) {
top.linkTo(parent.top, margin = 8.dp)
// end.linkTo(imageRef.start, margin = 8.dp)
start.linkTo(parent.start, margin = 8.dp)
}
.background(Color(0xffeeeeee)),
// textAlign = TextAlign.Left,
text = if (bake.recipeName.isEmpty()) "<Unnamed>" else bake.recipeName,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary,
softWrap = false,
overflow = TextOverflow.Ellipsis,
)
Column(
horizontalAlignment = Alignment.End,
modifier = modifier
.width(50.dp)
.constrainAs(gramColRef) {
top.linkTo(titleRef.bottom, margin = 8.dp)
end.linkTo(ingrColRef.start, margin = 8.dp)
start.linkTo(parent.start, margin = 8.dp)
}
) {
ingredeints.forEachIndexed { _, it ->
Text(
text = it.weightGram,
style = MaterialTheme.typography.titleSmall,
softWrap = false,
overflow = TextOverflow.Ellipsis
)
}
}
Column(
modifier = modifier
.constrainAs(ingrColRef) {
top.linkTo(titleRef.bottom, margin = 8.dp)
start.linkTo(gramColRef.end, margin = 8.dp)
end.linkTo(ingrColRef.start, margin = 8.dp)
},
) {
ingredeints.forEachIndexed { _, it ->
Text(
text = it.name,
style = MaterialTheme.typography.titleSmall,
softWrap = false,
overflow = TextOverflow.Ellipsis
)
}
}
Column(
modifier = modifier
.width(50.dp)
.constrainAs(percentColRef) {
top.linkTo(titleRef.bottom, margin = 8.dp)
end.linkTo(imageRef.start, margin = 8.dp)
start.linkTo(ingrColRef.end, margin = 8.dp)
},
horizontalAlignment = Alignment.End
) {
ingredeints.forEachIndexed { i, it ->
Text(
text = if (i == 0) "" else it.bakingPercent,
style = MaterialTheme.typography.titleSmall,
softWrap = false,
overflow = TextOverflow.Ellipsis
)
}
}
Text(
modifier = modifier.padding(
top = 8.dp,
start = 4.dp,
end = 4.dp,
bottom = 4.dp
),
text = bake.notes,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 3
)
if (bake.imagePath.isNotEmpty()) {
Image(
modifier = modifier
.constrainAs(imageRef) {
top.linkTo(parent.top, margin = 8.dp)
end.linkTo(parent.end, margin = 8.dp)
}
.padding(4.dp)
.requiredSize(150.dp)
.clip(RoundedCornerShape(14.dp))
.border(
4.dp,
MaterialTheme.colorScheme.primaryContainer,
RoundedCornerShape(14.dp)
),
painter = rememberAsyncImagePainter(
remember(uri) {
ImageRequest.Builder(context)
.data(uri)
// TODO, think of caching improvements
// .diskCacheKey(uri.toString() + key.value)
// .memoryCacheKey(uri.toString() + key.value)
.diskCachePolicy(CachePolicy.DISABLED)
.memoryCachePolicy(CachePolicy.DISABLED)
.build()
}
),
contentScale = ContentScale.Crop,
contentDescription = "Image of your bake"
)
} else {
Spacer(modifier = modifier
.background(Color.Blue)
.width(150.dp)
.height(10.dp)
.constrainAs(imageRef) {
top.linkTo(parent.top, margin = 8.dp)
end.linkTo(parent.end, margin = 8.dp)
})
}
Text(
modifier = modifier
.padding(4.dp)
.constrainAs(dateRef) {
bottom.linkTo(parent.bottom, margin = 8.dp)
top.linkTo(imageRef.bottom, margin = 8.dp)
end.linkTo(parent.end, margin = 8.dp)
},
text = bake.startTime.toString(),
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
// textAlign = Layout.Alignment.ALIGN_CENTER
)
createHorizontalChain(
gramColRef, ingrColRef, percentColRef, imageRef,
chainStyle = ChainStyle.SpreadInside
)
createVerticalChain(
imageRef, dateRef,
chainStyle = ChainStyle.SpreadInside
)
createHorizontalChain(
titleRef, imageRef,
chainStyle = ChainStyle.SpreadInside
)
}
}
}
In the title you have to add as constrain width = Dimension.fillToConstraints, end.linkTo(imageRef.start, margin = 8.dp) and maxLines = 1:
Text(
modifier = Modifier
.padding(4.dp)
.constrainAs(titleRef) {
top.linkTo(parent.top, margin = 8.dp)
end.linkTo(imageRef.start, margin = 8.dp)
start.linkTo(parent.start, margin = 8.dp)
width = Dimension.fillToConstraints
}
.background(Color(0xffeeeeee)),
text = "Recipe Name",
//...
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
For the list instead of using 3 Columns you do can something different.
For the ingredient name add the weight(1f) modifier and the maxLines = 1
Column(
modifier = Modifier
.constrainAs(listRef) {
top.linkTo(titleRef.bottom, margin = 8.dp)
end.linkTo(imageRef.start, margin = 8.dp)
start.linkTo(parent.start, margin = 8.dp)
width = Dimension.fillToConstraints
}
) {
//forEach Row...
Row(Modifier.fillMaxWidth()) {
Text(text = "50g", Modifier.width(xx.dp), textAlign = TextAlign.End)
Text(
text = "Very long text long text",
Modifier
.padding(start=4.dp)
.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(text = "75%", Modifier.width(xx.dp), textAlign = TextAlign.End)
}
}

Android compose vertical line connect components

I want make a UI like this picture.
What I want:
Now my result:
My code:
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
imageVector = Icons.Rounded.CheckCircle,
contentDescription = null,
modifier = Modifier
.size(16.dp)
.onGloballyPositioned {
topOffset = it.boundsInRoot().bottomCenter
}
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
fontSize = 16.sp,
)
}
Spacer(modifier = Modifier.size(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
imageVector = Icons.Rounded.CheckCircle,
contentDescription = null,
modifier = Modifier
.size(16.dp)
.onGloballyPositioned {
bottomOffset = it.boundsInRoot().bottomCenter
}
)
Spacer(modifier = Modifier.size(8.dp))
Text(
text = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
fontSize = 16.sp,
)
}
I want draw a vertical line between the CheckCircle icons.
You can have a fixed height and then draw a line behind. You should also have another icon with white arrow.
An example:
#Composable
fun checkComposable() {
// padding between rows
val padding = 50.dp
Row {
Box(Modifier.height(intrinsicSize = IntrinsicSize.Min)) {
Box(modifier = Modifier
.width(4.dp)
.padding(top = padding / 2, bottom = padding / 2)
.fillMaxHeight()
.background(Color.Black)
.align(Alignment.Center))
Column {
Box(Modifier.height(padding)) {
Image(
imageVector = Icons.Rounded.CheckCircle,
contentDescription = null,
modifier = Modifier
.size(16.dp)
.align(Alignment.Center)
)
}
Box(Modifier.height(padding)) {
Image(
imageVector = Icons.Rounded.CheckCircle,
contentDescription = null,
modifier = Modifier
.size(16.dp)
.align(Alignment.Center)
)
}
}
}
Spacer(modifier = Modifier.width(8.dp))
Column{
Box(Modifier.height(padding)) {
Text(
modifier = Modifier.align(Alignment.CenterStart),
text = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
fontSize = 16.sp,
)
}
Box(Modifier.height(padding)) {
Text(
modifier = Modifier.align(Alignment.CenterStart),
text = "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
fontSize = 16.sp,
)
}
}
}
}
A workaround, could be this:
Row {
Vector() // Complete Icon, ticks and line
Column {
Text(...)
Text(...)
}
}

Clickable modifier on Row not receiving events

I have the following page on which I'm trying to display a list of saved cards with the ability to add a new one. The last item in the column is an expandable one so that when clicked, the user can see a form for filling out card info in order to add a new card. This was working just fine yesterday, but I can't figure out why it's not working today.
The actual CardItem elements receive clicks just fine but the custom expandable one does not and neither does the ShadowWrapper parent nor the RaisedCard one.
Cards screen:
private data class CreditCard(val type: CreditCardTypes, val lastDigits: String)
#Composable
fun CardSelectionScreen(onCardSelected: () -> Unit) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.padding(vertical = 24.dp, horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
var selectedCardIndex by remember { mutableStateOf(0) }
val cardList = listOf(
CreditCard(CreditCardTypes.MASTERCARD, "3429"),
CreditCard(CreditCardTypes.VISA, "3429"),
CreditCard(CreditCardTypes.MASTERCARD, "3429")
)
TopBarPadding(true)
Spacer(modifier = Modifier.height(10.dp))
RaisedCard() {
Column(
modifier = Modifier.padding(vertical = 20.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = stringResource(id = R.string.please_select_a_card),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
textAlign = TextAlign.Start
)
Spacer(modifier = Modifier.height(9.dp))
for (i in cardList.indices) {
CreditCardItem(cardList[i],
isSelected = selectedCardIndex == i, onItemSelected = { ->
selectedCardIndex = i
})
}
ShadowWrapper( // This is the item's layout
cardElevation = 1.dp,
shadowElevation = 3.dp
) {
Column(
modifier = Modifier
.animateContentSize()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.background(
if (selectedCardIndex == cardList.size) colorResource(
id = R.color.bottom_modal_drawer_background
) else Color.White
)
.padding(horizontal = 16.dp, vertical = 16.dp)
.clickable(
indication = null,
interactionSource = MutableInteractionSource()
) { // this does not register at all, tried with Log.d
selectedCardIndex = cardList.size
},
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.ic_add_credit_card),
contentDescription = "Add credit card icon"
)
Spacer(modifier = Modifier.width(13.dp))
Text(
stringResource(id = R.string.new_card_addition),
textAlign = TextAlign.Start,
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
color = colorResource(id = R.color.Orange_100)
)
}
if (selectedCardIndex == cardList.size) {
Column(
modifier = Modifier.padding(
horizontal = 16.dp
)
) {
Spacer(modifier = Modifier.padding(22.fixedDp()))
Text(
text = LocalContext.current.getString(R.string.add_credit_card_top_msg),
fontSize = 16.sp,
fontWeight = FontWeight.Bold,
color = colorResource(id = R.color.Black_100)
)
Spacer(modifier = Modifier.padding(10.dp))
InputField(label = LocalContext.current.getString(R.string.owner_name))
Spacer(modifier = Modifier.padding(18.fixedDp()))
InputField(label = LocalContext.current.getString(R.string.credit_card_number))
Spacer(modifier = Modifier.padding(18.fixedDp()))
Row() {
Box(
modifier = Modifier
.weight(1.5f)
) {
InputField(label = LocalContext.current.getString(R.string.expiration_date))
}
Spacer(modifier = Modifier.padding(6.fixedDp()))
Box(
modifier = Modifier
.weight(1f)
) {
InputField(
label = LocalContext.current.getString(R.string.cvv),
isPassword = true,
placeholder = ""
)
}
}
Spacer(modifier = Modifier.height(34.fixedDp()))
Row() {
MyCheckbox(
modifier = Modifier.padding(top = 3.dp),
isCheckedInitially = true
)
Spacer(modifier = Modifier.width(13.dp))
Text(
stringResource(id = R.string.save_card_for_future_transactions),
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
color = colorResource(id = R.color.Black_100)
)
}
Spacer(modifier = Modifier.padding(22.fixedDp()))
}
}
}
}
Spacer(modifier = Modifier.height(2.dp))
}
}
Spacer(modifier = Modifier.height(32.dp))
MyButton(
text = stringResource(id = R.string.continue_text),
MyButtonType.PRIMARY,
onClick = { onCardSelected() }
)
Spacer(modifier = Modifier.height(20.dp))
AcceptedCardsFooter()
BottomBarPadding(true)
}
}
#Composable
private fun CreditCardItem(
cardDetails: CreditCard,
isSelected: Boolean,
onItemSelected: () -> Unit
) {
ShadowWrapper(cardElevation = 1.dp, shadowElevation = 3.dp) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.background(if (isSelected) colorResource(id = R.color.bottom_modal_drawer_background) else Color.White)
.padding(horizontal = 16.dp, vertical = 15.dp)
.clickable(indication = null, interactionSource = MutableInteractionSource()) {
onItemSelected()
},
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
MyRadioButton(label = "", selected = isSelected)
Spacer(modifier = Modifier.width(16.dp))
Box(
modifier = Modifier
.width(43.dp)
.height(33.dp)
.clip(RoundedCornerShape(4.dp))
.background(colorResource(id = R.color.Grey_10))
.padding(horizontal = 6.dp, vertical = 7.dp)
) {
Image(
painter = painterResource(id = cardDetails.type.icon),
contentDescription = "",
modifier = Modifier.align(Alignment.Center)
)
}
Spacer(modifier = Modifier.padding(8.fixedDp()))
Text(
text = "${cardDetails.type.prefix}****${cardDetails.lastDigits}",
fontSize = 16.sp,
color = colorResource(id = R.color.Black_100)
)
}
}
}
}
RaisedCard.kt:
#Composable
fun RaisedCard(
modifier: Modifier = Modifier,
mainBody: #Composable () -> Unit
) {
Card(
shape = RoundedCornerShape(13.dp),
elevation = 10.dp,
modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
) {
Column(
modifier = Modifier
.background(Color.White)
.padding(horizontal = 16.dp)
) {
mainBody()
}
}
}
ShadowWrapper.kt:
#Composable
fun ShadowWrapper(
modifier: Modifier = Modifier,
border: BorderStroke = BorderStroke(0.dp, Color.Transparent),
cardElevation: Dp = 2.dp,
shadowElevation: Dp = 1.dp,
shadowShapeRadius: Dp = 6.dp,
content: #Composable () -> Unit,
) {
Card(
elevation = cardElevation,
border = border,
shape = RoundedCornerShape(shadowShapeRadius),
modifier = modifier.shadow(shadowElevation, RoundedCornerShape(shadowShapeRadius)).wrapContentHeight()
) {
content()
}
}
I wasn't able to reproduce your issue, probably because other parts of your application are triggering additional recompositions. However the cause is most likely that you forget to remember your MutableInteractionSources.
Like this:
otherModifiers.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { // this does not register at all, tried with Log.d
selectedCardIndex = cardList.size
}
If you do not wrap MutableInteractionSource in a remember, a new instance is created on every recomposition, so state like a previous touch events is lost

Jetpack compose code to scroll down to the position of a specific UI element on clicking a Text

I am trying to scroll down to the position of a specific UI element on clicking a Text.
The code for my Text is:
Text(
"What is autosaving?",
color = colorResource(id = R.color.text_highlight),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._11ssp).toSp()
},
fontFamily = FontFamily(
Font(R.font.poppins_regular)
),
modifier = Modifier.constrainAs(whatIsAutosaving) {
top.linkTo(glWhatIsAutoSaving)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
)
On clicking this Text my screen should scroll to the beginning position of another Text. The code for this another Text is:
Text(
stringResource(id = R.string.autosave_info),
color = colorResource(id = R.color.bright_green),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._11ssp).toSp()
}, fontFamily = FontFamily(
Font(R.font.poppins_regular)
),
modifier = Modifier.constrainAs(autoSaveInfo) {
top.linkTo(glAutoSaveInfo)
start.linkTo(glLeft)
end.linkTo(glRight)
width = Dimension.fillToConstraints
},
)
How do I achieve this?
EDIT:
The complete code for my screen is:
#Composable
fun Autosave(navController: NavController) {
val query = remember { mutableStateOf("") }
val errorMsg = remember { mutableStateOf(false) }
Box(
modifier = Modifier
.background(color = MaterialTheme.colors.background)
.fillMaxSize()
) {
ConstraintLayout(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
val (logo, illustration, title, triangle, slider, percent, maxLimitTxt,
maxLimitTextField, buttonSave, whatIsAutosaving, autoSaveInfo, progressBar,
detailsRow, iconUp, spacer, error) = createRefs()
val glLogo = createGuidelineFromTop(0.0075f)
val glIllustrationTop = createGuidelineFromTop(0.0235f)
val glIllustrationBottom = createGuidelineFromTop(0.045f)
val glIllustrationLeft = createGuidelineFromStart(0.27f)
val glIllustrationRight = createGuidelineFromEnd(0.27f)
val glTitle = createGuidelineFromTop(0.053f)
val glSlider = createGuidelineFromTop(0.062f)
val glMaxLimitTxt = createGuidelineFromTop(0.086f)
val glMaxLimitTextField = createGuidelineFromTop(0.09f)
val glButtonSaveTop = createGuidelineFromTop(0.11f)
val glButtonSaveBottom = createGuidelineFromTop(0.12f)
val glWhatIsAutoSaving = createGuidelineFromTop(0.125f)
val glAutoSaveInfo = createGuidelineFromTop(0.175f)
val glSpacer = createGuidelineFromTop(0.99f)
val glLeft = createGuidelineFromStart(0.1f)
val glRight = createGuidelineFromEnd(0.1f)
val glRightIcon = createGuidelineFromEnd(0.825f)
val glLeftTextField = createGuidelineFromStart(0.3f)
val glRightTextField = createGuidelineFromEnd(0.3f)
val coroutineScope = rememberCoroutineScope()
val scrollState = rememberScrollState()
var scrollToPosition by remember { mutableStateOf(0F) }
Image(
painter = painterResource(id = R.drawable.effect_app_bg_720),
contentDescription = "effect top",
modifier = Modifier
.fillMaxSize()
.scale(1.325f)
)
Image(
painter = painterResource(id = R.drawable.logo_voodlee),
contentDescription = "logo", modifier = Modifier
.constrainAs(logo) {
top.linkTo(glLogo)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
Image(
painter = painterResource(id = R.drawable.img_autosaving),
contentDescription = "autosave image",
modifier = Modifier.constrainAs(illustration) {
top.linkTo(glIllustrationTop)
bottom.linkTo(glIllustrationBottom)
start.linkTo(glIllustrationLeft)
end.linkTo(glIllustrationRight)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
}
)
Text(
"Set the percentage for autosaving",
color = colorResource(id = R.color.bright_green),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._13ssp).toSp()
}, fontFamily = FontFamily(
Font(R.font.poppins_regular)
),
modifier = Modifier.constrainAs(title) {
top.linkTo(glTitle)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
Image(
painter = painterResource(id = R.drawable.ic_triangle_dn),
modifier = Modifier
.height(39.dp)
.width(29.dp)
.constrainAs(triangle) {
top.linkTo(title.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
contentDescription = "triangle down"
)
Column(modifier = Modifier.constrainAs(slider) {
top.linkTo(glSlider)
start.linkTo(parent.start)
end.linkTo(parent.end)
}) {
val context = LocalContext.current
val customView = remember { com.shawnlin.numberpicker.NumberPicker(context) }
// Adds view to Compose
AndroidView({ customView }) { view ->
// View's been inflated - add logic here if necessary
with(view) {
orientation = HORIZONTAL
//dividerDrawable = ResourcesCompat.getDrawable(resources, R.drawable.bg_blue, null)
textColor =
ResourcesCompat.getColor(resources, R.color.slider_num_color, null)
selectedTextColor =
ResourcesCompat.getColor(resources, R.color.slider_num_color, null)
selectedTextSize = 120f
wheelItemCount = 6
value = 10
minValue = 0
maxValue = 99
layoutParams.width = MATCH_PARENT
setDividerColorResource(R.color.fade_green)
setDividerDistance(180)
setDividerThickness(10)
}
}
Text(
"%",
color = colorResource(id = R.color.bright_green),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._18ssp).toSp()
}, fontFamily = FontFamily(
Font(R.font.poppins_regular)
),
modifier = Modifier
.align(CenterHorizontally)
.offset(y = (-5).dp)
)
}
Text(
"Max Limit per autosaving",
color = colorResource(id = R.color.bright_green),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._13ssp).toSp()
},
fontFamily = FontFamily(
Font(R.font.poppins_regular)
),
modifier = Modifier.constrainAs(maxLimitTxt) {
top.linkTo(glMaxLimitTxt)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
)
TextField(
value = query.value,
onValueChange = { newValue -> query.value = newValue
if (newValue != "")
errorMsg.value = newValue.toInt() > 1500
},
label = {
Text(" Amount",
color = colorResource(id = R.color.bright_green),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._15ssp).toSp()
},
textAlign = TextAlign.Center
)
},
textStyle = TextStyle(
textAlign = TextAlign.Center,
color = colorResource(id = R.color.bright_green),
fontFamily = FontFamily(Font(R.font.poppins_regular)),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._15ssp).toSp()
},
),
modifier = Modifier.constrainAs(maxLimitTextField) {
top.linkTo(glMaxLimitTextField)
start.linkTo(glLeftTextField)
end.linkTo(glRightTextField)
width = Dimension.fillToConstraints
},
colors = TextFieldDefaults.textFieldColors(
backgroundColor = Color.Transparent,
unfocusedIndicatorColor = colorResource(id = R.color.bright_green),
focusedIndicatorColor = colorResource(id = R.color.bright_green)
)
)
Text(
text =
"*Please enter amount less than Rs.1500",
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._8ssp).toSp()
},
color = colorResource(id = R.color.voodlee_red),
modifier = Modifier
.padding(top = 8.dp)
.alpha(
if (errorMsg.value) {
1f
} else 0f
)
.constrainAs(error) {
top.linkTo(maxLimitTextField.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
},
)
Button(
onClick = {
navController.navigate("fourth_screen")
},
modifier = Modifier.constrainAs(buttonSave) {
top.linkTo(glButtonSaveTop)
bottom.linkTo(glButtonSaveBottom)
start.linkTo(glLeft)
end.linkTo(glRight)
width = Dimension.fillToConstraints
height = Dimension.fillToConstraints
},
colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(id = R.color.voodlee_red))
) {
Text(
"Save", color = colorResource(id = R.color.dark_blue),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._16ssp).toSp()
},
)
}
Text(
"What is autosaving?",
color = colorResource(id = R.color.text_highlight),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._11ssp).toSp()
},
fontFamily = FontFamily(
Font(R.font.poppins_regular)
),
modifier = Modifier
.constrainAs(whatIsAutosaving) {
top.linkTo(glWhatIsAutoSaving)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
.clickable {
coroutineScope.launch {
scrollState.animateScrollTo(scrollToPosition.roundToInt())
}
},
)
Text(
stringResource(id = R.string.autosave_info),
color = colorResource(id = R.color.bright_green),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._11ssp).toSp()
}, fontFamily = FontFamily(
Font(R.font.poppins_regular)
),
modifier = Modifier
.constrainAs(autoSaveInfo) {
top.linkTo(glAutoSaveInfo)
start.linkTo(glLeft)
end.linkTo(glRight)
width = Dimension.fillToConstraints
}
.onGloballyPositioned { coordinates ->
scrollToPosition = coordinates.positionInParent().y
},
)
Row(
modifier = Modifier
.padding(top = 40.dp, bottom = 50.dp)
.constrainAs(detailsRow) {
top.linkTo(autoSaveInfo.bottom)
start.linkTo(glLeft)
end.linkTo(glRight)
width = Dimension.fillToConstraints
},
) {
Text(
text = "For more details",
color = colorResource(id = R.color.bright_green),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._11ssp).toSp()
}, fontFamily = FontFamily(
Font(R.font.poppins_regular)
),
)
Spacer(modifier = Modifier.padding(5.dp))
Text(
text = "Click here",
color = colorResource(id = R.color.text_highlight),
fontSize = with(LocalDensity.current) {
dimensionResource(id = R.dimen._11ssp).toSp()
},
fontFamily = FontFamily(
Font(R.font.poppins_regular)
),
)
}
Image(
painter = painterResource(id = R.drawable.ic_btn_upward),
modifier = Modifier
.height(32.dp)
.constrainAs(iconUp) {
top.linkTo(detailsRow.bottom)
start.linkTo(glLeft)
end.linkTo(glRightIcon)
width = Dimension.fillToConstraints
},
contentDescription = ""
)
Spacer(modifier = Modifier
.padding(bottom = 50.dp)
.constrainAs(spacer) {
top.linkTo(glSpacer)
start.linkTo(parent.start)
end.linkTo(parent.end)
},)
}
Card(
Modifier
.align(BottomCenter)
.fillMaxWidth()
.alpha(if (query.value == "") 1f else 0f),
backgroundColor = MaterialTheme.colors.secondaryVariant
) {
ProgressBar5UI(
Modifier
.padding(start = 40.dp, end = 40.dp, top = 10.dp)
)
}
Card(
Modifier
.align(BottomCenter)
.fillMaxWidth()
.alpha(if (errorMsg.value) 1f else 0f),
backgroundColor = MaterialTheme.colors.secondaryVariant
) {
ProgressBar6UI(
Modifier
.padding(start = 40.dp, end = 40.dp, top = 10.dp)
)
}
Card(
Modifier
.align(BottomCenter)
.fillMaxWidth()
.alpha(if (query.value != "" && !errorMsg.value) 1f else 0f),
backgroundColor = MaterialTheme.colors.secondaryVariant
) {
ProgressBar7UI(
Modifier
.padding(start = 40.dp, end = 40.dp, top = 10.dp)
)
}
}
}
Is there possibly any special way to scroll on clicking an element while using Constraint Layout?
You can use the onGloballyPositioned modifier to retrieve the position of a composable and then use the method scrollState.animateScrollTo to scroll to that position.
Something like:
val coroutineScope = rememberCoroutineScope()
val scrollState = rememberScrollState()
var scrollToPosition by remember { mutableStateOf(0F) }
Column(Modifier.verticalScroll(scrollState)) {
Text(
"Click here to scroll",
modifier = Modifier.clickable {
coroutineScope.launch {
scrollState.animateScrollTo(scrollToPosition.roundToInt())
}
}
)
//...
Text(
"Target",
modifier = Modifier.onGloballyPositioned { coordinates ->
scrollToPosition = coordinates.positionInParent().y
}
)
}

Categories

Resources