(Compose UI) - Keyboard (IME) overlaps content of app - android

A few days ago I bumped on a problem where a part of my view is overlaped by keyboard.
Let's say we have 3 different dialogs (could be any content), which looks like this:
When I want to write in anything, last dialog is covered by keyboard:
And there's no way to see what user wrote. Here's my code:
#Composable
fun BuildWordsView(navController: NavController, sharedViewModel: SharedViewModel) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(PrimaryLight)
.fillMaxSize()
) {
BuildWordsScreenContents()
}
}
#Composable
fun BuildWordsScreenContents() {
Column(
Modifier
.fillMaxSize()
.padding(all = 16.dp)
) {
val inputBoxModifier = Modifier
.clip(RoundedCornerShape(10.dp))
.background(Primary)
.weight(12f)
.wrapContentHeight()
InputBlock("Dialog1", inputBoxModifier)
Spacer(Modifier.weight(1f))
InputBlock("Dialog2", inputBoxModifier)
Spacer(Modifier.weight(1f))
InputBlock("Dialog3", inputBoxModifier)
}
}
#Composable
fun InputBlock(dialogText: String, inputBlockModifier: Modifier) {
Column(modifier = inputBlockModifier) {
Text(
dialogText,
fontSize = 30.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
)
var text by remember { mutableStateOf("") }
TextField(
value = text,
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center),
onValueChange = { text = it },
label = { Text("Label") }
)
}
}
This question seems to be similar to mine but answers modificate the content of view which I want to avoid:
Software keyboard overlaps content of jetpack compose view
By now I figured out how to solve this problem and I share my approach as an answer

My approach to deal with this problem is using Insets for Jetpack Compose:
https://google.github.io/accompanist/insets/
In order to start dealing with problem you need to add depency to gradle (current version is 0.22.0-rc).
dependencies {
implementation "com.google.accompanist:accompanist-insets:0.22.0-rc"
}
Then you need to wrap your content in your activity with ProvideWindowInsets
setContent {
ProvideWindowInsets {
YourTheme {
//YOUR CONTENT HERE
}
}
}
Additionaly you need to add following line in your activity onCreate() function:
WindowCompat.setDecorFitsSystemWindows(window, false)
Update: Despite this function is recommended, to my experience it may make this approach not work. If you face any problem, you may need to delete this line.
Now your project is set up to use Insets
In the next steps I'm gonna use code I provided in question
First of all you need to wrap your main Column with
ProvideWindowInsets(windowInsetsAnimationsEnabled = true)
Then let's modificate a modifier a bit by adding:
.statusBarsPadding()
.navigationBarsWithImePadding()
.verticalScroll(rememberScrollState())
As you can see the trick in my approach is to use verticalScroll(). Final code of main column should look like this:
#Composable
fun BuildWordsView(navController: NavController, sharedViewModel: SharedViewModel) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(PrimaryLight)
.statusBarsPadding()
.navigationBarsWithImePadding()
.verticalScroll(rememberScrollState())
.fillMaxSize()
) {
BuildWordsScreenContents()
}
}
}
Now let's modificate the modifier of Column in fun BuildWordsScreenContents()
The main modification is that we provide a height of our screen by:
.height(LocalConfiguration.current.screenHeightDp.dp)
This means that height of our Column would fit our screen perfectly. So when keyboard is not opened the Column will not be scrollable
There is the full code:
#Composable
fun BuildWordsView(navController: NavController, sharedViewModel: SharedViewModel) {
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(PrimaryLight)
.statusBarsPadding()
.navigationBarsWithImePadding()
.verticalScroll(rememberScrollState())
.fillMaxSize()
) {
BuildWordsScreenContents()
}
}
}
#Composable
fun BuildWordsScreenContents() {
Column(
Modifier
.height(LocalConfiguration.current.screenHeightDp.dp)
.padding(all = 16.dp)
) {
val inputBoxModifier = Modifier
.clip(RoundedCornerShape(10.dp))
.background(Primary)
.weight(12f)
.wrapContentHeight()
InputBlock("Dialog1", inputBoxModifier)
Spacer(Modifier.weight(1f))
InputBlock("Dialog2", inputBoxModifier)
Spacer(Modifier.weight(1f))
InputBlock("Dialog3", inputBoxModifier)
}
}
#Composable
fun InputBlock(dialogText: String, inputBlockModifier: Modifier) {
Column(modifier = inputBlockModifier) {
Text(
dialogText,
fontSize = 30.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
)
var text by remember { mutableStateOf("") }
TextField(
value = text,
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center),
onValueChange = { text = it },
label = { Text("Label") }
)
}
}
The final code allows us to scroll down the view:
Important Note For APIs 30-
For APIs lower then 30 you need to modificate the AndroidManifest.xml file
In <activity you need to add android:windowSoftInputMode="adjustResize" in order to make it work. It do not resize your components but it is obligatory to make this approach work
Manifest should look like this:
<activity
android:name=".MainActivity"
android:windowSoftInputMode="adjustResize"
Feel free to give me any tips how can I improve my question. AFAIK this problem is as old as android and I wanted to create a quick tutorial how to manage that. Happy coding!

Here's my solution, using the experimental features in Compose 1.2.0
In build.gradle (:project)
...
ext {
compose_version = '1.2.0-beta03'
}
...
In build.gradle (:app)
...
dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material:material:$compose_version"
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation "androidx.compose.foundation:foundation-layout:$compose_version"
...
}
In AndroidManifest.xml
<activity
...
android:windowSoftInputMode="adjustResize" >
In AuthScreen.kt
#OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
#Composable
fun AuthScreen(
val focusManager = LocalFocusManager.current
val coroutineScope = rememberCoroutineScope()
// Setup the handles to items to scroll to.
val bringIntoViewRequesters = mutableListOf(remember { BringIntoViewRequester() })
repeat(6) {
bringIntoViewRequesters += remember { BringIntoViewRequester() }
}
val buttonViewRequester = remember { BringIntoViewRequester() }
fun requestBringIntoView(focusState: FocusState, viewItem: Int) {
if (focusState.isFocused) {
coroutineScope.launch {
delay(200) // needed to allow keyboard to come up first.
if (viewItem >= 2) { // force to scroll to button for lower fields
buttonViewRequester.bringIntoView()
} else {
bringIntoViewRequesters[viewItem].bringIntoView()
}
}
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top,
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.navigationBarsPadding()
.imePadding()
.padding(10.dp)
.verticalScroll(rememberScrollState())
) {
repeat(6) { viewItem ->
Row(
modifier = Modifier
.bringIntoViewRequester(bringIntoViewRequesters[viewItem]),
) {
TextField(
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
keyboardActions = KeyboardActions(
onNext = { focusManager.moveFocus(FocusDirection.Down) }),
modifier = Modifier
.onFocusEvent { focusState ->
requestBringIntoView(focusState, viewItem)
},
)
}
}
Button(
onClick = {},
modifier = Modifier
.bringIntoViewRequester(buttonViewRequester)
) {
Text(text = "I'm Visible")
}
}
}

Try to google into such keywords: Modifier.statusBarsPadding(), systemBarsPadding(), navigationBarsPadding().
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeStatusBarTransparent()
//WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
Box(
Modifier
.background(Color.Blue)
.fillMaxSize()
.padding(top = 10.dp, bottom = 10.dp)
.statusBarsPadding() //systemBarsPadding
) {
//Box(Modifier.background(Color.Green).navigationBarsPadding()) {
Greeting("TopStart", Alignment.TopStart)
Greeting("BottomStart", Alignment.BottomStart)
Greeting("TopEnd", Alignment.TopEnd)
Greeting("BottomEnd", Alignment.BottomEnd)
//}
}
}
/* setContent {
MyComposeApp1Theme {
// A surface container using the 'background' color from the theme
Surface(modifier = Modifier.fillMaxSize(), color = Color.Red) {
Box(Modifier
.fillMaxSize()
.padding(top = 34.dp)
) {
Greeting("Android")
}
}
}
}*/
}
}
#Composable
fun Greeting(name: String, contentAlignment: Alignment) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = contentAlignment
) {
Text(
text = "Hello $name!",
Modifier
.background(color = Color.Cyan)
)
}
}
#Preview(showBackground = true)
#Composable
fun DefaultPreview() {
MyComposeApp1Theme {
Greeting("Android", Alignment.TopStart)
}
}
#Suppress("DEPRECATION")
fun Activity.makeStatusBarTransparent() {
window.apply {
clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
statusBarColor = android.graphics.Color.GREEN//android.graphics.Color.TRANSPARENT
}
}
val Int.dp
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
toFloat(),
Resources.getSystem().displayMetrics
)

Related

Icon does not become invisible in jetpack compose

I have simple viewpager and toolbar in my code :
#Composable
fun DefaultAppBar(
mainViewModel: MainViewModel
) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl ) {
TopAppBar(
backgroundColor = Color.White
,
title = {
Text(text = "",
modifier = Modifier
.clickable { }
// margin
.padding(start = 160.dp)
)
},
actions = {
if(mainViewModel.searchWidgetVisibility.value) {
IconButton(
onClick = { },
modifier = Modifier,
) {
Icon(
imageVector = Icons.Filled.Search,
contentDescription = "Search Icon",
tint = Color.Black,
modifier = Modifier,
)
}
}
}
)
}
}
As you can see above, I have an search icon which I want it to be visible whenever I enter SecondScreen in my viewpager .
MainViewModel.kt :
class MainViewModel:ViewModel() {
private val _searchWidgetVisibility: MutableState<Boolean> = mutableStateOf(value = false)
val searchWidgetVisibility: MutableState<Boolean> = _searchWidgetVisibility
fun updateSearchWidgetVisibility(newValue:Boolean) {
_searchWidgetVisibility.value = newValue
}
}
The searchWidgetVisibility controls the visibility of my search icon.
These are my pages in my viewpager :
#Composable
fun SecondScreen(
mainViewModel: MainViewModel
) {
mainViewModel.updateSearchWidgetVisibility(true)
Box(
modifier = Modifier
.fillMaxSize()
.background(color = C2)
,
contentAlignment = Alignment.Center
) {
Text(text = "second screen")
}
}
#Composable
fun FirstScreen(
mainViewModel: MainViewModel
) {
mainViewModel.updateSearchWidgetVisibility(false)
Box(
modifier = Modifier
.fillMaxSize()
.background(color = C2)
,
contentAlignment = Alignment.Center
) {
Text(text = "first screen")
}
}
But that does not work. when my search icon become visible for the first time,
when I navigate to the FirstScreen, It does not become invisible.
what is the problem here?
You need to place your mainViewModel.updateSearchWidgetVisibility(false) to not be called in your composition, Compose code needs to be side effect free. Because Composables can run at any time, in parallel or not at all. The position of where you are running that line of code, is not guaranteed to be executed at all.
More info here: https://developer.android.com/jetpack/compose/mental-model#parallel
If you need those functions to run when that composable is added to the composition, you should use LaunchedEffect to do so.
#Composable
fun FirstScreen(
mainViewModel: MainViewModel
) {
LaunchedEffect(mainViewModel) {
mainViewModel.updateSearchWidgetVisibility(false)
}
Box(
modifier = Modifier
.fillMaxSize()
.background(color = C2)
,
contentAlignment = Alignment.Center
) {
Text(text = "first screen")
}
}

Jetpack compose - Lazy Column crashes on Recompose

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

Jetpack Compose onClick { ... } Toast not showing

I am trying to learn jetpack compose and this is the code I am trying out
My Activity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
UserList()
// MessageCard()
}
}
}
My Data class
data class User(val id: Int)
val users = listOf(User(1), User(2), User(3), User(4), User(5), User(5))
#Composable
fun UserList( ) {
LazyColumn {
items(users){
user ->MessageCard()
}
}
}
#Composable
fun MessageCard() {
val context = LocalContext.current
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.clickable {
Toast.makeText(context, R.string.app_name,Toast.LENGTH_SHORT)
},
backgroundColor = MaterialTheme.colors.surface.copy(alpha = .5f)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "Title",
modifier = Modifier
.padding(horizontal = 15.dp)
.fillMaxWidth(),
style = MaterialTheme.typography.h6,
)
Text(
text = "Sub title", modifier = Modifier
.padding(horizontal = 15.dp)
.fillMaxWidth(),
style = MaterialTheme.typography.subtitle1
)
}
}
}
}
The Toast message does not appear when I click a MessageCard
Any help is greatly appreciated.
You forgot to call Toast's show()
Toast.makeText(context, R.string.app_name,Toast.LENGTH_SHORT).show()

Open ModalSheetLayout on TextField focus instead of Keyboard

I am working with Jetpack Compose and I have a page with several TextFields, where I want that, in several of them, when I click on the input, instead of appearing the keyboard, a ModalSheetLayout appears with different layouts that I have. Is this possible? I'm still not able to do this, all I can do is simple things when the focus changes. Can anyone help me?
The below sample should give a basic idea of how to do this.
Code
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun BottomSheetSelectionDemo() {
val coroutineScope: CoroutineScope = rememberCoroutineScope()
val modalBottomSheetState: ModalBottomSheetState = rememberModalBottomSheetState(
initialValue = ModalBottomSheetValue.Hidden,
)
val colors = arrayListOf("Red", "Green", "Blue", "White", "Black")
val (value, setValue) = remember {
mutableStateOf(colors[0])
}
val toggleModalBottomSheetState = {
coroutineScope.launch {
if (!modalBottomSheetState.isAnimationRunning) {
if (modalBottomSheetState.isVisible) {
modalBottomSheetState.hide()
} else {
modalBottomSheetState.show()
}
}
}
}
ModalBottomSheetLayout(
sheetState = modalBottomSheetState,
sheetContent = {
LazyColumn {
items(colors) {
Text(
text = it,
modifier = Modifier
.fillMaxWidth()
.clickable {
setValue(it)
toggleModalBottomSheetState()
}
.padding(
horizontal = 16.dp,
vertical = 12.dp,
),
)
}
}
},
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.fillMaxSize(),
) {
MyReadOnlyTextField(
value = value,
label = "Select a color",
onClick = {
toggleModalBottomSheetState()
},
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 4.dp,
),
)
}
}
}
#Composable
fun MyReadOnlyTextField(
modifier: Modifier = Modifier,
value: String,
label: String,
onClick: () -> Unit,
) {
Box(
modifier = modifier,
) {
androidx.compose.material3.OutlinedTextField(
value = value,
onValueChange = {},
modifier = Modifier
.fillMaxWidth(),
label = {
Text(
text = label,
)
},
)
Box(
modifier = Modifier
.matchParentSize()
.alpha(0f)
.clickable(
onClick = onClick,
),
)
}
}

How we can use bottom sheet in jetpack compose?

I am start to migrate my project to jetpack compose, and I am learning jetpack compose now, I want to use bottom sheet in my project, I search on internet to use bottom sheet, I find some codes, I use it in my app, every things looks good, but when I run the my app, it crashed, I am not sure where I mistake? Is there any other solution?
class MyActivity : ComponentActivity() {
#ExperimentalMaterialApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ModalBottomSheetLayoutScreen()
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun ModalBottomSheetLayoutScreen() {
val modalBottomSheetState = rememberModalBottomSheetState(initialValue =
ModalBottomSheetValue.Hidden)
val scope = rememberCoroutineScope()
ModalBottomSheetLayout(
sheetContent = {
},
sheetState = modalBottomSheetState,
sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
sheetBackgroundColor = colorResource(id = R.color.white),
// scrimColor = Color.Red, // Color for the fade background when you open/close the drawer
) {
Scaffold(
backgroundColor = colorResource(id = R.color.white)
) {
MyScreen(scope = scope, state = modalBottomSheetState)
}
}
}
#OptIn(ExperimentalMaterialApi::class)
#Composable
fun MyScreen(
scope: CoroutineScope, state: ModalBottomSheetState
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier
.width(170.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "click",
modifier = Modifier
.clickable {
scope.launch {
state.show()
}
}
)}} }
#Preview(showBackground = true)
#Composable
fun ModalBottomSheetLayoutScreenPreview() {
ModalBottomSheetLayoutScreen()
}
It crashes because your sheetContent is empty. I've reported this bug. In a real app you should have some content, otherwise you don't need ModalBottomSheetLayout at all.
Specifying any view inside sheetContent solves the problem.

Categories

Resources