Suppose I have some activity with a jetpack-compose content
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ScrollableColumn(
modifier = Modifier
.fillMaxSize()
.border(4.dp, Color.Red)
) {
val (text, setText) = remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = setText,
label = {},
modifier = Modifier
.fillMaxWidth()
)
for (i in 0..100) {
Text("Item #$i")
}
}
}
}
}
If I were to launch this activity and focus on the TextField a software keyboard would pop up.
The interface, however, would not react to it. ScrollableColumn's bottom border (.border(4.dp, Color.Red)) would not be visible, as well as 100th item (Text("Item #$i")).
In other words, software keyboard overlaps content.
How can I make jetpack compose respect visible area changes (due to software keyboard)?
You can use the standard android procedure, but I don't know if a Compose specific way exists.
If you set the SoftInputMode to SOFT_INPUT_ADJUST_RESIZE, the Layout will resize on keyboard change.
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
setContent { /* Composable Content */ }
}
}
otherwise, you could use the flags in the manifest. See here for more information:
Move layouts up when soft keyboard is shown?
You can use Accompanist's inset library https://google.github.io/accompanist/insets
first use ProvideWindowInsets at the root of your composable hierarchy most of the time below your app theme compose and set windowInsetsAnimationsEnabled true
ProvideWindowInsets(windowInsetsAnimationsEnabled = true) {
// content }
The use navigationBarsWithImePadding() modifier on TextField
OutlinedTextField(
// other params,
modifier = Modifier.navigationBarsWithImePadding() )
Finaly make sure to call WindowCompat.setDecorFitsSystemWindows(window, false) from your activity(inside onCreate). If you want Software keyboard on/off to animate set your activity's windowSoftInputMode adjustResize in AndroidManifests
<activity
android:name=".MyActivity"
android:windowSoftInputMode="adjustResize">
Besides Compose, no external libraries are needed for this now.
Set android:windowSoftInputMode=adjustResize on the activity in your manifest file
e.g.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".MyActivity"
android:windowSoftInputMode="adjustResize"/>
</application>
</manifest>
Then for the composable that you want the UI to react to, you can use Modifier.imePadding() which reacts to IME visibility
Box(Modifier.imePadding()) {
// content
}
I faced the same problem.
Use OnGlobalLayoutListener which will observe the actual IME rect size and will be triggered when the soft keyboard is fully visible.
Worked solution for me:
val bringIntoViewRequester = remember { BringIntoViewRequester() }
val scope = rememberCoroutineScope()
val view = LocalView.current
DisposableEffect(view) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
scope.launch { bringIntoViewRequester.bringIntoView() }
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
TextField(
modifier = Modifier.bringIntoViewRequester(bringIntoViewRequester),
...
)
Origin here
I came up with idea of using accompanist insets library.
Someone could be interested because my approach does not modificate the contents of an application.
I link my post below:
(Compose UI) - Keyboard (IME) overlaps content of app
If you want your TextField scroll up when keyboard appears.
The following it work for me.
You need to add these
class YourActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
setContent { /* Composable Content */ }
}
And add this to your app/build.gradle
implementation "com.google.accompanist:accompanist-insets-ui:0.24.7-alpha"
In my case just adding android:windowSoftInputMode="adjustResize" to activity was enough to solve the problem.
It depends on how you build your UI. If yours screen's root is a vertically scrollable container or a Box, the keyboard resize might get managed automatically.
Use the modifier imePadding(). This will allow the specific compositions to adjust themselves when the keyboard pops up. This does not require you to set any flag on the activity
Related
As shown above, the list of items, the text input field and the add button go up when the user open the keyboard,
I want the list of items to stay in position while the text input field and the add buton go up as it does.
code:
Activity:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
OlegarioLopezTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) { Navigation() }
}
}
}
The Navigation() func just call the Composable
Composable:
#Composable
fun ListScreen(
viewModel: MainScreenViewModel,
navController: NavController
) {
LazyColumn{...}
MainTextField(viewModel)
AddButton(viewModel)
}
Ensure that the activity's windowSoftInputMode is set to adjustResize:
<activity
android:name=".MyActivity"
android:windowSoftInputMode="adjustResize">
</activity>
In this way the activity's main window is always resized to make room for the soft keyboard on screen.
Then just use a layout as:
Column() {
LazyColumn(Modifier.weight(1f)) {
//..
}
Row(){
TextField()
Button()
}
}
This was exactly my problem with an extra twist that it only became an issue after the activity returned from pause.
A brand new state had no issue, but when resuming suddenly my entire mainactivity was being pushed up with the keyboard. SUPER weird behavior.
I actually set windowSoftInputMode to "adjustPan" abd it works as expected now, which is how it worked as a fresh activity.
I have a simple view like this. In the emulator, when I press Tab on the keyboard, nothing happens. There's no indication that the button in focused, and the onClick is not called when I press Enter.
NOTE: This is for accessibility, i.e., those who cannot use their fingers to tap and need an indicator that the button has focus, or those who use the app in a tablet + keyboard.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(color = MaterialTheme.colors.background) {
Button(onClick = { println("clicked") }) {
Text(text = "A Button")
}
}
}
}
}
Please help. The Compose version is 1.0.5.
By the way,
I've read almost all pages of Compose documentation in Android Developers, including Accessibility in Compose, but I cannot find any useful information that solves my simple problem.
Compose TextField could receive focus either by pressing Tab or arrow keys.
I've also tried to place a View Button and a Compose Button side by side, only ViewButton could receive focus.
Update
val iSource = remember { MutableInteractionSource() }
val focused by iSource.collectIsFocusedAsState()
val pressed by iSource.collectIsPressedAsState()
Button(
onClick = { println("clicked") },
Modifier.focusable(interactionSource = iSource),
) {
Text(text = "A Button (focused=${focused}, pressed=${pressed})")
}
This way, "focused" become true when I navigate by using arrow keys (Up/Down), but:
There's no focused indication.
Pressing Tab still doesn't work.
When it's focused, pressing Space or Enter doesn't trigger the onClick listener.
I have create a simple example with six TextFields inside a LazyColumn, when you click the last TextField, the keyboard hides it, if you hide the keyboard and click again last TextField, works fine.
In the AndroidManifest I use "adjustPan"
android:windowSoftInputMode="adjustPan"
This is a capture when you click the last TextField first time, hides the last TextField
This is a capture when you click the last TextField second time, works correctly
This is the code
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
TestComposeTheme {
val numbers = listOf(1,2,3,4,5,6)
LazyColumn() {
items(numbers) { index->
TextField(index = index)
}
}
}
}
}
}
#Composable
fun TextField(index: Int){
var text by remember { mutableStateOf("Hello$index") }
TextField(
modifier = Modifier.padding(25.dp),
value = text,
onValueChange = { text = it },
label = { Text("TextField$index") }
)
}
Does anyone know if there is any way that the first time the last TextField is tapped, it would prevent the keyboard from hiding it
EDIT: There is a known issue:
192043120
This is already a known issue. https://issuetracker.google.com/issues/192043120
One hack to overcome this is use a column with verticalScroll
Column(Modifier.verticalScroll(rememberScrollState(), reverseScrolling = true){
// Content
}
put this in your app AndroidManifest.xml inside activity tag
<activity
...
android:windowSoftInputMode="adjustResize|stateVisible">
When I click on TextField, I need to scroll UI upwards to show login button to the user and not hide it behind keyboard.
I am using RelocationRequester for the same.
I am using this for detecting keyboard show/hide event:
fun listenKeyboard() {
val activityRootView =
(requireActivity().findViewById<View>(android.R.id.content) as ViewGroup).getChildAt(0)
activityRootView.viewTreeObserver.addOnGlobalLayoutListener(object :
ViewTreeObserver.OnGlobalLayoutListener {
private var wasOpened = false
private val DefaultKeyboardDP = 100
private val EstimatedKeyboardDP =
DefaultKeyboardDP + 48
private val r: Rect = Rect()
override fun onGlobalLayout() {
val estimatedKeyboardHeight = TypedValue
.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
EstimatedKeyboardDP.toFloat(),
activityRootView.resources.displayMetrics
)
.toInt()
activityRootView.getWindowVisibleDisplayFrame(r)
val heightDiff: Int = activityRootView.rootView.height - (r.bottom - r.top)
val isShown = heightDiff >= estimatedKeyboardHeight
if (isShown == wasOpened) {
return
}
wasOpened = isShown
keyboardVisibleState(isShown)
}
})
}
and once the keyboard is visible, I am calling the relocationRequestor's bringIntoView().
coroutineScope.launch {
delay(250)
relocationRequester.bringIntoView()
}
Its behaving randomly, working on some devices and not on others. Is there any better solution to deal with this issue?
Since Compose 1.2.0-alpha03, Accompanist Insets was mostly moved into Compose Foundation, check out migration guide for more details. The main changes to below answer is that ProvideWindowInsets is no longer needed and some imports should be replaced.
You can use accompanist insets. You'll be able to check if keyboard is presented using LocalWindowInsets.current.ime.isVisible or add .imePadding() to your screen container.
This works great. But to make it work you'll have to disable window decor fitting:
This library does not disable window decor fitting. For your view hierarchy to able to receive insets, you need to make sure to call: WindowCompat.setDecorFitsSystemWindows(window, false) from your Activity. You also need to set the system bar backgrounds to be transparent, which can be done with our System UI Controller library.
If you don't want to do this, you will have to look for another solution.
Example in onCreate:
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
ProvideWindowInsets {
ComposePlaygroundTheme {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.statusBarsPadding()
.navigationBarsWithImePadding()
.padding(10.dp)
) {
TextField(value = "", onValueChange = {})
Spacer(Modifier.weight(1f))
Button(onClick = {}) {
Text(text = "Proceed")
}
}
}
}
}
p.s. Also this won't help with lazy views: it's gonna decrease container size, but won't scroll to selected item. Waiting this issue to be resolved
As the insets from accompanist are deprecated, the ones at androidx.compose.foundation should be used.
I was able to use WindowInsets.Companion.ime.getBottom(LocalDensity.current) > 0 to check whether the IME is shown or not. Otherwise Modifier.imePadding() can also be used if it fits your usecase better. I am not exactly sure whether this properly works for landscape mode & other weird keyboard combinations, but it seems to work in portrait mode with a "bottom" keyboard.
The "official" API for this is still in the Feature Request stage, it's on the issue tracker: https://issuetracker.google.com/issues/217770337
You can also just add the following line to your Manifest in the activity part :
android:windowSoftInputMode="adjustResize"
Like in this example:
<activity
android:name=".MainActivity"
android:exported="true"
android:label="#string/app_name"
android:windowSoftInputMode="adjustResize"
android:theme="#style/Theme.MyApp">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
I want to programmatically change which tab is selected as the user scrolls past each "see more" item in the list below. How would I best accomplish this?
As Ryan M writes, you can use LazyListState.firstVisibleItemIndex. The magic of Compose is that you can just use it in an if statement and Compose will do the work. Look at the following example, which displays a different text based on the first visible item. Similarly, you can select a different tab based on the first visible item.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val listState = rememberLazyListState()
Column {
Text(if (listState.firstVisibleItemIndex < 100) "< 100" else ">= 100")
LazyColumn(state = listState) {
items(1000) {
Text(
text = "$it",
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
}
}
Just create a NestedScrollConnection and assign it to parent view nestedScroll modifier:
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
// called when you scroll the content
return Offset.Zero
}
}
}
Assign it to LazyColumn or to its parent composed view:
Modifier.nestedScroll(nestedScrollConnection)
Exemple:
LazyColumn(
...
modifier = Modifier.nestedScroll(nestedScrollConnection)
) {
items(...) {
Text("Your text...")
}
}
You can use the LazyListState.firstVisibleItemIndex property (obtained via rememberLazyListState and set as the state parameter to LazyColumn) and set the current tab based on that.
Reading that property is considered a model read in Compose, so it will trigger a recomposition whenever the first visible item changes.
If you instead want to do something more complex based on more than just the first visible item, you can use LazyListState.layoutInfo to get information about all visible items and their locations, rather than just the first.