3

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:

enter image description here

When I want to write in anything, last dialog is covered by keyboard:

enter image description here

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

Drogheda
  • 209
  • 3
  • 16
  • Does this answer your question? [Software keyboard overlaps content of jetpack compose view](https://stackoverflow.com/questions/64050392/software-keyboard-overlaps-content-of-jetpack-compose-view) – Phil Dukhov Jan 06 '22 at 00:18
  • You shouldn't create an other question just because current answers doesn't fit you. You should left your own answer there instead. – Phil Dukhov Jan 06 '22 at 00:19
  • @PhilipDukhov it does not answer my question and I mentioned it. Please read carefully my problem. The main difference is that I do not want to create scrollable layout but leave it as it is (however I use this as a trick). Moreover I do not show any lists in my view. That are completly different problems – Drogheda Jan 06 '22 at 01:15

3 Answers3

7

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:

enter image description here

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!

Drogheda
  • 209
  • 3
  • 16
  • 1
    you're using fillMaxWidth() and fillMaxSize() I think you forgot to remove fillMaxWidth() – Edhar Khimich Mar 24 '22 at 21:12
  • 2
    Accompanist is deprecated – Kristy Welsh May 31 '22 at 15:06
  • Hello In my case, I have to handle both the scenario in my project: Example: Case 1: Need to resize the screen (AdjustResize) Case 2: No need to resize the screen (AdjustResize) If we give this in the manifest and activity how can I handle these two scenarios in my project: Note: Assume that both are different composes and screens. – Tippu Fisal Sheriff Oct 28 '22 at 05:37
  • @TippuFisalSheriff did you find a solution? I also need to use AdjustResize only for some composable screens where it's going to work normally (scrollable message list and input field at the bottom of the screen to type a new message) but for other screens it breaks the logic and input field appears behind keyboard (like login, sign up, where input field at the center). – user924 May 01 '23 at 17:55
  • Still, I didn't find the solution to the above. – Tippu Fisal Sheriff May 02 '23 at 05:37
4

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")
        }
    }
}
RealityExpander
  • 133
  • 1
  • 8
  • Hello In my case, I have to handle both the scenario in my project: Example: Case 1: Need to resize the screen (AdjustResize) Case 2: No need to resize the screen (AdjustResize) If we give this in the manifest and activity how can I handle these two scenarios in my project: Note: Assume that both are different composes and screens. – Tippu Fisal Sheriff Oct 28 '22 at 05:37
  • @TippuFisalSheriff I guess the only way would to have the composes in different activities. I dont know of a way to change this at run time. Let me know what you find out. – RealityExpander Oct 28 '22 at 06:37
  • Yeah sure, but we follow single-activity architecture – Tippu Fisal Sheriff Oct 28 '22 at 07:02
  • @TippuFisalSheriff Same. Not sure if there is a solution, ive only seen the "modify the Manifest" solution, which seems rather archaic, like most the android libraries and techniques! – RealityExpander Oct 28 '22 at 07:28
  • can't understand, can you please explain in detail – Tippu Fisal Sheriff Oct 28 '22 at 07:31
  • The only way (that I know) to adjust the resize/pan is in the `AndroidManifest.xml` file. Other than that, Im not sure how... – RealityExpander Nov 16 '22 at 04:20
0

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
    )
yozhik
  • 4,644
  • 14
  • 65
  • 98
  • Hello In my case, I have to handle both the scenario in my project: Example: Case 1: Need to resize the screen (AdjustResize) Case 2: No need to resize the screen (AdjustResize) If we give this in the manifest and activity how can I handle these two scenarios in my project: Note: Assume that both are different composes and screens. – Tippu Fisal Sheriff Oct 28 '22 at 05:37