I'm having trouble understanding how imePadding() and BringIntoViewRequester() work.
I have made a signup view and I believe I have applied imePadding correctly to the text field modifiers.
But for the view requesters I read in this medium article that you need to have multiple view requesters for multiple textfields. When I click on a textfield that's under the keyboard it does not come into view.
Am I meant to apply the view requesters to the AppSurface Composable or the overarching Column in SignupView?
Is there a better way to structure the view so that the textfields snaps just above the keyboard and comes into view?
I've also looked at focus requester which I don't think is the right use case for this.
@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
@Composable
fun SignupView(
navController: NavHostController,
viewModel: SignupViewModel = viewModel()
) {
val focusManager = LocalFocusManager.current
val context = LocalContext.current
// Variables to show the user error messages
var showFieldErrorDialog by remember { mutableStateOf(false) }
var showEULAToast by remember { mutableStateOf(false) }
var eulaItem by remember { mutableStateOf("") }
// Variables for EULA bottom sheet
val skipPartiallyExpanded by rememberSaveable { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = skipPartiallyExpanded)
val scope = rememberCoroutineScope()
// Variables used for IME handling
// TODO text fields not coming into view when clicked
val scrollState = rememberScrollState()
val fNameViewRequester = remember { BringIntoViewRequester() }
val lNameViewRequester = remember { BringIntoViewRequester() }
val emailViewRequester = remember { BringIntoViewRequester() }
val passViewRequester = remember { BringIntoViewRequester() }
val passCheckRequester = remember { BringIntoViewRequester() }
// Handles bottom sheet state
LaunchedEffect(eulaItem) {
if (eulaItem.isNotEmpty()) {
sheetState.show()
} else {
sheetState.hide()
}
}
AppSurface(
modifier = Modifier
.fillMaxSize()
.verticalScroll(scrollState),
content = {
// Top Component
Box(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
PrimaryText(text = stringResource(R.string.hello_there))
PageTitleText(text = stringResource(R.string.create_an_account))
}
LightDarkSwitcher(
modifier = Modifier
.size(50.dp)
.align(Alignment.TopEnd)
)
}
Spacer(modifier = Modifier.weight(1f))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Row {
CustomOutlinedTextField(
// First Name Field
modifier = Modifier
.weight(1f)
.imePadding()
.bringIntoViewRequester(fNameViewRequester)
.onFocusEvent { focusState ->
if (focusState.isFocused) {
scope.launch {
fNameViewRequester.bringIntoView()
}
}
},
label = stringResource(R.string.first_name),
icon = Icons.Outlined.Person,
onValueChange = {
viewModel.onEvent(SignupUIEvent.FirstNameChanged(it.trim()))
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Next
),
passStatus = viewModel.uiState.firstNamePass
)
Spacer(modifier = Modifier.width(10.dp))
CustomOutlinedTextField(
// Last Name Field
modifier = Modifier
.weight(1f)
.imePadding()
.bringIntoViewRequester(lNameViewRequester)
.onFocusEvent { focusState ->
if (focusState.isFocused) {
scope.launch {
lNameViewRequester.bringIntoView()
}
}
},
label = stringResource(R.string.last_name),
icon = Icons.Outlined.Person,
onValueChange = {
viewModel.onEvent(SignupUIEvent.LastNameChanged(it.trim()))
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Words,
imeAction = ImeAction.Next
),
passStatus = viewModel.uiState.lastNamePass
)
}
Spacer(modifier = Modifier.height(10.dp))
CustomOutlinedTextField(
// Email Field
modifier = Modifier
.imePadding()
.bringIntoViewRequester(emailViewRequester)
.onFocusEvent { focusState ->
if (focusState.isFocused) {
scope.launch {
emailViewRequester.bringIntoView()
}
}
},
label = stringResource(R.string.email),
icon = Icons.Outlined.Email,
onValueChange = {
viewModel.onEvent(SignupUIEvent.EmailChanged(it.trim()))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
),
passStatus = viewModel.uiState.emailPass
)
Spacer(modifier = Modifier.height(10.dp))
CustomOutlinedTextField(
// Password Field
modifier = Modifier
.imePadding()
.bringIntoViewRequester(passViewRequester)
.onFocusEvent { focusState ->
if (focusState.isFocused) {
scope.launch {
passViewRequester.bringIntoView()
}
}
},
isPassword = true,
label = stringResource(R.string.password),
icon = Icons.Outlined.Lock,
onValueChange = {
// Do not trim password, passes through `Validator`
viewModel.onEvent(SignupUIEvent.PasswordChanged(it))
},
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
),
passStatus = viewModel.uiState.passwordPass
)
Spacer(modifier = Modifier.padding(10.dp))
Column {
// Current Password Requirements
PasswordCondition(
passStatus = viewModel.uiState.passwordLengthPass,
label = "At least 6 characters long"
)
PasswordCondition(
passStatus = viewModel.uiState.passwordSpecialCharPass,
label = "Includes a special character"
)
PasswordCondition(
passStatus = viewModel.uiState.passwordUppercasePass,
label = "Includes an uppercase character"
)
PasswordCondition(
passStatus = viewModel.uiState.passwordNumberPass,
label = "Includes a number"
)
PasswordCondition(
passStatus = viewModel.uiState.passwordWhitespacePass,
label = "No whitespace characters"
)
}
Spacer(modifier = Modifier.padding(10.dp))
CustomOutlinedTextField(
// Confirm Password Field
modifier = Modifier
.imePadding()
.bringIntoViewRequester(passCheckRequester)
.onFocusEvent { focusState ->
if (focusState.isFocused) {
scope.launch {
passCheckRequester.bringIntoView()
}
}
},
isPassword = true,
label = stringResource(R.string.confirm_password),
onValueChange = {
// Do not trim password, passes through `Validator`
viewModel.onEvent(SignupUIEvent.ConfirmPasswordChanged(it))
},
icon = Icons.Outlined.Lock,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
),
passStatus = viewModel.uiState.confirmPasswordPass
)
AuthCheckboxComponent(
// EULA Component
onCheckedChange = {
// Clear focus when clicked on EULA
viewModel.onEvent(SignupUIEvent.EULACheckboxClicked(it))
focusManager.clearFocus()
},
onTextClicked = { eulaItem = it },
passStatus = viewModel.uiState.eulaPass
)
}
Spacer(modifier = Modifier.weight(1f))
// Bottom Component
BottomComponent(
textQuery = "Already have an account? ",
textClickable = "Login",
action = "Register",
navController = navController,
onButtonClicked = {
// Pass the NavHostController for signup handling
viewModel.onEvent(SignupUIEvent.RegisterButtonClicked(navController))
if (!viewModel.isFormValid()) {
showFieldErrorDialog = true
}
if (!viewModel.isEulaAccepted() && viewModel.isFormValid()) {
showEULAToast = true
}
},
// Button is disabled when sign process is happening
isEnabled = !viewModel.signupProgress
)
// Bottom Sheet for EULA
if (eulaItem.isNotEmpty()) {
ModalBottomSheet(
onDismissRequest = {
eulaItem = ""
},
sheetState = sheetState,
modifier = Modifier.fillMaxHeight(0.9f),
) {
EULADisplay(eulaItem = eulaItem, closeButtonClicked = { eulaItem = "" })
}
}
// Dialog when there are errors in the user fields
if (showFieldErrorDialog) {
AuthDialog(
onDismiss = { showFieldErrorDialog = false },
titleMessage = "Sign Up Error",
statusMessage = "Some information is incorrect"
)
}
// Dialog when there is a signup error
if (viewModel.errorMessage != null) {
AuthDialog(
onDismiss = { viewModel.clearErrorMessage() },
titleMessage = "Signup Failed",
statusMessage = viewModel.errorMessage ?: "An unknown error has occurred"
)
}
// Toast message when EULA is not checked
if (showEULAToast) {
Toast.makeText(context, "Please accept the Terms and Conditions", Toast.LENGTH_SHORT).show()
showEULAToast = false
}
}
)
}
@Composable
fun AppSurface(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
overlayingContent: @Composable () -> Unit = { }
) {
val focusManager = LocalFocusManager.current
Surface(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTapGestures(onTap = {
focusManager.clearFocus()
})
}
.windowInsetsPadding(WindowInsets.statusBars)
) {
Box(modifier = Modifier.fillMaxSize()) {
Column(modifier = modifier
.fillMaxSize()
.padding(28.dp)
.imePadding()
) {
content()
}
overlayingContent()
}
Spacer(
modifier = Modifier
.fillMaxWidth()
.windowInsetsBottomHeight(WindowInsets.navigationBars)
)
}
}