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
Related
I have problem when the software keyboard opens I do see the screen as follows:
I have something like:
#OptIn(
ExperimentalLayoutApi::class, ExperimentalComposeUiApi::class,
ExperimentalFoundationApi::class
)
#Composable
fun ScreenScanPrint(vmUser: ViewModelUser, vm: ViewModelScanPrint) {
val density = LocalDensity.current
Column(
modifier = Modifier
.background(lightGrayBackGround)
.fillMaxSize()
.navigationBarsPadding()
.imePadding()
) {
Box(
modifier = Modifier
.height(114.dp)
.fillMaxWidth()
) {
TopNavBarWithProfilePictureAndBackButton(
text = "Αναζήτηση Προϊόντος",
vmUser = vmUser,
isLight = true
)
}
Column(modifier = Modifier.padding(24.dp)) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
colors = CardDefaults.cardColors(containerColor = Color(0xFFF4F4FC)),
shape = RoundedCornerShape(10.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier.padding(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.ic_baseline_lens_blur_24),
modifier = Modifier.size(20.dp),
contentDescription = ""
)
Text(
modifier = Modifier
.padding(start = 4.dp),
text = "Περιγραφή",
fontFamily = FontFamily(Font(R.font.manrope_semi_bold)),
fontSize = 15.sp,
color = appGray
)
}
Text(
modifier = Modifier
.padding(start = 18.dp, end = 18.dp, top = 0.dp),
text = vm.state.productName,
fontFamily = FontFamily(Font(R.font.manrope_regular)),
fontSize = 15.sp,
color = appGray
)
}
Column(
modifier = Modifier.padding(horizontal = 18.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(2f))
Row(verticalAlignment = Alignment.Bottom) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(bottom = 12.dp)
) {
Image(
painter = painterResource(id = R.drawable.ic_round_euro_24_gray),
contentDescription = "",
modifier = Modifier.size(16.dp),
colorFilter = ColorFilter.tint(appGray)
)
Text(
modifier = Modifier
.padding(start = 8.dp),
text = "Τιμή",
fontFamily = FontFamily(Font(R.font.manrope_semi_bold)),
fontSize = 14.sp,
color = appGray
)
}
Spacer(modifier = Modifier.weight(1f))
Text(
modifier = Modifier,
text = vm.state.price.ifEmpty { "-" },
fontFamily = FontFamily(Font(R.font.manrope_semi_bold)),
fontSize = 38.sp,
color = appGray
)
}
Spacer(modifier = Modifier.weight(1f))
Box(
Modifier
.height(2.dp)
.fillMaxWidth()
.background(
appGray.copy(0.15f),
shape = DottedShape(step = 15.dp)
)
)
Spacer(modifier = Modifier.weight(1f))
ItemWithIconAndText(
text = "Διαθέσημα Κεντρικό",
imageResource = R.drawable.ic_round_content_paste_24,
numberIs = vm.state.storeRoomCenter.ifEmpty { "-" }
)
Spacer(modifier = Modifier.weight(1f))
ItemWithIconAndText(
text = "Διαθέσημα Αποθήκη",
imageResource = R.drawable.ic_round_content_paste_24,
numberIs = vm.state.storeRoomWarehouse.ifEmpty { "-" }
)
Spacer(modifier = Modifier.weight(1f))
ItemWithIconAndText(
text = "Δεσμευμένα",
imageResource = R.drawable.ic_baseline_content_paste_go_24,
numberIs = vm.state.reservedProducts.ifEmpty { "-" }
)
Spacer(modifier = Modifier.weight(2f))
TextFieldWithIconAndButton(
text = vm.state.barcodeText,
imageResource = R.drawable.svg_barcode_white_small,
descriptionText = "Barcode",
onTextChange = { vm.onEvent(ScanPrintEvent.OnBarcodeChange(it)) }
) {
}
Spacer(modifier = Modifier.weight(1f))
TextFieldWithIconAndButton(
text = vm.state.printText,
imageResource = R.drawable.svg_printer_white_small,
descriptionText = "Εκτυπωτής ισόγειο",
onTextChange = { vm.onEvent(ScanPrintEvent.OnPrintChange(it)) }
) { }
Spacer(modifier = Modifier.weight(2f))
androidx.compose.material.Button(
shape = CircleShape,
enabled = true,
onClick = {
vm.onEvent(ScanPrintEvent.OnScanClick)
},
colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent),
contentPadding = PaddingValues(),
elevation = ButtonDefaults.elevation(
defaultElevation = 12.dp,
pressedElevation = 4.dp
),
modifier = Modifier.size(84.dp)
) {
Box(
Modifier
.fillMaxSize()
.background(blueButtonGradient),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.svg_scanner_big_fab),
contentDescription = ""
)
}
}
Spacer(modifier = Modifier.weight(1f))
}
}
}
}
And in manifest I have set up as: android:windowSoftInputMode="adjustResize"
in my activity.
I would like to see the screen moving up with keyboard bellow, without altering the weights. For example when I click on barcode:
Problem is imePadding() remove it and see result
and try use adjustPan in manifest
I am new in jetpack compose, and I am try to learn it, so I have simple onboarding screen in jetpack compose, when I update project to jetpack compose 1.1.1 , "Next" button not working for onboarding screen to scroll horizontally. it was working when I use jetpack compose 1.0.0 version, I do not know what change in new version, any idea?
#ExperimentalPagerApi
#Composable
fun OnBoardScreen() {
val scaffoldState = rememberScaffoldState()
val onBoardViewModel : OnBoardViewModel = viewModel()
val context = LocalContext.current
val currentPage = onBoardViewModel.currentPage.collectAsState()
Toast.makeText(context, "${currentPage.value}", Toast.LENGTH_SHORT).show()
val pagerState = rememberPagerState(
pageCount = onBoardItem.size,
initialOffscreenLimit = 2,
initialPage = 0,
infiniteLoop = false
)
Scaffold(
modifier = Modifier.fillMaxSize(),
scaffoldState = scaffoldState
) {
Surface(
modifier = Modifier.fillMaxSize()
) {
LaunchedEffect(scaffoldState.snackbarHostState){
pagerState.animateScrollToPage(
page = currentPage.value
)
}
Box(
modifier = Modifier
.fillMaxWidth()
.background(Gray200)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
HorizontalPager(
state = pagerState
) { page ->
Column(
modifier = Modifier
.padding(top = 65.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(id = onBoardItem[page].image),
contentDescription = "OnBoardImage",
modifier = Modifier
.size(250.dp)
)
Text(
text = onBoardItem[page].title,
modifier = Modifier
.padding(top = 50.dp),
color = Color.White,
fontWeight = FontWeight.Bold,
fontSize = 20.sp
)
Text(
text = onBoardItem[page].desc,
modifier = Modifier
.padding(30.dp),
color = Color.White,
fontSize = 18.sp,
textAlign = TextAlign.Center
)
}
}
PagerIndicator(onBoardItem.size, pagerState.currentPage)
}
Box(
modifier = Modifier
.align(Alignment.BottomCenter)
) {
Row(
modifier = Modifier
.padding(bottom = 20.dp)
.fillMaxWidth(),
horizontalArrangement = if (pagerState.currentPage != 2 ) {
Arrangement.SpaceBetween
} else {
Arrangement.Center
}
) {
if (pagerState.currentPage == 2) {
OutlinedButton(
onClick = {
Toast.makeText(context, "Start the Screen", Toast.LENGTH_SHORT).show()
},
shape = RoundedCornerShape(45.dp)
) {
Text(
text = "Get Started",
modifier = Modifier.padding(
vertical = 8.dp,
horizontal = 40.dp
),
color = Color.Black
)
}
} else {
Text(
text = "Skip",
color = Color.White,
modifier = Modifier.padding(start = 20.dp),
fontSize = 18.sp,
fontWeight = FontWeight.Medium
)
Text(
text = "Next",
color = Color.White,
modifier = Modifier
.clickable {
onBoardViewModel.setCurrentPage(pagerState.currentPage + 1)
}
.padding(end = 20.dp),
fontSize = 18.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
}
}
}
It looks like you expect these lines to scroll the pager every time you change the value of currentPage:
LaunchedEffect(scaffoldState.snackbarHostState) {
pagerState.animateScrollToPage(
page = currentPage.value
)
}
But scaffoldState.snackbarHostState in this scope is a static value, which means LaunchedEffect is not gonna be relaunched.
One option is to pass currentPage instead, but also, as your currentPage is backed by flow, it's cleaner to collect it:
LaunchedEffect(Unit) {
onBoardViewModel.currentPage
.collect {
pagerState.animateScrollToPage(
page = currentPage.value
)
}
}
p.s.
Also, when you need to make a text button, instead of adding clickable to the Text you can use TextButton:
TextButton(
onClick = {
}
) {
Text(/*...*/)
}
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})
I am creating a profile page for the app and need to perform different actions based upon items clicked inside LazyColumn.
What I want is when I click on Rate us, play store to open, click on feedback browser should open, clicking on settings user should navigate to the settings screen. How to perform such different actions in LazyColumn click listener.
Here is code,
ProfileListModel.kt
data class ProfileListModel(
val imageVector: ImageVector,
#StringRes val contentDescription: Int,
#StringRes val text: Int
)
ProfileListRepository.kt
class ProfileListRepository {
fun getAllData(): List<ProfileListModel> {
return listOf(
ProfileListModel(
imageVector = Icons.Default.InvertColors,
contentDescription = R.string.app_theme,
text = R.string.app_theme
),
ProfileListModel(
imageVector = Icons.Default.NoAccounts,
contentDescription = R.string.turn_on_incognito,
text = R.string.turn_on_incognito
),
ProfileListModel(
imageVector = Icons.Default.Language,
contentDescription = R.string.app_language,
text = R.string.app_language
),
ProfileListModel(
imageVector = Icons.Default.Settings,
contentDescription = R.string.settings,
text = R.string.settings
),
ProfileListModel(
imageVector = Icons.Default.ModeOfTravel,
contentDescription = R.string.drive_mode,
text = R.string.drive_mode
),
ProfileListModel(
imageVector = Icons.Default.ChildCare,
contentDescription = R.string.child_mode,
text = R.string.child_mode
),
ProfileListModel(
imageVector = Icons.Default.BarChart,
contentDescription = R.string.time_watched,
text = R.string.time_watched
),
ProfileListModel(
imageVector = Icons.Default.Storage,
contentDescription = R.string.account_data,
text = R.string.account_data
),
ProfileListModel(
imageVector = Icons.Default.StarRate,
contentDescription = R.string.rate_us_on_play_store,
text = R.string.rate_us_on_play_store
),
ProfileListModel(
imageVector = Icons.Default.Feedback,
contentDescription = R.string.feedback,
text = R.string.feedback
),
ProfileListModel(
imageVector = Icons.Default.Forum,
contentDescription = R.string.forum,
text = R.string.forum
),
ProfileListModel(
imageVector = Icons.Default.HelpCenter,
contentDescription = R.string.help_center,
text = R.string.help_center
),
ProfileListModel(
imageVector = Icons.Default.PrivacyTip,
contentDescription = R.string.privacy_policy,
text = R.string.privacy_policy
),
ProfileListModel(
imageVector = Icons.Default.ControlPointDuplicate,
contentDescription = R.string.terms_of_service,
text = R.string.terms_of_service
)
)
}
}
ProfileListItem.kt
#Composable
fun ProfileListItem(profileListModel: ProfileListModel) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.clickable(
onClick = {
/*todo("perform different actions based upon item clicked")*/
}
)
/*.clickable { selectedItem(profileListModel) }*/
.padding(all = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = profileListModel.imageVector,
contentDescription = stringResource(id = profileListModel.contentDescription),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(end = 16.dp)
)
Text(
modifier = Modifier.wrapContentSize(),
text = stringResource(id = profileListModel.text)
)
}
}
ProfileScreen.kt
#ExperimentalMaterial3Api
#Composable
fun ProfileScreen() {
Box(modifier = Modifier.fillMaxSize()) {
Surface(color = MaterialTheme.colorScheme.background) {
ProfileList()
}
}
}
#ExperimentalMaterial3Api
#Composable
fun ProfileList() {
val profileListRepository = ProfileListRepository()
val getAllData = profileListRepository.getAllData()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
item {
ProfileCard()
}
item {
NightLight()
}
itemsIndexed(items = getAllData) { index, profileListModel ->
ProfileListItem(profileListModel = profileListModel)
}
}
}
#Composable
fun NightLight() {
val checkedState = remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.clickable(onClick = { checkedState.value = !checkedState.value })
.padding(horizontal = 16.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Row(modifier = Modifier.wrapContentSize(), verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Default.ModeNight,
contentDescription = stringResource(id = R.string.night_light),
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.padding(end = 16.dp)
)
Text(
modifier = Modifier.wrapContentSize(),
text = stringResource(id = R.string.night_light)
)
}
Switch(checked = checkedState.value, onCheckedChange = { checkedState.value = it })
}
}
#ExperimentalMaterial3Api
#Composable
fun ProfileCard(
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(all = 16.dp),
shape = RoundedCornerShape(size = 16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
Column(
modifier = Modifier.padding(all = 16.dp)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Click to login")
Image(
painter = painterResource(id = R.drawable.avatar),
contentDescription = "avatar",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(size = 72.dp)
.clip(shape = CircleShape)
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(text = "Level 0")
Card(
modifier = Modifier.wrapContentSize(),
shape = RoundedCornerShape(size = 16.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 3.dp)
) {
Text(
text = "Check in",
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
)
}
}
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(top = 16.dp)
) {
Column(
modifier = Modifier
.wrapContentHeight()
.weight(1f)
) {
Text(text = "0")
Text(text = "Social reputation")
}
Column(
modifier = Modifier
.wrapContentHeight()
.weight(1f)
) {
Text(text = "0", modifier = Modifier.fillMaxWidth())
Text(text = "Credit score", modifier = Modifier.fillMaxWidth())
}
}
}
}
}
#ExperimentalMaterial3Api
#Preview(
showBackground = true, name = "Light mode",
uiMode = Configuration.UI_MODE_NIGHT_NO or Configuration.UI_MODE_TYPE_NORMAL
)
/*#Preview(
showBackground = true, name = "Night mode",
uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL
)*/
#Composable
fun ProfileScreenPreview() {
ProfileScreen()
}
You can pass in an id to identify the item being clicked and then when the item is clicked, pass that id back to your viewmodel. This is what uni-directional flow is about in Compose. Example:
#ExperimentalMaterialApi
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(intent)
setContent {
ProfileList(
onItemClick = {itemId ->
viewmodel.handleItem(itemId)
}
)
}
}
}
#Composable
fun ProfileList(
onItemClick: (itemId: MenuItemIDs) -> Unit
) {
val profileListRepository = ProfileListRepository()
val getAllData = profileListRepository.getAllData()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
) {
item {
ProfileCard(
itemId = MenuItemIDs.ProfilCard,
onItemClick = onItemClick
)
}
item {
NightLight()
}
itemsIndexed(items = getAllData) { index, profileListModel ->
ProfileListItem(profileListModel = profileListModel)
}
}
}
#Composable
fun ProfileCard(
modifier: Modifier = Modifier,
itemId: MenuItemIDs,
onItemClick: (itemId: MenuItemIDs) -> Unit
) {
Card(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(all = 16.dp)
.clickable {
onItemClick(itemId)
}
) {
}
}
enum class MenuItemIDs {
ProfilCard,
NightLight,
AppTheme,
blah, blah, blah...
}
How do I create a rounded checkbox in Jetpackcompose like this. I tried using a Shape composable on it but it doesn't work.
I was looking on how to do the same thing you were asking, your question helped me on my journey so it is only fair I share. Add some animations and you are set my friend.
Make a round looking icon by using a box and an icon
Box(
modifier = Modifier
.clip(CircleShape)
.size(40.dp)
.background(Color.Black)
.padding(3.dp)
.clip(CircleShape)
.background(Color.White),
contentAlignment = Alignment.Center
) {
Icon(imageVector = Icons.Default.Check, contentDescription = "")
}
2.Place the newly made rounded icon and some text next to each other by using a Row
Row(
verticalAlignment = Alignment.CenterVertically,
){
Box(
modifier = Modifier
.clip(CircleShape)
.size(40.dp)
.background(Color.Black)
.padding(3.dp)
.clip(CircleShape)
.background(Color.White),
contentAlignment = Alignment.Center
) {
Icon(imageVector = Icons.Default.Check, contentDescription = "")
}
Text(
text = checkedText.value,
color = color.value,
fontSize = 20.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 5.dp)
)
}
3.Replace whatever you want with variables so you can customize
it
#Composable
fun RoundedCheckView(
) {
val isChecked = remember { mutableStateOf(false) }
val checkedText = remember { mutableStateOf("unChecked") }
val circleSize = remember { mutableStateOf(20.dp) }
val circleThickness = remember { mutableStateOf(2.dp) }
val color = remember { mutableStateOf(Color.Gray) }
Row(
verticalAlignment = Alignment.CenterVertically,
{
Box(
modifier = Modifier
.clip(CircleShape)
.size(circleSize.value)
.background(color.value)
.padding(circleThickness.value)
.clip(CircleShape)
.background(Color.White) ,
contentAlignment = Alignment.Center
) {
Icon(imageVector = Icons.Default.Check, contentDescription = "")
}
Text(
text = checkedText.value,
color = color.value,
fontSize = 20.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 5.dp)
)
}
}
4.Finally add Modifier.toggleable to the row, basically making it a clickable item that toggles (between true and false) a variable in this case isChecked. Then just customize the variables according to what you need
#Composable
fun RoundedCheckView()
{
val isChecked = remember { mutableStateOf(false) }
val checkedText = remember { mutableStateOf("unChecked") }
val circleSize = remember { mutableStateOf(20.dp) }
val circleThickness = remember { mutableStateOf(2.dp) }
val color = remember { mutableStateOf(Color.Gray) }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.toggleable(value = isChecked.value,role = Role.Checkbox) {
isChecked.value = it
if (isChecked.value) {
checkedText.value = "Checked"
circleSize.value = 40.dp
circleThickness.value = 3.dp
color.value = Color.Black
} else {
checkedText.value = "unChecked"
circleSize.value = 20.dp
circleThickness.value = 2.dp
color.value = Color.Gray
}
}) {
Box(
modifier = Modifier
.clip(CircleShape)
.size(circleSize.value)
.background(color.value)
.padding(circleThickness.value)
.clip(CircleShape)
.background(Color.White) ,
contentAlignment = Alignment.Center
) {
if(isChecked.value){
Icon(imageVector = Icons.Default.Check, contentDescription = "")
}
}
Text(
text = checkedText.value,
color = color.value,
fontSize = 20.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(start = 5.dp)
)
}
}
This is how we can make a custom check box in jetpack compose
val isCheck = remember { mutableStateOf(false) }
Row {
Card(
modifier = Modifier.background(Color.White),
elevation = 0.dp,
shape = RoundedCornerShape(6.dp),
border = BorderStroke(1.5.dp, color = titleColor)
) {
Box(
modifier = Modifier
.size(25.dp)
.background(if (isCheck.value) titleColor else Color.White)
.clickable {
isCheck.value = !isCheck.value
},
contentAlignment = Center
) {
if(isCheck.value)
Icon(Icons.Default.Check, contentDescription = "", tint = Color.White)
}
}
Text(
modifier = Modifier
.align(CenterVertically)
.padding(start = 10.dp),
text = "I agree with the terms & condition",
)
}
You can try to make it use Box with modifier content alignment center. and put an icon on there.
#Preview
#Composable
fun Check() {
Box(
modifier = Modifier
.clip(CircleShape)
.size(50.dp)
.background(Color.Red)
.padding(5.dp)
.clip(CircleShape)
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Icon(imageVector = Icons.Default.Check, contentDescription = "")
}
}
We can use animations to make it behave similar to the default Checkbox. Using Modifier.toggleable on the top-level Row, the entire thing is clickable, including the label. This also creates the proper semantics for screen reader users. You can change the shape of the card to get a circular checkbox.
#Composable
fun PrimaryCheckbox(
label: String,
modifier: Modifier = Modifier,
size: Float = 24f,
checkedColor: Color = DarkGray,
uncheckedColor: Color = White,
checkmarkColor: Color = White,
onValueChange: () -> Unit
) {
var isChecked by remember { mutableStateOf(false) }
val checkboxColor: Color by animateColorAsState(if (isChecked) checkedColor else uncheckedColor)
val density = LocalDensity.current
val duration = 200
Row(
modifier = modifier
.toggleable(
value = isChecked,
role = Role.Checkbox,
onValueChange = {
isChecked = !isChecked
onValueChange.invoke()
}
)
) {
Card(
elevation = 0.dp,
shape = RoundedCornerShape(4.dp),
border = BorderStroke(1.5.dp, color = checkedColor),
) {
Box(
modifier = Modifier
.size(size.dp)
.background(checkboxColor),
contentAlignment = Alignment.Center
) {
androidx.compose.animation.AnimatedVisibility(
visible = isChecked,
enter = slideInHorizontally(
animationSpec = tween(duration)
) {
with(density) { (size * -0.5).dp.roundToPx() }
} + expandHorizontally(
expandFrom = Alignment.Start,
animationSpec = tween(duration)
),
exit = fadeOut()
) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = checkmarkColor
)
}
}
}
Text(
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 8.dp),
text = label,
)
}
}