fix(android): stabilize chat composer ime and tab layout

This commit is contained in:
Ayaan Zaidi
2026-02-25 13:35:33 +05:30
committed by Ayaan Zaidi
parent f894c23e64
commit 959cbafcdb
5 changed files with 70 additions and 71 deletions

View File

@@ -50,6 +50,7 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustNothing"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|uiMode|density|keyboard|keyboardHidden|navigation"> android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|uiMode|density|keyboard|keyboardHidden|navigation">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

View File

@@ -9,9 +9,6 @@ import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle import androidx.lifecycle.repeatOnLifecycle
@@ -28,7 +25,6 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val isDebuggable = (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 val isDebuggable = (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
WebView.setWebContentsDebuggingEnabled(isDebuggable) WebView.setWebContentsDebuggingEnabled(isDebuggable)
applyImmersiveMode()
NodeForegroundService.start(this) NodeForegroundService.start(this)
permissionRequester = PermissionRequester(this) permissionRequester = PermissionRequester(this)
screenCaptureRequester = ScreenCaptureRequester(this) screenCaptureRequester = ScreenCaptureRequester(this)
@@ -59,18 +55,6 @@ class MainActivity : ComponentActivity() {
} }
} }
override fun onResume() {
super.onResume()
applyImmersiveMode()
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
applyImmersiveMode()
}
}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
viewModel.setForeground(true) viewModel.setForeground(true)
@@ -80,12 +64,4 @@ class MainActivity : ComponentActivity() {
viewModel.setForeground(false) viewModel.setForeground(false)
super.onStop() super.onStop()
} }
private fun applyImmersiveMode() {
WindowCompat.setDecorFitsSystemWindows(window, false)
val controller = WindowInsetsControllerCompat(window, window.decorView)
controller.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
controller.hide(WindowInsetsCompat.Type.systemBars())
}
} }

View File

@@ -5,14 +5,13 @@ import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.isImeVisible import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
@@ -41,6 +40,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import ai.openclaw.android.MainViewModel import ai.openclaw.android.MainViewModel
@@ -65,10 +65,8 @@ private enum class StatusVisual {
} }
@Composable @Composable
@OptIn(ExperimentalLayoutApi::class)
fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier) { fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier) {
var activeTab by rememberSaveable { mutableStateOf(HomeTab.Connect) } var activeTab by rememberSaveable { mutableStateOf(HomeTab.Connect) }
val imeVisible = WindowInsets.isImeVisible
val statusText by viewModel.statusText.collectAsState() val statusText by viewModel.statusText.collectAsState()
val isConnected by viewModel.isConnected.collectAsState() val isConnected by viewModel.isConnected.collectAsState()
@@ -96,19 +94,29 @@ fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier)
) )
}, },
bottomBar = { bottomBar = {
if (!imeVisible) { BottomTabBar(
BottomTabBar( activeTab = activeTab,
activeTab = activeTab, onSelect = { activeTab = it },
onSelect = { activeTab = it }, )
)
}
}, },
) { innerPadding -> ) { innerPadding ->
val density = LocalDensity.current
val imeVisible = WindowInsets.ime.getBottom(density) > 0
val contentBottomPadding =
if (activeTab == HomeTab.Chat && imeVisible) {
0.dp
} else {
innerPadding.calculateBottomPadding()
}
Box( Box(
modifier = modifier =
Modifier Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding) .padding(
top = innerPadding.calculateTopPadding(),
bottom = contentBottomPadding,
)
.background(mobileBackgroundGradient), .background(mobileBackgroundGradient),
) { ) {
when (activeTab) { when (activeTab) {

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -161,6 +162,7 @@ fun ChatComposer(
label = "Refresh", label = "Refresh",
icon = Icons.Default.Refresh, icon = Icons.Default.Refresh,
enabled = true, enabled = true,
compact = true,
onClick = onRefresh, onClick = onRefresh,
) )
@@ -168,6 +170,7 @@ fun ChatComposer(
label = "Abort", label = "Abort",
icon = Icons.Default.Stop, icon = Icons.Default.Stop,
enabled = pendingRunCount > 0, enabled = pendingRunCount > 0,
compact = true,
onClick = onAbort, onClick = onAbort,
) )
} }
@@ -196,7 +199,12 @@ fun ChatComposer(
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, modifier = Modifier.size(16.dp)) Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, modifier = Modifier.size(16.dp))
} }
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
Text("Send", style = mobileHeadline.copy(fontWeight = FontWeight.Bold)) Text(
text = "Send",
style = mobileHeadline.copy(fontWeight = FontWeight.Bold),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} }
} }
} }
@@ -207,12 +215,13 @@ private fun SecondaryActionButton(
label: String, label: String,
icon: androidx.compose.ui.graphics.vector.ImageVector, icon: androidx.compose.ui.graphics.vector.ImageVector,
enabled: Boolean, enabled: Boolean,
compact: Boolean = false,
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Button( Button(
onClick = onClick, onClick = onClick,
enabled = enabled, enabled = enabled,
modifier = Modifier.height(44.dp), modifier = if (compact) Modifier.size(44.dp) else Modifier.height(44.dp),
shape = RoundedCornerShape(14.dp), shape = RoundedCornerShape(14.dp),
colors = colors =
ButtonDefaults.buttonColors( ButtonDefaults.buttonColors(
@@ -222,15 +231,17 @@ private fun SecondaryActionButton(
disabledContentColor = mobileTextTertiary, disabledContentColor = mobileTextTertiary,
), ),
border = BorderStroke(1.dp, mobileBorderStrong), border = BorderStroke(1.dp, mobileBorderStrong),
contentPadding = ButtonDefaults.ContentPadding, contentPadding = if (compact) PaddingValues(0.dp) else ButtonDefaults.ContentPadding,
) { ) {
Icon(icon, contentDescription = label, modifier = Modifier.size(14.dp)) Icon(icon, contentDescription = label, modifier = Modifier.size(14.dp))
Spacer(modifier = Modifier.width(5.dp)) if (!compact) {
Text( Spacer(modifier = Modifier.width(5.dp))
text = label, Text(
style = mobileCallout.copy(fontWeight = FontWeight.SemiBold), text = label,
color = if (enabled) mobileTextSecondary else mobileTextTertiary, style = mobileCallout.copy(fontWeight = FontWeight.SemiBold),
) color = if (enabled) mobileTextSecondary else mobileTextTertiary,
)
}
} }
} }

View File

@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@@ -122,33 +123,35 @@ fun ChatSheetContent(viewModel: MainViewModel) {
modifier = Modifier.weight(1f, fill = true), modifier = Modifier.weight(1f, fill = true),
) )
ChatComposer( Row(modifier = Modifier.fillMaxWidth().imePadding()) {
healthOk = healthOk, ChatComposer(
thinkingLevel = thinkingLevel, healthOk = healthOk,
pendingRunCount = pendingRunCount, thinkingLevel = thinkingLevel,
attachments = attachments, pendingRunCount = pendingRunCount,
onPickImages = { pickImages.launch("image/*") }, attachments = attachments,
onRemoveAttachment = { id -> attachments.removeAll { it.id == id } }, onPickImages = { pickImages.launch("image/*") },
onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) }, onRemoveAttachment = { id -> attachments.removeAll { it.id == id } },
onRefresh = { onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) },
viewModel.refreshChat() onRefresh = {
viewModel.refreshChatSessions(limit = 200) viewModel.refreshChat()
}, viewModel.refreshChatSessions(limit = 200)
onAbort = { viewModel.abortChat() }, },
onSend = { text -> onAbort = { viewModel.abortChat() },
val outgoing = onSend = { text ->
attachments.map { att -> val outgoing =
OutgoingAttachment( attachments.map { att ->
type = "image", OutgoingAttachment(
mimeType = att.mimeType, type = "image",
fileName = att.fileName, mimeType = att.mimeType,
base64 = att.base64, fileName = att.fileName,
) base64 = att.base64,
} )
viewModel.sendChat(message = text, thinking = thinkingLevel, attachments = outgoing) }
attachments.clear() viewModel.sendChat(message = text, thinking = thinkingLevel, attachments = outgoing)
}, attachments.clear()
) },
)
}
} }
} }