[#1467] Activity resizing according to soft keyboard (#1506)

* [#1467] Activity resizing according to soft keyboard

Closes #1467

* [#1467] Seed keyboard handling

* [#1467] Seed keyboard handling

Closes #1467

* [#1467] Documentation update

* TODO reference

* [#1467] Documentation update

Closes #1467

* [#1467] Detekt fix

Closes #1467

---------

Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Milan 2024-07-19 12:00:50 +02:00 committed by GitHub
parent 0bc7757aa2
commit 741601b65f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 35 additions and 22 deletions

View File

@ -17,6 +17,10 @@ and this application adheres to [Semantic Versioning](https://semver.org/spec/v2
- The Restore Success dialog has been reworked into a separate screen, allowing users to opt out of the Keep screen - The Restore Success dialog has been reworked into a separate screen, allowing users to opt out of the Keep screen
on while restoring option on while restoring option
## Fixed
- Support Screen now shows the Send button above keyboard instead of overlaying it. This was achieved by setting
`adjustResize` to `MainActivity` and adding `imePadding` to top level composable
## [1.1.3 (682)] - 2024-07-03 ## [1.1.3 (682)] - 2024-07-03
### Added ### Added

View File

@ -16,3 +16,6 @@ directly impact users rather than highlighting other key architectural updates.*
- The About screen has been redesigned to align with the new design guidelines - The About screen has been redesigned to align with the new design guidelines
- The Restore Success dialog has been reworked into a separate screen, allowing users to opt out of the Keep screen - The Restore Success dialog has been reworked into a separate screen, allowing users to opt out of the Keep screen
on while restoring option on while restoring option
### Fixed
- Support Screen now shows the Send button above keyboard instead of overlaying it

View File

@ -15,9 +15,10 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="false" android:exported="false"
android:windowSoftInputMode="adjustResize"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/Theme.App.Starting" /> android:theme="@style/Theme.App.Starting" />
</application> </application>
</manifest> </manifest>

View File

@ -11,6 +11,7 @@ import androidx.annotation.VisibleForTesting
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -133,6 +134,7 @@ class MainActivity : AppCompatActivity() {
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight() .fillMaxHeight()
.imePadding()
) { ) {
BindCompLocalProvider { BindCompLocalProvider {
MainContent() MainContent()

View File

@ -30,14 +30,11 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -47,7 +44,6 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.testTag import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
@ -84,7 +80,7 @@ import co.electriccoin.zcash.ui.screen.restore.state.WordList
import co.electriccoin.zcash.ui.screen.restore.state.wordValidation import co.electriccoin.zcash.ui.screen.restore.state.wordValidation
import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentHashSetOf import kotlinx.collections.immutable.persistentHashSetOf
import kotlinx.coroutines.launch import kotlinx.coroutines.delay
@Preview @Preview
@Composable @Composable
@ -224,7 +220,6 @@ fun RestoreWallet(
paste: () -> String?, paste: () -> String?,
onFinished: () -> Unit onFinished: () -> Unit
) { ) {
val scope = rememberCoroutineScope()
var text by rememberSaveable { mutableStateOf("") } var text by rememberSaveable { mutableStateOf("") }
val parseResult = ParseResult.new(completeWordList, text) val parseResult = ParseResult.new(completeWordList, text)
@ -237,10 +232,16 @@ fun RestoreWallet(
} }
// To avoid unnecessary recompositions that this flow produces // To avoid unnecessary recompositions that this flow produces
SideEffect { LaunchedEffect(Unit) {
scope.launch { userWordList.wordValidation().collect {
userWordList.wordValidation().collect { if (it is SeedPhraseValidation.Valid) {
isSeedValid = it is SeedPhraseValidation.Valid // TODO [#1522]: temporary fix to wait for other states to update first
// TODO [#1522]: https://github.com/Electric-Coin-Company/zashi-android/issues/1522
@Suppress("MagicNumber")
delay(100)
isSeedValid = true
} else {
isSeedValid = false
} }
} }
} }
@ -258,6 +259,7 @@ fun RestoreWallet(
} }
) )
} }
RestoreStage.Birthday -> { RestoreStage.Birthday -> {
RestoreSeedBirthdayTopAppBar( RestoreSeedBirthdayTopAppBar(
onBack = { onBack = {
@ -287,6 +289,7 @@ fun RestoreWallet(
.fillMaxWidth() .fillMaxWidth()
) )
} }
RestoreStage.Birthday -> { RestoreStage.Birthday -> {
// No content. The action button is part of scrollable content. // No content. The action button is part of scrollable content.
} }
@ -318,6 +321,7 @@ fun RestoreWallet(
modifier = commonModifier modifier = commonModifier
) )
} }
RestoreStage.Birthday -> { RestoreStage.Birthday -> {
RestoreBirthdayMainContent( RestoreBirthdayMainContent(
zcashNetwork = zcashNetwork, zcashNetwork = zcashNetwork,
@ -400,7 +404,7 @@ private fun RestoreSeedMainContent(
goNext: () -> Unit, goNext: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val scope = rememberCoroutineScope() val focusManager = LocalFocusManager.current
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
val textFieldScrollToHeight = rememberSaveable { mutableIntStateOf(0) } val textFieldScrollToHeight = rememberSaveable { mutableIntStateOf(0) }
@ -468,24 +472,21 @@ private fun RestoreSeedMainContent(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge)) Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
} }
if (isSeedValid) { LaunchedEffect(isSeedValid) {
// Clear focus and hide keyboard to make it easier for users to see the next button if (isSeedValid) {
LocalSoftwareKeyboardController.current?.hide() // Clear focus and hide keyboard to make it easier for users to see the next button
LocalFocusManager.current.clearFocus() focusManager.clearFocus()
}
} }
DisposableEffect(parseResult) { LaunchedEffect(parseResult) {
// Causes the TextFiled to refocus // Causes the TextFiled to refocus
if (!isSeedValid) { if (!isSeedValid) {
focusRequester.requestFocus() focusRequester.requestFocus()
} }
// Causes scroll to the TextField after the first type action
if (text.isNotEmpty() && userWordList.current.value.isEmpty()) { if (text.isNotEmpty() && userWordList.current.value.isEmpty()) {
scope.launch { scrollState.animateScrollTo(textFieldScrollToHeight.intValue)
scrollState.animateScrollTo(textFieldScrollToHeight.intValue)
}
} }
onDispose { /* Nothing to dispose */ }
} }
} }
@ -674,9 +675,11 @@ private fun Autocomplete(
is ParseResult.Autocomplete -> { is ParseResult.Autocomplete -> {
Pair(false, parseResult.suggestions) Pair(false, parseResult.suggestions)
} }
is ParseResult.Warn -> { is ParseResult.Warn -> {
return return
} }
else -> { else -> {
Pair(false, null) Pair(false, null)
} }