mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 11:48:38 +00:00
fix(android): stabilize chat composer ime and tab layout
This commit is contained in:
@@ -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" />
|
||||||
|
|||||||
@@ -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())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user