Reputation: 209
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
Upvotes: 3
Views: 13534
Reputation: 131
I did like this:
Make sure that in you AndroidMainifest.xml
you set:
android:windowSoftInputMode="adjustResize"
Add this function (within the screen or in one other file):
@Composable
fun rememberImeState(): State<Boolean> {
val imeState = remember {
mutableStateOf(false)
}
val view = LocalView.current
DisposableEffect(view) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
val isKeyboardOpen = ViewCompat.getRootWindowInsets(view)
?.isVisible(WindowInsetsCompat.Type.ime()) ?: true
imeState.value = isKeyboardOpen
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
}
return imeState
}
val imeState = rememberImeState()
val scrollState = rememberScrollState()
LaunchedEffect(key1 = imeState.value) {
if (imeState.value) {
scrollState.animateScrollTo(scrollState.maxValue, tween(300))
}
}
Column(
modifier = modifier.verticalScroll(scrollState),
verticalArrangement = Arrangement.Top,
horizontalAlignment = CenterHorizontally
) {
UserSelection(
onClick = { onClick(it) }
)
}
Upvotes: 0
Reputation: 5074
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
)
Upvotes: 0
Reputation: 334
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")
}
}
}
Upvotes: 4
Reputation: 209
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:
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!
Upvotes: 7