Handling keyboard inset for Android sdk < 30

This commit is contained in:
ThomasSession 2024-07-26 15:49:41 +10:00
parent e813756fb3
commit 456f8d0b3a
2 changed files with 126 additions and 55 deletions

View File

@ -27,7 +27,11 @@ import org.thoughtcrime.securesms.groups.JoinCommunityFragment
@AndroidEntryPoint @AndroidEntryPoint
class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate { class StartConversationFragment : BottomSheetDialogFragment(), StartConversationDelegate {
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * 0.94).toInt() } companion object{
const val PEEK_RATIO = 0.94f
}
private val defaultPeekHeight: Int by lazy { (Resources.getSystem().displayMetrics.heightPixels * PEEK_RATIO).toInt() }
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, inflater: LayoutInflater, container: ViewGroup?,

View File

@ -1,10 +1,13 @@
package org.thoughtcrime.securesms.conversation.start.newmessage package org.thoughtcrime.securesms.conversation.start.newmessage
import androidx.compose.animation.AnimatedVisibility import android.graphics.Rect
import android.os.Build
import android.view.ViewTreeObserver
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.consumeWindowInsets
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.height import androidx.compose.foundation.layout.height
@ -15,23 +18,31 @@ import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import network.loki.messenger.R import network.loki.messenger.R
import org.thoughtcrime.securesms.onboarding.ui.ContinuePrimaryOutlineButton import org.thoughtcrime.securesms.conversation.start.StartConversationFragment.Companion.PEEK_RATIO
import org.thoughtcrime.securesms.ui.LoadingArcOr import org.thoughtcrime.securesms.ui.LoadingArcOr
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.components.AppBar import org.thoughtcrime.securesms.ui.components.AppBar
import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon import org.thoughtcrime.securesms.ui.components.BorderlessButtonWithIcon
import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode import org.thoughtcrime.securesms.ui.components.MaybeScanQrCode
@ -39,7 +50,13 @@ import org.thoughtcrime.securesms.ui.components.PrimaryOutlineButton
import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField import org.thoughtcrime.securesms.ui.components.SessionOutlinedTextField
import org.thoughtcrime.securesms.ui.components.SessionTabRow import org.thoughtcrime.securesms.ui.components.SessionTabRow
import org.thoughtcrime.securesms.ui.contentDescription import org.thoughtcrime.securesms.ui.contentDescription
import org.thoughtcrime.securesms.ui.theme.LocalColors
import org.thoughtcrime.securesms.ui.theme.LocalDimensions
import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.LocalType
import org.thoughtcrime.securesms.ui.theme.PreviewTheme
import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider
import org.thoughtcrime.securesms.ui.theme.ThemeColors
import kotlin.math.max
private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan) private val TITLES = listOf(R.string.enter_account_id, R.string.qrScan)
@ -76,63 +93,113 @@ private fun EnterAccountId(
callbacks: Callbacks, callbacks: Callbacks,
onHelp: () -> Unit = {} onHelp: () -> Unit = {}
) { ) {
Column( // the scaffold is required to provide the contentPadding. That contentPadding is needed
modifier = Modifier // to properly handle the ime padding.
.fillMaxSize() Scaffold() { contentPadding ->
.verticalScroll(rememberScrollState()) // we need this extra surface to handle nested scrolling properly,
.imePadding() // because this scrollable component is inside a bottomSheet dialog which is itself scrollable
) { Surface(
Column( modifier = Modifier.nestedScroll(rememberNestedScrollInteropConnection()),
modifier = Modifier.padding(vertical = LocalDimensions.current.spacing), color = LocalColors.current.backgroundSecondary
horizontalAlignment = Alignment.CenterHorizontally,
) { ) {
SessionOutlinedTextField(
text = state.newMessageIdOrOns,
modifier = Modifier
.padding(horizontal = LocalDimensions.current.spacing),
contentDescription = "Session id input box",
placeholder = stringResource(R.string.accountIdOrOnsEnter),
onChange = callbacks::onChange,
onContinue = callbacks::onContinue,
error = state.error?.string(),
isTextErrorColor = state.isTextErrorColor
)
Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing)) var accountModifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
BorderlessButtonWithIcon( // There is a known issue with the ime padding on android versions below 30
text = stringResource(R.string.messageNewDescription), // So we these older versions we need to resort to some manual padding based on the visible height
modifier = Modifier // when the keyboard is up
.contentDescription(R.string.AccessibilityId_help_desk_link) if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
.padding(horizontal = LocalDimensions.current.mediumSpacing) val keyboardHeight by keyboardHeight()
.fillMaxWidth(), accountModifier = accountModifier.padding(bottom = keyboardHeight)
style = LocalType.current.small, } else {
color = LocalColors.current.textSecondary, accountModifier = accountModifier
iconRes = R.drawable.ic_circle_question_mark, .consumeWindowInsets(contentPadding)
onClick = onHelp .imePadding()
) }
}
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) Column(
Spacer(Modifier.weight(2f)) modifier = accountModifier
) {
Column(
modifier = Modifier.padding(vertical = LocalDimensions.current.spacing),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SessionOutlinedTextField(
text = state.newMessageIdOrOns,
modifier = Modifier
.padding(horizontal = LocalDimensions.current.spacing),
contentDescription = "Session id input box",
placeholder = stringResource(R.string.accountIdOrOnsEnter),
onChange = callbacks::onChange,
onContinue = callbacks::onContinue,
error = state.error?.string(),
isTextErrorColor = state.isTextErrorColor
)
PrimaryOutlineButton( Spacer(modifier = Modifier.height(LocalDimensions.current.xxxsSpacing))
modifier = Modifier
.align(Alignment.CenterHorizontally) BorderlessButtonWithIcon(
.padding(horizontal = LocalDimensions.current.xlargeSpacing) text = stringResource(R.string.messageNewDescription),
.padding(bottom = LocalDimensions.current.smallSpacing) modifier = Modifier
.fillMaxWidth() .contentDescription(R.string.AccessibilityId_help_desk_link)
.contentDescription(R.string.next), .padding(horizontal = LocalDimensions.current.mediumSpacing)
enabled = state.isNextButtonEnabled, .fillMaxWidth(),
onClick = callbacks::onContinue style = LocalType.current.small,
) { color = LocalColors.current.textSecondary,
LoadingArcOr(state.loading) { iconRes = R.drawable.ic_circle_question_mark,
Text(stringResource(R.string.next)) onClick = onHelp
)
}
Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing))
Spacer(Modifier.weight(2f))
PrimaryOutlineButton(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(horizontal = LocalDimensions.current.xlargeSpacing)
.padding(bottom = LocalDimensions.current.smallSpacing)
.fillMaxWidth()
.contentDescription(R.string.next),
enabled = state.isNextButtonEnabled,
onClick = callbacks::onContinue
) {
LoadingArcOr(state.loading) {
Text(stringResource(R.string.next))
}
}
} }
} }
} }
} }
@Composable
fun keyboardHeight(): MutableState<Dp> {
val view = LocalView.current
var keyboardHeight = remember { mutableStateOf(0.dp) }
val density = LocalDensity.current
DisposableEffect(view) {
val listener = ViewTreeObserver.OnGlobalLayoutListener {
val rect = Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height * PEEK_RATIO
val keypadHeightPx = max( screenHeight - rect.bottom, 0f)
keyboardHeight.value = with(density) { keypadHeightPx.toDp() }
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
}
return keyboardHeight
}
@Preview @Preview
@Composable @Composable
private fun PreviewNewMessage( private fun PreviewNewMessage(