[#789] Fix Send Screen Layout

* [#804][Design system] Paddings - Dimens

- Initial commit to ensure it meets our requirements
- This solution is now in parallel with a simpler existing Paddings solution
- Four spacing groups defined
- Provides a way to define default and custom spacing values
- Distinguished by screen size, but other metrics also available (e.g. orientation, aspect ratio, layout direction or screen shape)

* Fix spacing value

* Move spacings change logic to comment

- We've decided to have only one regular spacing group for now, which is suitable for most of current phone devices

* Move Dimens out of internal package

* Link TODO for later Paddings remove

* Link issue of Use Dimens across the app to TODO

* Add vertical scrolling feature to SendForm screen

* Add vertical scrolling feature to Settings screen

+ Switched from Paddings to Dimens spacing system

* Add paddings, vertical scrolling, move modifiers

- As part of this is also move modifiers to the caller side where it makes sense

* Improve Send screen layout

- Added header view to the screen
- Added spacers between some UI parts of the screen
- Fixed background colors
- And improved textfields sizes

* Link Fix ZEC balance number formatting issue

* Link Disable Memo UI field in case of Transparent address issue

* Replace all .dp with predefined Dimens spacing in SendView

* Create custom FormTextField UI component

* Re-write paddings to spacers in list
This commit is contained in:
Honza Rychnovsky 2023-03-16 15:36:06 +01:00 committed by GitHub
parent 5cfa3a99e3
commit d37310a935
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 151 additions and 64 deletions

View File

@ -17,10 +17,12 @@ import co.electriccoin.zcash.ui.design.theme.ZcashTheme
fun Header( fun Header(
text: String, text: String,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
color: Color = ZcashTheme.colors.onBackgroundHeader textAlign: TextAlign = TextAlign.Start,
color: Color = ZcashTheme.colors.onBackgroundHeader,
) { ) {
Text( Text(
text = text, text = text,
textAlign = textAlign,
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
color = color, color = color,
modifier = modifier modifier = modifier
@ -46,8 +48,8 @@ fun Body(
@Composable @Composable
fun Small( fun Small(
text: String, text: String,
textAlign: TextAlign, modifier: Modifier = Modifier,
modifier: Modifier = Modifier textAlign: TextAlign = TextAlign.Start
) { ) {
Text( Text(
text = text, text = text,

View File

@ -0,0 +1,34 @@
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType
@Suppress("LongParameterList")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FormTextField(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
label: @Composable (() -> Unit)? = null,
keyboardOptions: KeyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text),
colors: TextFieldColors = TextFieldDefaults.textFieldColors(
containerColor = Color.Transparent
)
) {
TextField(
value = value,
onValueChange = onValueChange,
label = label,
keyboardOptions = keyboardOptions,
colors = colors,
modifier = modifier
)
}

View File

@ -2,14 +2,15 @@ package co.electriccoin.zcash.ui.screen.send.view
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
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.Spacer import androidx.compose.foundation.layout.Spacer
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.height
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.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -17,7 +18,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -29,8 +29,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import cash.z.ecc.android.sdk.model.Memo import cash.z.ecc.android.sdk.model.Memo
import cash.z.ecc.android.sdk.model.MonetarySeparators import cash.z.ecc.android.sdk.model.MonetarySeparators
import cash.z.ecc.android.sdk.model.Zatoshi import cash.z.ecc.android.sdk.model.Zatoshi
@ -42,10 +42,14 @@ import cash.z.ecc.android.sdk.model.toZecString
import cash.z.ecc.sdk.fixture.ZatoshiFixture import cash.z.ecc.sdk.fixture.ZatoshiFixture
import co.electriccoin.zcash.ui.R import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT import co.electriccoin.zcash.ui.design.MINIMAL_WEIGHT
import co.electriccoin.zcash.ui.design.component.Body
import co.electriccoin.zcash.ui.design.component.FormTextField
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.Header
import co.electriccoin.zcash.ui.design.component.PrimaryButton import co.electriccoin.zcash.ui.design.component.PrimaryButton
import co.electriccoin.zcash.ui.design.component.TimedButton import co.electriccoin.zcash.ui.design.component.TimedButton
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
import co.electriccoin.zcash.ui.screen.send.ext.ABBREVIATION_INDEX import co.electriccoin.zcash.ui.screen.send.ext.ABBREVIATION_INDEX
import co.electriccoin.zcash.ui.screen.send.ext.Saver import co.electriccoin.zcash.ui.screen.send.ext.Saver
import co.electriccoin.zcash.ui.screen.send.ext.abbreviated import co.electriccoin.zcash.ui.screen.send.ext.abbreviated
@ -92,12 +96,21 @@ fun Send(
}) })
}) { paddingValues -> }) { paddingValues ->
SendMainContent( SendMainContent(
paddingValues, myBalance = mySpendableBalance,
mySpendableBalance, sendStage = sendStage,
sendStage, pressAndHoldInteractionSource = pressAndHoldInteractionSource,
pressAndHoldInteractionSource, setSendStage = setSendStage,
setSendStage, onCreateAndSend = onCreateAndSend,
onCreateAndSend = onCreateAndSend modifier = Modifier
.verticalScroll(
rememberScrollState()
)
.padding(
top = paddingValues.calculateTopPadding() + dimens.spacingDefault,
bottom = dimens.spacingDefault,
start = dimens.spacingDefault,
end = dimens.spacingDefault
)
) )
} }
} }
@ -123,32 +136,34 @@ private fun SendTopAppBar(onBack: () -> Unit) {
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
private fun SendMainContent( private fun SendMainContent(
paddingValues: PaddingValues,
myBalance: Zatoshi, myBalance: Zatoshi,
sendStage: SendStage, sendStage: SendStage,
pressAndHoldInteractionSource: MutableInteractionSource, pressAndHoldInteractionSource: MutableInteractionSource,
setSendStage: (SendStage) -> Unit, setSendStage: (SendStage) -> Unit,
onCreateAndSend: (ZecSend) -> Unit onCreateAndSend: (ZecSend) -> Unit,
modifier: Modifier = Modifier
) { ) {
val (zecSend, setZecSend) = rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(null) } val (zecSend, setZecSend) = rememberSaveable(stateSaver = ZecSend.Saver) { mutableStateOf(null) }
if (sendStage == SendStage.Form || null == zecSend) { if (sendStage == SendStage.Form || null == zecSend) {
SendForm( SendForm(
paddingValues,
myBalance = myBalance, myBalance = myBalance,
previousZecSend = zecSend previousZecSend = zecSend,
) { onCreateAndSend = {
setSendStage(SendStage.Confirmation) setSendStage(SendStage.Confirmation)
setZecSend(it) setZecSend(it)
} },
modifier = modifier
)
} else { } else {
Confirmation( Confirmation(
paddingValues, zecSend = zecSend,
zecSend, pressAndHoldInteractionSource = pressAndHoldInteractionSource,
pressAndHoldInteractionSource onConfirmation = {
) { onCreateAndSend(zecSend)
onCreateAndSend(zecSend) },
} modifier = modifier
)
} }
} }
@ -159,15 +174,17 @@ private fun SendMainContent(
@Composable @Composable
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
private fun SendForm( private fun SendForm(
paddingValues: PaddingValues,
myBalance: Zatoshi, myBalance: Zatoshi,
previousZecSend: ZecSend?, previousZecSend: ZecSend?,
onCreateAndSend: (ZecSend) -> Unit onCreateAndSend: (ZecSend) -> Unit,
modifier: Modifier = Modifier
) { ) {
val context = LocalContext.current val context = LocalContext.current
val monetarySeparators = MonetarySeparators.current() val monetarySeparators = MonetarySeparators.current()
val allowedCharacters = ZecString.allowedCharacters(monetarySeparators) val allowedCharacters = ZecString.allowedCharacters(monetarySeparators)
// TODO [#809]: Fix ZEC balance on Send screen
// TODO [#809]: https://github.com/zcash/secant-android-wallet/issues/809
var amountZecString by rememberSaveable { var amountZecString by rememberSaveable {
mutableStateOf(previousZecSend?.amount?.toZecString() ?: "") mutableStateOf(previousZecSend?.amount?.toZecString() ?: "")
} }
@ -181,41 +198,58 @@ private fun SendForm(
} }
Column( Column(
Modifier modifier
.fillMaxHeight() .fillMaxHeight()
.padding(top = paddingValues.calculateTopPadding())
) { ) {
Row(Modifier.fillMaxWidth()) { Header(
Text(text = myBalance.toZecString()) text = stringResource(id = R.string.send_balance, myBalance.toZecString()),
} textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
Body(
text = stringResource(id = R.string.send_balance_subtitle),
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
TextField( Spacer(modifier = Modifier.height(dimens.spacingLarge))
FormTextField(
value = amountZecString, value = amountZecString,
onValueChange = { newValue -> onValueChange = { newValue ->
if (!ZecStringExt.filterContinuous(context, monetarySeparators, newValue)) { if (!ZecStringExt.filterContinuous(context, monetarySeparators, newValue)) {
return@TextField return@FormTextField
} }
amountZecString = newValue.filter { allowedCharacters.contains(it) } amountZecString = newValue.filter { allowedCharacters.contains(it) }
}, },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
label = { Text(stringResource(id = R.string.send_amount)) } label = { Text(stringResource(id = R.string.send_amount)) },
modifier = Modifier.fillMaxWidth()
) )
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(dimens.spacingSmall))
TextField( FormTextField(
value = recipientAddressString, value = recipientAddressString,
onValueChange = { recipientAddressString = it }, onValueChange = { recipientAddressString = it },
label = { Text(stringResource(id = R.string.send_to)) } label = { Text(stringResource(id = R.string.send_to)) },
modifier = Modifier.fillMaxWidth()
) )
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(dimens.spacingSmall))
TextField(value = memoString, onValueChange = { // TODO [#810]: Disable Memo UI field in case of Transparent address
if (Memo.isWithinMaxLength(it)) { // TODO [#810]: https://github.com/zcash/secant-android-wallet/issues/810
memoString = it FormTextField(
} value = memoString,
}, label = { Text(stringResource(id = R.string.send_memo)) }) onValueChange = {
if (Memo.isWithinMaxLength(it)) {
memoString = it
}
},
label = { Text(stringResource(id = R.string.send_memo)) },
modifier = Modifier.fillMaxWidth()
)
Spacer(Modifier.fillMaxHeight(MINIMAL_WEIGHT)) Spacer(Modifier.fillMaxHeight(MINIMAL_WEIGHT))
@ -225,9 +259,14 @@ private fun SendForm(
* without regard for RTL. This will get resolved once we do proper validation for * without regard for RTL. This will get resolved once we do proper validation for
* the fields. * the fields.
*/ */
Text(validation.joinToString(", ")) Text(
text = validation.joinToString(", "),
modifier = Modifier.fillMaxWidth()
)
} }
Spacer(modifier = Modifier.height(dimens.spacingDefault))
PrimaryButton( PrimaryButton(
onClick = { onClick = {
val zecSendValidation = ZecSendExt.new( val zecSendValidation = ZecSendExt.new(
@ -253,15 +292,12 @@ private fun SendForm(
@Composable @Composable
private fun Confirmation( private fun Confirmation(
paddingValues: PaddingValues,
zecSend: ZecSend, zecSend: ZecSend,
pressAndHoldInteractionSource: MutableInteractionSource, pressAndHoldInteractionSource: MutableInteractionSource,
onConfirmation: () -> Unit onConfirmation: () -> Unit,
modifier: Modifier = Modifier
) { ) {
Column( Column(modifier) {
Modifier
.padding(top = paddingValues.calculateTopPadding())
) {
Text( Text(
stringResource( stringResource(
R.string.send_amount_and_address_format, R.string.send_amount_and_address_format,

View File

@ -1,7 +1,11 @@
package co.electriccoin.zcash.ui.screen.settings.view package co.electriccoin.zcash.ui.screen.settings.view
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
@ -25,7 +29,7 @@ import co.electriccoin.zcash.ui.R
import co.electriccoin.zcash.ui.design.component.GradientSurface import co.electriccoin.zcash.ui.design.component.GradientSurface
import co.electriccoin.zcash.ui.design.component.SwitchWithLabel import co.electriccoin.zcash.ui.design.component.SwitchWithLabel
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.design.theme.ZcashTheme.paddings import co.electriccoin.zcash.ui.design.theme.ZcashTheme.dimens
@Preview("Settings") @Preview("Settings")
@Composable @Composable
@ -75,10 +79,16 @@ fun Settings(
onBackgroundSyncSettingsChanged = onBackgroundSyncSettingsChanged, onBackgroundSyncSettingsChanged = onBackgroundSyncSettingsChanged,
onIsKeepScreenOnDuringSyncSettingsChanged = onIsKeepScreenOnDuringSyncSettingsChanged, onIsKeepScreenOnDuringSyncSettingsChanged = onIsKeepScreenOnDuringSyncSettingsChanged,
onAnalyticsSettingsChanged = onAnalyticsSettingsChanged, onAnalyticsSettingsChanged = onAnalyticsSettingsChanged,
modifier = Modifier.padding( modifier = Modifier
top = paddingValues.calculateTopPadding() + paddings.padding, .verticalScroll(
bottom = paddings.padding rememberScrollState()
) )
.padding(
top = paddingValues.calculateTopPadding() + dimens.spacingDefault,
bottom = dimens.spacingDefault,
start = dimens.spacingDefault,
end = dimens.spacingDefault
)
) )
} }
} }
@ -150,20 +160,23 @@ private fun SettingsMainContent(
SwitchWithLabel( SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_background_sync), label = stringResource(id = R.string.settings_enable_background_sync),
state = isBackgroundSyncEnabled, state = isBackgroundSyncEnabled,
onStateChange = { onBackgroundSyncSettingsChanged(!isBackgroundSyncEnabled) }, onStateChange = { onBackgroundSyncSettingsChanged(!isBackgroundSyncEnabled) }
modifier = Modifier.padding(paddings.padding)
) )
Spacer(modifier = Modifier.height(dimens.spacingXlarge))
SwitchWithLabel( SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_keep_screen_on), label = stringResource(id = R.string.settings_enable_keep_screen_on),
state = isKeepScreenOnDuringSyncEnabled, state = isKeepScreenOnDuringSyncEnabled,
onStateChange = { onIsKeepScreenOnDuringSyncSettingsChanged(!isKeepScreenOnDuringSyncEnabled) }, onStateChange = { onIsKeepScreenOnDuringSyncSettingsChanged(!isKeepScreenOnDuringSyncEnabled) }
modifier = Modifier.padding(paddings.padding)
) )
Spacer(modifier = Modifier.height(dimens.spacingXlarge))
SwitchWithLabel( SwitchWithLabel(
label = stringResource(id = R.string.settings_enable_analytics), label = stringResource(id = R.string.settings_enable_analytics),
state = isAnalyticsEnabled, state = isAnalyticsEnabled,
onStateChange = { onAnalyticsSettingsChanged(!isAnalyticsEnabled) }, onStateChange = { onAnalyticsSettingsChanged(!isAnalyticsEnabled) }
modifier = Modifier.padding(paddings.padding)
) )
} }
} }

View File

@ -2,6 +2,8 @@
<string name="send_title">Send ZEC</string> <string name="send_title">Send ZEC</string>
<string name="send_back_content_description">Back</string> <string name="send_back_content_description">Back</string>
<string name="send_scan_content_description">Scan</string> <string name="send_scan_content_description">Scan</string>
<string name="send_balance" formatted="true"><xliff:g id="balance" example="12.345">%1$s</xliff:g> ZEC Available</string>
<string name="send_balance_subtitle">Additional funds may be in transit.</string>
<string name="send_to">Who would you like to send ZEC to?</string> <string name="send_to">Who would you like to send ZEC to?</string>
<string name="send_amount">How much?</string> <string name="send_amount">How much?</string>
<string name="send_memo">Memo</string> <string name="send_memo">Memo</string>