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
android:name=".MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustNothing"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|uiMode|density|keyboard|keyboardHidden|navigation">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@@ -9,9 +9,6 @@ import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.material3.Surface
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.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@@ -28,7 +25,6 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
val isDebuggable = (applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
WebView.setWebContentsDebuggingEnabled(isDebuggable)
applyImmersiveMode()
NodeForegroundService.start(this)
permissionRequester = PermissionRequester(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() {
super.onStart()
viewModel.setForeground(true)
@@ -80,12 +64,4 @@ class MainActivity : ComponentActivity() {
viewModel.setForeground(false)
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.only
import androidx.compose.foundation.layout.offset
@@ -41,6 +40,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import ai.openclaw.android.MainViewModel
@@ -65,10 +65,8 @@ private enum class StatusVisual {
}
@Composable
@OptIn(ExperimentalLayoutApi::class)
fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier) {
var activeTab by rememberSaveable { mutableStateOf(HomeTab.Connect) }
val imeVisible = WindowInsets.isImeVisible
val statusText by viewModel.statusText.collectAsState()
val isConnected by viewModel.isConnected.collectAsState()
@@ -96,19 +94,29 @@ fun PostOnboardingTabs(viewModel: MainViewModel, modifier: Modifier = Modifier)
)
},
bottomBar = {
if (!imeVisible) {
BottomTabBar(
activeTab = activeTab,
onSelect = { activeTab = it },
)
}
BottomTabBar(
activeTab = activeTab,
onSelect = { activeTab = it },
)
},
) { 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(
modifier =
Modifier
.fillMaxSize()
.padding(innerPadding)
.padding(
top = innerPadding.calculateTopPadding(),
bottom = contentBottomPadding,
)
.background(mobileBackgroundGradient),
) {
when (activeTab) {

View File

@@ -5,6 +5,7 @@ import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
@@ -161,6 +162,7 @@ fun ChatComposer(
label = "Refresh",
icon = Icons.Default.Refresh,
enabled = true,
compact = true,
onClick = onRefresh,
)
@@ -168,6 +170,7 @@ fun ChatComposer(
label = "Abort",
icon = Icons.Default.Stop,
enabled = pendingRunCount > 0,
compact = true,
onClick = onAbort,
)
}
@@ -196,7 +199,12 @@ fun ChatComposer(
Icon(Icons.AutoMirrored.Filled.Send, contentDescription = null, modifier = Modifier.size(16.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,
icon: androidx.compose.ui.graphics.vector.ImageVector,
enabled: Boolean,
compact: Boolean = false,
onClick: () -> Unit,
) {
Button(
onClick = onClick,
enabled = enabled,
modifier = Modifier.height(44.dp),
modifier = if (compact) Modifier.size(44.dp) else Modifier.height(44.dp),
shape = RoundedCornerShape(14.dp),
colors =
ButtonDefaults.buttonColors(
@@ -222,15 +231,17 @@ private fun SecondaryActionButton(
disabledContentColor = mobileTextTertiary,
),
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))
Spacer(modifier = Modifier.width(5.dp))
Text(
text = label,
style = mobileCallout.copy(fontWeight = FontWeight.SemiBold),
color = if (enabled) mobileTextSecondary else mobileTextTertiary,
)
if (!compact) {
Spacer(modifier = Modifier.width(5.dp))
Text(
text = label,
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -122,33 +123,35 @@ fun ChatSheetContent(viewModel: MainViewModel) {
modifier = Modifier.weight(1f, fill = true),
)
ChatComposer(
healthOk = healthOk,
thinkingLevel = thinkingLevel,
pendingRunCount = pendingRunCount,
attachments = attachments,
onPickImages = { pickImages.launch("image/*") },
onRemoveAttachment = { id -> attachments.removeAll { it.id == id } },
onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) },
onRefresh = {
viewModel.refreshChat()
viewModel.refreshChatSessions(limit = 200)
},
onAbort = { viewModel.abortChat() },
onSend = { text ->
val outgoing =
attachments.map { att ->
OutgoingAttachment(
type = "image",
mimeType = att.mimeType,
fileName = att.fileName,
base64 = att.base64,
)
}
viewModel.sendChat(message = text, thinking = thinkingLevel, attachments = outgoing)
attachments.clear()
},
)
Row(modifier = Modifier.fillMaxWidth().imePadding()) {
ChatComposer(
healthOk = healthOk,
thinkingLevel = thinkingLevel,
pendingRunCount = pendingRunCount,
attachments = attachments,
onPickImages = { pickImages.launch("image/*") },
onRemoveAttachment = { id -> attachments.removeAll { it.id == id } },
onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) },
onRefresh = {
viewModel.refreshChat()
viewModel.refreshChatSessions(limit = 200)
},
onAbort = { viewModel.abortChat() },
onSend = { text ->
val outgoing =
attachments.map { att ->
OutgoingAttachment(
type = "image",
mimeType = att.mimeType,
fileName = att.fileName,
base64 = att.base64,
)
}
viewModel.sendChat(message = text, thinking = thinkingLevel, attachments = outgoing)
attachments.clear()
},
)
}
}
}