keyboard jumpy fix
This commit is contained in:
@@ -17,3 +17,4 @@ See `patterns.md` for detailed design conventions.
|
|||||||
- MainScreen uses `@Immutable` data class for ProfileSheetState (good practice)
|
- MainScreen uses `@Immutable` data class for ProfileSheetState (good practice)
|
||||||
- Drag gesture threshold: 60f px for swipe-open/close channel list
|
- Drag gesture threshold: 60f px for swipe-open/close channel list
|
||||||
- LazyListState for channel list is owned by ViewModel (correct — survives recomposition)
|
- LazyListState for channel list is owned by ViewModel (correct — survives recomposition)
|
||||||
|
- IME insets: enableEdgeToEdge() is active; Scaffold default contentWindowInsets excludes IME; MessageTimeline Column must carry `.imePadding()` before static padding — without it the keyboard overlays the input bar. AndroidView EditText does not auto-participate in Compose IME avoidance. Never use windowSoftInputMode=adjustResize with edge-to-edge.
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop">
|
android:launchMode="singleTop"
|
||||||
|
android:windowSoftInputMode="adjustNothing">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@@ -44,6 +45,8 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
@@ -289,7 +292,8 @@ fun MessageTimeline(
|
|||||||
.padding(
|
.padding(
|
||||||
top = contentPadding.calculateTopPadding(),
|
top = contentPadding.calculateTopPadding(),
|
||||||
bottom = contentPadding.calculateBottomPadding(),
|
bottom = contentPadding.calculateBottomPadding(),
|
||||||
),
|
)
|
||||||
|
.imePadding(),
|
||||||
) {
|
) {
|
||||||
if (selectedChannel != null) {
|
if (selectedChannel != null) {
|
||||||
if (selectedThread != null) {
|
if (selectedThread != null) {
|
||||||
@@ -1372,18 +1376,78 @@ private fun MessageInput(
|
|||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TextField(
|
val surfaceVariant = MaterialTheme.colorScheme.surfaceVariant
|
||||||
value = text,
|
val onSurface = MaterialTheme.colorScheme.onSurface
|
||||||
onValueChange = { text = it },
|
val onSurfaceVariant = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
placeholder = { Text("Message #$channelName", color = MaterialTheme.colorScheme.onSurfaceVariant) },
|
val textStyle = MaterialTheme.typography.bodyLarge
|
||||||
modifier = Modifier.weight(1f).clip(RoundedCornerShape(8.dp)).heightIn(max = 160.dp),
|
val density = LocalDensity.current
|
||||||
maxLines = 8,
|
AndroidView(
|
||||||
colors = TextFieldDefaults.colors(
|
factory = { ctx ->
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
object : android.widget.EditText(ctx) {
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
|
override fun requestRectangleOnScreen(rect: android.graphics.Rect?, immediate: Boolean): Boolean {
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
// Disable system scroll-into-view; Compose imePadding() handles it
|
||||||
focusedIndicatorColor = Color.Transparent,
|
return false
|
||||||
),
|
}
|
||||||
|
}.apply {
|
||||||
|
hint = "Message #$channelName"
|
||||||
|
setHintTextColor(onSurfaceVariant.toArgb())
|
||||||
|
setTextColor(onSurface.toArgb())
|
||||||
|
setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, textStyle.fontSize.value)
|
||||||
|
background = android.graphics.drawable.GradientDrawable().apply {
|
||||||
|
setColor(surfaceVariant.toArgb())
|
||||||
|
cornerRadius = with(density) { 8.dp.toPx() }
|
||||||
|
}
|
||||||
|
setPadding(
|
||||||
|
with(density) { 16.dp.toPx().toInt() },
|
||||||
|
with(density) { 12.dp.toPx().toInt() },
|
||||||
|
with(density) { 16.dp.toPx().toInt() },
|
||||||
|
with(density) { 12.dp.toPx().toInt() },
|
||||||
|
)
|
||||||
|
maxLines = 8
|
||||||
|
inputType = android.text.InputType.TYPE_CLASS_TEXT or
|
||||||
|
android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE or
|
||||||
|
android.text.InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||||
|
isSingleLine = false
|
||||||
|
|
||||||
|
// Prevent EditText from fighting with Compose's imePadding()
|
||||||
|
imeOptions = android.view.inputmethod.EditorInfo.IME_FLAG_NO_EXTRACT_UI
|
||||||
|
|
||||||
|
addTextChangedListener(object : android.text.TextWatcher {
|
||||||
|
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
|
||||||
|
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
|
||||||
|
override fun afterTextChanged(s: android.text.Editable?) {
|
||||||
|
text = s?.toString() ?: ""
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
androidx.core.view.ViewCompat.setOnReceiveContentListener(
|
||||||
|
this,
|
||||||
|
arrayOf("image/*"),
|
||||||
|
) { _, payload ->
|
||||||
|
val clip = payload.clip
|
||||||
|
var remaining = payload
|
||||||
|
for (i in 0 until clip.itemCount) {
|
||||||
|
val uri = clip.getItemAt(i).uri
|
||||||
|
if (uri != null) {
|
||||||
|
attachedUris = attachedUris + uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Return null to indicate all content was consumed
|
||||||
|
if (clip.itemCount > 0 && (0 until clip.itemCount).any { clip.getItemAt(it).uri != null }) null
|
||||||
|
else remaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { editText ->
|
||||||
|
if (editText.text.toString() != text) {
|
||||||
|
editText.setText(text)
|
||||||
|
editText.setSelection(text.length)
|
||||||
|
}
|
||||||
|
editText.hint = "Message #$channelName"
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.heightIn(max = 160.dp),
|
||||||
)
|
)
|
||||||
if (emojiPacks.isNotEmpty()) {
|
if (emojiPacks.isNotEmpty()) {
|
||||||
IconButton(onClick = { showEmojiPackPicker = true }) {
|
IconButton(onClick = { showEmojiPackPicker = true }) {
|
||||||
|
|||||||
Reference in New Issue
Block a user