I was making a login for my app in the new android jetpack's compose.
I want to make a OTP layout like in the given photo.
check full example here
const val PIN_VIEW_TYPE_UNDERLINE = 0
const val PIN_VIEW_TYPE_BORDER = 1
#Composable
fun PinView(
pinText: String,
onPinTextChange: (String) -> Unit,
digitColor: Color = MaterialTheme.colors.onBackground,
digitSize: TextUnit = 16.sp,
containerSize: Dp = digitSize.value.dp * 2,
digitCount: Int = 4,
type: Int = PIN_VIEW_TYPE_UNDERLINE,
) {
BasicTextField(value = pinText,
onValueChange = onPinTextChange,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
decorationBox = {
Row(horizontalArrangement = Arrangement.SpaceBetween) {
repeat(digitCount) { index ->
DigitView(index, pinText, digitColor, digitSize, containerSize, type = type)
Spacer(modifier = Modifier.width(5.dp))
}
}
})
}
#Composable
private fun DigitView(
index: Int,
pinText: String,
digitColor: Color,
digitSize: TextUnit,
containerSize: Dp,
type: Int = PIN_VIEW_TYPE_UNDERLINE,
) {
val modifier = if (type == PIN_VIEW_TYPE_BORDER) {
Modifier
.width(containerSize)
.border(
width = 1.dp,
color = digitColor,
shape = MaterialTheme.shapes.medium
)
.padding(bottom = 3.dp)
} else Modifier.width(containerSize)
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center) {
Text(
text = if (index >= pinText.length) "" else pinText[index].toString(),
color = digitColor,
modifier = modifier,
style = MaterialTheme.typography.body1,
fontSize = digitSize,
textAlign = TextAlign.Center)
if (type == PIN_VIEW_TYPE_UNDERLINE) {
Spacer(modifier = Modifier.height(2.dp))
Box(
modifier = Modifier
.background(digitColor)
.height(1.dp)
.width(containerSize)
)
}
}
}
You can use a very simple layout for each char in the otp.
Something like
#Composable
fun OtpChar(){
var text by remember { mutableStateOf("1") }
val maxChar = 1
Column(Modifier.background(DarkGray),
horizontalAlignment = Alignment.CenterHorizontally){
TextField(
value =text,
onValueChange = {if (it.length <= maxChar) text = it},
modifier = Modifier.width(50.dp),
singleLine = true,
textStyle = LocalTextStyle.current.copy(
fontSize = 20.sp,
textAlign= TextAlign.Center),
colors= TextFieldDefaults.textFieldColors(
textColor = White,
backgroundColor = Transparent,
unfocusedIndicatorColor = Transparent,
focusedIndicatorColor = Transparent)
)
Divider(Modifier
.width(28.dp)
.padding(bottom = 2.dp)
.offset(y=-10.dp),
color = White,
thickness = 1.dp)
}
}
You can add some features like:
manage the focus in Next direction with the TAB key
manage the focus in Previous direction with the BACK SPACE key
how to move to the next textfield when a digit is entered
Something like:
fun OtpChar(
modifier: Modifier = Modifier
){
val pattern = remember { Regex("^[^\\t]*\$") } //to not accept the tab key as value
var (text,setText) = remember { mutableStateOf("") }
val maxChar = 1
val focusManager = LocalFocusManager.current
LaunchedEffect(
key1 = text,
) {
if (text.isNotEmpty()) {
focusManager.moveFocus(
focusDirection = FocusDirection.Next,
)
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally
){
TextField(
value =text,
onValueChange = {
if (it.length <= maxChar &&
((it.isEmpty() || it.matches(pattern))))
setText(it)
},
modifier = modifier
.width(50.dp)
.onKeyEvent {
if (it.key == Key.Tab) {
focusManager.moveFocus(FocusDirection.Next)
true
}
if (text.isEmpty() && it.key == Key.Backspace) {
focusManager.moveFocus(FocusDirection.Previous)
}
false
},
textStyle = LocalTextStyle.current.copy(
fontSize = 20.sp,
textAlign= TextAlign.Center),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next
),
colors= TextFieldDefaults.textFieldColors(
backgroundColor = Transparent,
unfocusedIndicatorColor = Transparent,
focusedIndicatorColor = Transparent),
)
Divider(
Modifier
.width(28.dp)
.padding(bottom = 2.dp)
.offset(y = -10.dp),
color = Teal200,
thickness = 1.dp)
}
}
Then just use something like a Row to display 4 OtpChars
val (item1, item2, item3, item4) = FocusRequester.createRefs()
Row(horizontalArrangement = Arrangement.SpaceBetween){
OtpChar(
modifier = Modifier
.focusRequester(item1)
.focusProperties {
next = item2
previous = item1
}
)
OtpChar(
modifier = Modifier
.focusRequester(item2)
.focusProperties {
next = item3
previous = item1
}
)
OtpChar(
modifier = Modifier
.focusRequester(item3)
.focusProperties {
next = item4
previous = item2
}
)
OtpChar(
modifier = Modifier
.focusRequester(item4)
.focusProperties {
previous = item3
next = item4
}
)
//....
}
If you faced keyboard issues try the code below:
#Composable
fun OtpCell(
modifier: Modifier,
value: String,
isCursorVisible: Boolean = false
) {
val scope = rememberCoroutineScope()
val (cursorSymbol, setCursorSymbol) = remember { mutableStateOf("") }
LaunchedEffect(key1 = cursorSymbol, isCursorVisible) {
if (isCursorVisible) {
scope.launch {
delay(350)
setCursorSymbol(if (cursorSymbol.isEmpty()) "|" else "")
}
}
}
Box(
modifier = modifier
) {
Text(
text = if (isCursorVisible) cursorSymbol else value,
style = MaterialTheme.typography.body1,
modifier = Modifier.align(Alignment.Center)
)
}
}
#ExperimentalComposeUiApi
#Composable
fun PinInput(
modifier: Modifier = Modifier,
length: Int = 5,
value: String = "",
onValueChanged: (String) -> Unit
) {
val focusRequester = remember { FocusRequester() }
val keyboard = LocalSoftwareKeyboardController.current
TextField(
value = value,
onValueChange = {
if (it.length <= length) {
if (it.all { c -> c in '0'..'9' }) {
onValueChanged(it)
}
if (it.length >= length) {
keyboard?.hide()
}
}
},
// Hide the text field
modifier = Modifier
.size(0.dp)
.focusRequester(focusRequester),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number
)
)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
repeat(length) {
OtpCell(
modifier = modifier
.size(width = 45.dp, height = 60.dp)
.clip(MaterialTheme.shapes.large)
.background(MaterialTheme.colors.surface)
.clickable {
focusRequester.requestFocus()
keyboard?.show()
},
value = value.getOrNull(it)?.toString() ?: "",
isCursorVisible = value.length == it
)
if (it != length - 1) Spacer(modifier = Modifier.size(8.dp))
}
}
}
Result:
First make the common TextField for OTP Screen
#Composable
fun CommonOtpTextField(otp: MutableState<String>) {
val max = 1
OutlinedTextField(
value = otp.value,
singleLine = true,
onValueChange = { if (it.length <= max) otp.value = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
shape = RoundedCornerShape(20.dp),
modifier = Modifier
.width(60.dp)
.height(60.dp),
maxLines = 1,
textStyle = LocalTextStyle.current.copy(
textAlign = TextAlign.Center
)
)
}
And Now use above CommonTextField to make four otp Field
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 15.dp, start = 15.dp, end = 15.dp),
horizontalArrangement = Arrangement.SpaceEvenly
) {
CommonOtpTextField(otp = otpOne)
CommonOtpTextField(otp = otpTwo)
CommonOtpTextField(otp = otpThree)
CommonOtpTextField(otp = otpFour)
}
Simple solution that uses one TextField and different code length
#Composable
fun RegistrationCodeInput(codeLength: Int) {
val code = remember { mutableStateOf("") }
val focusRequester = FocusRequester()
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
BasicTextField(
value = code.value,
onValueChange = { if (it.length <= codeLength) code.value = it },
Modifier.focusRequester(focusRequester = focusRequester),
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
decorationBox = {
CodeInputDecoration(code.value, codeLength)
})
}
}
#Composable
private fun CodeInputDecoration(code: String, length: Int) {
Box(
modifier = Modifier
.padding(16.dp)
.border(
border = BorderStroke(2.dp, color = borderColor),
shape = Shapes.small
)
) {
Row(
) {
for (i in 0 until length) {
val text = if (i < code.length) code[i].toString() else ""
CodeEntry(text)
}
}
}
}
#Composable
private fun CodeEntry(text: String) {
Box(
modifier = Modifier
.width(42.dp)
.height(42.dp),
contentAlignment = Alignment.Center
) {
Text(text = text)
}
}
#Preview
#Composable
fun PreviewInput() {
RegistrationCodeInput(4)
}
Related
I have a simple slider that I need to disable the button at star and when user moves the slider it should be enabled, i can disable it at first but i cant to enable it back, for some reason it doesnt take the new variable value this is my code.
My thoughts its because the var is declared at first and the slider value is capsuled somehow?
but if that is the case how do i pass the value to the component?
#OptIn(ExperimentalComposeUiApi::class)
#Composable
fun PopupWindowDialogStudent(
onConfirm: (Int, String) -> Unit,
parentUiState: StudentHomeUiState,
) {
// val openDialog = remember { mutableStateOf(parentUiState.showInAppFeedback) }
val openDialog = remember { mutableStateOf(!parentUiState.showInAppFeedback) }
var sliderPosition by remember { mutableStateOf(5f) }
var enable by remember { mutableStateOf(false) }
val recommend = sliderPosition.toInt()
Column(
) {
Box {
if (openDialog.value) {
Dialog(
onDismissRequest = { openDialog.value = false },
properties = DialogProperties(),
)
{
Box(
Modifier
.fillMaxWidth()
.height(500.dp)
.background(Color.White, RoundedCornerShape(10.dp))
.border(1.dp, color = Color.White, RoundedCornerShape(20.dp))
) {
Column(
modifier = Modifier
.fillMaxSize()
.fillMaxHeight()
.padding(horizontal = 5.dp)
.verticalScroll(rememberScrollState())
,
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 10.dp)) {
var completeValue by remember { mutableStateOf("") }
completeValue = sliderPosition.toString()
Column(horizontalAlignment = Alignment.CenterHorizontally){
Slider(
value = sliderPosition,
onValueChange = {
sliderPosition = it
var enable = true
},
valueRange = 0f..10f,
steps = 9,
onValueChangeFinished = {
completeValue
},
modifier = Modifier.width(180.dp),
colors = SliderDefaults.colors(
thumbColor = secondaryColor,
activeTrackColor = Color.Blue
)
)
}
}
Spacer(modifier = Modifier.height(10.dp))
var comment by remember { mutableStateOf("") }
val keyboardController = LocalSoftwareKeyboardController.current
TextField(
value = comment,
placeholder = { Text(text = "¿Tienes algún otro comentario?") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {keyboardController?.hide()}),
onValueChange = {
comment = it
}
)
Spacer(modifier = Modifier.height(10.dp))
if (enable){
Button(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
onClick = {
onConfirm(recommend,comment)
openDialog.value = !openDialog.value
}
) {
Text(
text = "¡Contesta y gana +20 puntos!",
style = MaterialTheme.typography.subtitle2,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(3.dp))
}
}else{
Button(
modifier = Modifier
.fillMaxWidth()
.padding(10.dp),
onClick = {
}
) {
Text(
text = "¡Contesta y gana +20 puntos!",
style = MaterialTheme.typography.subtitle2,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(3.dp))
}
}
}
}
}
}
}
}
}
}
In the Slider you can use the onValueChangeFinished to change the enable value:
var enable by remember { mutableStateOf(false) }
Slider(
value = sliderPosition,
onValueChange = {
sliderPosition = it
enable = true
},
onValueChangeFinished = {
completeValue
enable = false
},
//...
)
Also you can avoid to use the if statement for the Button. Just use:
Button(
//...
enabled = enable,
onClick = {
openDialog.value = !openDialog.value
}
) {
//...
}
I am very new to jetpack compose please help,
I have use Surface() but my views are overlapping one another,
I want separate view as first one should be TopHeader() and another one should be BillForm
My code is:-
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp {
//TopHeader()
MainContent()
}
}
}
}
#Composable
fun MyApp(content: #Composable () -> Unit) {
JetTipAppTheme {
// A surface container using the 'background' color from the theme
Surface(color = MaterialTheme.colors.background) {
content()
}
}
}
TopHeader function to display the pink color view in the given image
#Composable
fun TopHeader(totalPerPerson: Double = 134.0) {
Surface(
modifier = Modifier
.fillMaxWidth()
.padding(15.dp)
.height(150.dp)
.clip(shape = CircleShape.copy(all = CornerSize(12.dp))),
//.clip(shape = RoundedCornerShape(corner = CornerSize(12.dp))),
color = Color(0xFFE9D7F7)
) {
Column(
modifier = Modifier.padding(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
val total = "%.2f".format(totalPerPerson)
Text(
text = "Total per person",
style = MaterialTheme.typography.h5
)
Text(
text = "$$total",
style = MaterialTheme.typography.h4,
fontWeight = FontWeight.ExtraBold
)
}
}
}
This is MainContent function:-
#Composable
fun MainContent() {
BillForm(){ billAmt ->
Log.d("AMT","MainContent: $billAmt")
}
}
BillForm composable function
#Composable
fun BillForm(modifier: Modifier = Modifier,
onValChange: (String) -> Unit = {}
){
val totalBillState = remember{
mutableStateOf("")
}
val validState = remember(totalBillState.value) {
totalBillState.value.trim().isNotEmpty()
}
val keyboardController = LocalSoftwareKeyboardController.current
val sliderPositionState = remember {
mutableStateOf(0f) //slider will take position from zero
}
TopHeader()
Surface(
modifier = Modifier
.padding(2.dp)
.fillMaxWidth(),
shape = RoundedCornerShape(corner = CornerSize(8.dp)),
border = BorderStroke(width = 1.dp, color = Color.LightGray)
) {
Column(
modifier = Modifier.padding(6.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.Start
) {
InputField(
valueState = totalBillState,
labelId = "Enter Bill",
enabled = true,
isSingleLined = true,
onAction = KeyboardActions{
if (!validState)return#KeyboardActions
onValChange(totalBillState.value.trim())
keyboardController?.hide()
}
)
//if(validState){
Row(
modifier = Modifier.padding(3.dp),
horizontalArrangement = Arrangement.Start
) {
Text(text = "Split",
modifier = Modifier.align(
alignment = Alignment.CenterVertically
))
//Spacer in between text and buttons
Spacer(modifier = Modifier.width(120.dp))
//Row for Buttons
Row(modifier = Modifier.padding(horizontal = 3.dp),
horizontalArrangement = Arrangement.End
) {
RoundIconButton( imageVector = Icons.Default.Remove,
onClick = { })
Text(text = "2",
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(start = 9.dp, end = 9.dp)
)
RoundIconButton( imageVector = Icons.Default.Add,
onClick = { })
}
}
//Tip Row
Row(modifier = Modifier
.padding(horizontal = 3.dp, vertical = 12.dp)
) {
Text(text = "Tip",
modifier = Modifier.align(alignment = Alignment.CenterVertically))
Spacer(modifier = Modifier.width(200.dp))
Text(text = "$33.00",
modifier = Modifier.align(alignment = Alignment.CenterVertically))
}
Column(verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "33%")
Spacer(modifier = Modifier.height(14.dp))
//Slider
Slider(value = sliderPositionState.value,
onValueChange = { newVal ->
sliderPositionState.value = newVal //<- this will change the position of the slider
Log.d("Slider","BillForm: $newVal")
},
modifier = Modifier.padding(start = 16.dp, end = 16.dp),
steps = 5,
onValueChangeFinished = {
}
)
}
// }else{
// Box() {
//
// }
// }
}
}
}
TopHeader composable view:-
BillForm view:-
What I want is like this given image:-
Use a Column(){} Composable for that purpose.
You can follow these basics that can help you to organize your composables and understand how its work.
Based on what was answered in this question (Open ModalSheetLayout on TextField focus instead of Keyboard) and the exchange of comments I did with #Abhimanyu, I was able to get the ModalBottomSheetLayout to appear when I click on one of the TextFields, however I encountered two more problems. I can't center what's in the content, nor can I center the content that's inside the sheet content. Can anyone help me understand why?
Here is a print of what is happening and my code:
#ExperimentalMaterialApi
#Preview
#Composable
fun ProfileScreen() {
var profileModalBottomSheetType by remember { mutableStateOf(ProfileModalBottomSheetType.NONE) }
val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
val coroutineScope = rememberCoroutineScope()
if (modalBottomSheetState.currentValue != ModalBottomSheetValue.Hidden) {
DisposableEffect(Unit) {
onDispose {
profileModalBottomSheetType = ProfileModalBottomSheetType.NONE
}
}
}
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 13.dp, topEnd = 13.dp),
sheetContent = {
Box(
modifier = Modifier
.padding(top = 10.dp)
.height(10.dp)
.width(100.dp)
.background(
color = Color.LightGray,
shape = RoundedCornerShape(4.dp)
)
)
when (profileModalBottomSheetType) {
ProfileModalBottomSheetType.SELECT_RATE -> {
SelectRateModalBottomSheet(listOf("Exact Rate", "Range"))
}
else -> {}
}
}
) {
LazyColumn(
modifier = Modifier
.width(320.dp)
) {
item {
HeightSpacer(40.dp)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Image(
painter = painterResource(id = R.drawable.ic_clearjobs_logo_2x),
contentDescription = null
)
}
HeightSpacer(47.dp)
Column(
modifier = Modifier
.width(320.dp)
.padding(start = 20.dp, end = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Profile Light Title",
style = TextStyle32Light,
textAlign = TextAlign.Center
)
Text(
text = "Profile Bold Title",
style = TextStyle32Bold,
textAlign = TextAlign.Center
)
}
HeightSpacer(47.dp)
Column(
modifier = Modifier
.background(
shape = RoundedCornerShape(
topStart = 13.dp,
topEnd = 13.dp
), color = Color.White
)
.padding(bottom = 140.dp)
.width(320.dp)
) {
Text(
text = stringResource(id = R.string.your_profile),
style = TextStyle28Bold,
modifier = Modifier.padding(
top = 40.dp,
start = 20.dp,
bottom = 30.dp
)
)
Text(
text = stringResource(id = R.string.salary_range),
style = TextStyle28Bold,
modifier = Modifier.padding(
top = 40.dp,
start = 20.dp,
bottom = 30.dp
)
)
Box {
LightBlueBorderTextField(
title = "Rate",
initialState = "Exact Rate",
textFieldTextStyle = TextStyle16BlackOpacity50Normal,
enabled = false
) { innerTextField ->
Row(verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.weight(1f)
.padding(start = Dimen10)
) {
innerTextField()
}
}
}
Box(
modifier = Modifier
.matchParentSize()
.alpha(0f)
.clickable(
onClick = {
profileModalBottomSheetType =
ProfileModalBottomSheetType.SELECT_RATE
toggleModalBottomSheetState(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
)
}
)
)
}
}
}
}
}
}
#Composable
fun SelectRateModalBottomSheet(options: List<String>) {
LazyColumn(
modifier = Modifier.padding(
start = 20.dp,
end = 20.dp,
top = 15.dp,
bottom = 15.dp
)
) {
items(options.size) { optionIndex ->
val option = options[optionIndex]
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 15.dp)
) {
Box(Modifier.weight(1f)) {
Text(text = option, style = TextStyle16BlackOpacity50Normal)
}
RadioButton(
selected = false,
onClick = { /*TODO*/ },
modifier = Modifier
.width(20.dp)
.height(20.dp)
)
}
if (optionIndex != options.lastIndex) {
Divider(color = Color.DarkGray, thickness = 1.dp)
HeightSpacer(dimen = 15.dp)
}
}
}
}
#Composable
fun HeightSpacer(dimen: Dp) {
Spacer(modifier = Modifier.height(dimen))
}
#Preview
#Composable
fun LightBlueBorderTextField(
title: String = "",
initialState: String = "",
textFieldTextStyle: TextStyle = TextStyle18Normal,
enabled: Boolean = true,
decorationBox: #Composable (innerTextField: #Composable () -> Unit) -> Unit = { innerTextField ->
Box(
Modifier.padding(start = Dimen10),
contentAlignment = Alignment.CenterStart
) {
innerTextField()
}
}
) {
Column {
val state = remember { mutableStateOf(TextFieldValue(initialState)) }
if (title.isNotEmpty()) {
Text(
text = title,
style = TextStyle16BlackBold,
modifier = Modifier.padding(
top = Dimen40,
start = Dimen30,
bottom = Dimen10
)
)
} else {
HeightSpacer(Dimen40)
}
CustomTextField(
state = state,
modifier = Modifier
.height(Dimen45)
.padding(start = Dimen20, end = Dimen20)
.border(
width = Dimen1,
color = LightBlue,
shape = RoundedCornerShape(Dimen13)
)
.background(Color.White, RoundedCornerShape(Dimen13))
.fillMaxWidth(),
textStyle = textFieldTextStyle,
decorationBox = decorationBox,
enabled = enabled
)
}
}
#Composable
fun CustomTextField(
state: MutableState<TextFieldValue>,
modifier: Modifier,
textStyle: TextStyle = TextStyle18Normal,
decorationBox: #Composable (innerTextField: #Composable () -> Unit) -> Unit,
enabled: Boolean = true
) {
BasicTextField(
modifier = modifier,
value = state.value,
onValueChange = { value -> state.value = value },
singleLine = true,
textStyle = textStyle,
decorationBox = decorationBox,
enabled = enabled
)
}
#ExperimentalMaterialApi
fun toggleModalBottomSheetState(
coroutineScope: CoroutineScope,
modalBottomSheetState: ModalBottomSheetState,
action: (() -> Unit)? = null,
) {
coroutineScope.launch {
if (!modalBottomSheetState.isAnimationRunning) {
if (modalBottomSheetState.isVisible) {
modalBottomSheetState.hide()
} else {
modalBottomSheetState.show()
}
}
action?.invoke()
}
}
internal enum class ProfileModalBottomSheetType {
NONE,
SELECT_JOB_KEYWORDS,
SELECT_WORK_LOCATIONS,
SELECT_TAGS,
SELECT_RATE,
SELECT_SALARY_PERIOD
}
I solved this issue by putting all the ModalBottomSheet content inside a Box with Modifier.fillMaxSize() and the contentAlignment = Alignment.Center. See my code below:
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetShape = RoundedCornerShape(topStart = Dimen13, topEnd = Dimen13),
sheetContent = {
JobDetailsModalBottomSheet(modalBottomSheetType, jobClicked) {
modalBottomSheetType = JobOpeningsScreenModalBottomSheetType.NONE
toggleModalBottomSheetState(
coroutineScope = coroutineScope,
modalBottomSheetState = modalBottomSheetState,
)
}
}) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
// any content
}
}
I'm not really sure why this doesn't work without Box, but after a few tries, this was the solution I found and it worked.
I have a LazyVerticalGrid composable that shows my items. I want to show an error message with a button in the center of LazyVerticalGrid when an error has occured. Here is my error item and my LazyVerticalGrid:
ErrorItem
#Composable
fun ErrorItem(
message: String,
modifier: Modifier = Modifier,
onRetryClick: () -> Unit,
) {
Column(
modifier = modifier
.padding(dimensionResource(id = R.dimen.dimen_8)),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = message,
maxLines = 1,
style = MaterialTheme.typography.subtitle2,
color = MaterialTheme.colors.error,
modifier = Modifier.padding(vertical = dimensionResource(id = R.dimen.dimen_16))
)
Button(
onClick = onRetryClick,
shape = CircleShape,
colors = ButtonDefaults.buttonColors(
backgroundColor = MaterialTheme.colors.error,
contentColor = MaterialTheme.colors.onError
)
) {
Text(text = stringResource(id = R.string.try_again))
}
}
}
#Composable
fun CompaniesScreen(
modifier: Modifier = Modifier
) {
val viewModel: CompaniesScreenViewModel = hiltViewModel()
val companies: LazyPagingItems<Company> = viewModel.companies.collectAsLazyPagingItems()
val isLoading = companies.loadState.refresh is LoadState.Loading
val isError = companies.loadState.refresh is LoadState.Error
val swipeRefreshState = rememberSwipeRefreshState(isRefreshing = isLoading)
val listState = rememberLazyGridState()
Scaffold(modifier = modifier, topBar = { LogoToolbar() }) { padding ->
SwipeRefresh(indicator = { state, trigger ->
SwipeRefreshIndicator(
state = state,
refreshTriggerDistance = trigger,
scale = true,
backgroundColor = MaterialTheme.colors.primary,
contentColor = MaterialTheme.colors.onPrimary
)
}, state = swipeRefreshState, onRefresh = companies::refresh) {
LazyVerticalGrid(
state = listState,
modifier = Modifier
.fillMaxSize()
.padding(
horizontal = dimensionResource(id = R.dimen.dimen_20),
vertical = dimensionResource(
id = R.dimen.dimen_24
)
),
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.dimen_20)),
verticalArrangement = Arrangement.spacedBy(
dimensionResource(
id = R.dimen.dimen_24
)
)
) {
if (!isLoading && !isError) {
items(companies.itemCount) { index ->
CompanyItem(companyName = companies[index]?.name!!)
}
}
companies.apply {
when {
loadState.refresh is LoadState.Loading -> {
items(16) {
CompanyItem(companyName = "", isLoading = true)
}
}
loadState.refresh is LoadState.Error -> {
val e = companies.loadState.refresh as LoadState.Error
item(span = {GridItemSpan(2)}) {
ErrorItem(
modifier = Modifier.fillMaxSize(), // fillParentMaxSize() is not working
message = e.error.localizedMessage
?: stringResource(id = R.string.something_went_wrong),
onRetryClick = companies::retry
)
}
}
}
}
}
}
}
}
I have tried to use fillParentMaxSizewith my ErrorItem but it is not working. Here is the result :
If you place your LazyVerticalGrid and ErrorItem inside a Box with contentAlignment = Alignment.Center ErrorItem will show center of your Box when isError is true
Box(
modifier= Modifier.fillMaxSize(),
contentAlignment = Alignment.Center){
LazyVerticalGrid(
// Rest of the properties
)
if (isError){
ErrorItem(""){
}
}
}
I'm trying to put a drop down for US states that uses a TextField and a TextField in a Row and the second TextField (zip) does not show up. What am I doing wrong?
Here is how I'm declaring the row:
Row (Modifier.fillMaxWidth()) {
StateSelection(
onFormChanged = onFormChanged,
selectedLocation = selectedLocation,
label = "State"
)
Spacer(modifier = Modifier.width(8.dp))
ShippingField(
modifier = modifier,
onFormChanged = onFormChanged,
formType = FormType.SHIPPING_ZIP,
label = "Zip",
valueField = selectedLocation.zipCode
)
}
StatesDropDown:
#Composable
fun StateSelection(
onFormChanged: (FormType, String) -> Unit,
selectedLocation: Address,
label: String
) {
// State variables
val statesMap = AddressUtils.mapOfAmericanStatesToValue
var stateName: String by remember { mutableStateOf(selectedLocation.shippingState) }
var expanded by remember { mutableStateOf(false)}
val focusManager = LocalFocusManager.current
Box(contentAlignment = Alignment.CenterStart) {
Row(
Modifier
.padding(vertical = 8.dp)
.clickable {
expanded = !expanded
}
.padding(8.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) { // Anchor view
TextField(
modifier = Modifier
.fillMaxWidth(),
value = stateName,
onValueChange = {
onFormChanged(FormType.SHIPPING_COUNTRY, it)
},
label = { Text(text = label) },
textStyle = MaterialTheme.typography.subtitle1,
singleLine = true,
trailingIcon = {
IconButton(onClick = { expanded = true }) {
Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription = "")
}
},
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done,
keyboardType = KeyboardType.Text
),
colors = TextFieldDefaults.textFieldColors(
cursorColor = MaterialTheme.colors.secondary,
textColor = MaterialTheme.colors.onPrimary,
focusedIndicatorColor = MaterialTheme.colors.secondary,
backgroundColor = MaterialTheme.colors.secondaryVariant
)
) // state name label
DropdownMenu(expanded = expanded, onDismissRequest = {
expanded = false
}) {
statesMap.asIterable().iterator().forEach {
val (key, value) = it
DropdownMenuItem(onClick = {
expanded = false
stateName = key
onFormChanged(FormType.SHIPPING_STATE, key)
},
modifier = Modifier.fillMaxWidth()) {
Text(text = key)
}
}
}
}
}
}
ZipCode:
#Composable
fun ShippingField(
modifier: Modifier,
onFormChanged: (FormType, String) -> Unit,
formType: FormType,
label: String,
valueField: String
) {
val focusManager = LocalFocusManager.current
var state by rememberSaveable {
mutableStateOf(valueField)
}
TextField(modifier = modifier
.background(MaterialTheme.colors.primaryVariant)
.fillMaxWidth(),
value = state,
onValueChange = {
state = it
onFormChanged(formType, it)
},
label = { Text(text = label) },
textStyle = MaterialTheme.typography.subtitle1,
singleLine = true,
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }),
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Next,
keyboardType = KeyboardType.Text
),
colors = TextFieldDefaults.textFieldColors(
cursorColor = MaterialTheme.colors.secondary,
textColor = MaterialTheme.colors.onPrimary,
focusedIndicatorColor = MaterialTheme.colors.secondary,
backgroundColor = MaterialTheme.colors.secondaryVariant
)
)
}
Row (Modifier.fillMaxWidth()) {
StateSelection(
modifier = Modifier.weight(1f)
...
)
Spacer(modifier = Modifier.width(8.dp))
ShippingField(
modifier = Modifier.weight(1f),
...
)
}
You should do it like above, and then pass the modifier to your composables below. Weight distributes and fills max width evenly on your composables
You have to remove the modifier = Modifier.fillMaxWidth() in the 1st TextField in the StatesDropDown composable.