[#1412] Receive Page split into horizontal pager

* ISSUE-1412 Receive Page split into horizontal pager

* ISSUE-1412 Code cleanup

* ISSUE-1412 Code cleanup

* Resolve code analysis warnings

* Improve vertical paddings

- So the entire screen is scrollable as expected
- This also moves us towards the newly updated screen design

---------

Co-authored-by: Milan Cerovsky <milan.cerovsky@leeaf.life>
Co-authored-by: Honza <rychnovsky.honza@gmail.com>
This commit is contained in:
Milan-Nerevar 2024-06-14 15:59:17 +02:00 committed by GitHub
parent 0a35c4fffd
commit 4cdb9f3024
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 303 additions and 149 deletions

View File

@ -0,0 +1,129 @@
@file:OptIn(ExperimentalFoundationApi::class)
package co.electriccoin.zcash.ui.design.component
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@Preview
@Composable
private fun PagerTabsPreview() {
ZcashTheme(forceDarkMode = false) {
BlankSurface {
PagerTabs(
pagerState = rememberPagerState { 2 },
tabs = persistentListOf("First", "Second"),
)
}
}
}
@Preview
@Composable
private fun PagerTabsDarkPreview() {
ZcashTheme(forceDarkMode = true) {
BlankSurface {
PagerTabs(
pagerState = rememberPagerState { 2 },
tabs = persistentListOf("First", "Second"),
)
}
}
}
@Composable
fun PagerTabs(
pagerState: PagerState,
tabs: ImmutableList<String>,
modifier: Modifier = Modifier,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
onTabSelected: (index: Int) -> Unit = {},
) {
TabRow(
modifier =
modifier
.padding(horizontal = ZcashTheme.dimens.screenHorizontalSpacingBig)
.border(ZcashTheme.dimens.spacingTiny, ZcashTheme.colors.layoutStroke),
selectedTabIndex = pagerState.currentPage,
divider = {},
indicator = {},
) {
tabs.forEachIndexed { index, tab ->
PagerTab(
title = tab,
selected = pagerState.currentPage == index,
onClick = {
coroutineScope.launch {
onTabSelected(index)
pagerState.animateScrollToPage(index)
}
},
)
}
}
}
@Composable
private fun PagerTab(
title: String,
selected: Boolean,
onClick: () -> Unit,
) {
Tab(
modifier =
Modifier
.fillMaxWidth(),
selected = selected,
onClick = onClick,
selectedContentColor = Color.Transparent,
unselectedContentColor = ZcashTheme.colors.layoutStroke
) {
Box(
modifier =
Modifier
.fillMaxSize()
.background(
if (selected) Color.Transparent else ZcashTheme.colors.layoutStroke
)
.padding(vertical = ZcashTheme.dimens.spacingMid, horizontal = ZcashTheme.dimens.spacingXtiny),
contentAlignment = Alignment.Center,
) {
Text(
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = ZcashTheme.dimens.spacingXtiny),
text = title,
color = if (selected) ZcashTheme.colors.textCommon else MaterialTheme.colorScheme.onPrimary,
style = ZcashTheme.extendedTypography.restoringTopAppBarStyle,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}

View File

@ -1,16 +1,20 @@
package co.electriccoin.zcash.ui.screen.receive.view package co.electriccoin.zcash.ui.screen.receive.view
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
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.fillMaxHeight 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
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.pager.HorizontalPager
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.Icon import androidx.compose.material3.Icon
@ -19,6 +23,8 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember 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
@ -44,34 +50,17 @@ import co.electriccoin.zcash.ui.common.model.VersionInfo
import co.electriccoin.zcash.ui.common.test.CommonTag import co.electriccoin.zcash.ui.common.test.CommonTag
import co.electriccoin.zcash.ui.design.component.BlankBgScaffold import co.electriccoin.zcash.ui.design.component.BlankBgScaffold
import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator import co.electriccoin.zcash.ui.design.component.CircularScreenProgressIndicator
import co.electriccoin.zcash.ui.design.component.PagerTabs
import co.electriccoin.zcash.ui.design.component.Reference import co.electriccoin.zcash.ui.design.component.Reference
import co.electriccoin.zcash.ui.design.component.SmallTopAppBar import co.electriccoin.zcash.ui.design.component.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.SubHeader
import co.electriccoin.zcash.ui.design.theme.ZcashTheme import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlin.math.roundToInt import kotlin.math.roundToInt
@Preview("Receive")
@Composable
private fun ComposablePreview() {
ZcashTheme(forceDarkMode = false) {
Receive(
screenBrightnessState = ScreenBrightnessState.NORMAL,
walletAddress = runBlocking { WalletAddressesFixture.new() },
snackbarHostState = SnackbarHostState(),
onSettings = {},
onAdjustBrightness = {},
onAddrCopyToClipboard = {},
onQrImageShare = {},
topAppBarSubTitleState = TopAppBarSubTitleState.None,
versionInfo = VersionInfoFixture.new(),
)
}
}
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
fun Receive( fun Receive(
@ -102,18 +91,16 @@ fun Receive(
CircularScreenProgressIndicator() CircularScreenProgressIndicator()
} else { } else {
ReceiveContents( ReceiveContents(
walletAddress = walletAddress, walletAddresses = walletAddress,
onAddressCopyToClipboard = onAddrCopyToClipboard, onAddressCopyToClipboard = onAddrCopyToClipboard,
onQrImageShare = onQrImageShare, onQrImageShare = onQrImageShare,
screenBrightnessState = screenBrightnessState, screenBrightnessState = screenBrightnessState,
versionInfo = versionInfo, versionInfo = versionInfo,
modifier = modifier =
Modifier.padding( Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault, top = paddingValues.calculateTopPadding()
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault, // We intentionally do not set the rest paddings, those are set by the underlying composable
start = ZcashTheme.dimens.screenHorizontalSpacingRegular, ),
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
) )
} }
} }
@ -160,154 +147,173 @@ private fun ReceiveTopAppBar(
) )
} }
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongParameterList") @Suppress("LongParameterList")
@Composable @Composable
private fun ReceiveContents( private fun ReceiveContents(
walletAddress: WalletAddresses, walletAddresses: WalletAddresses,
onAddressCopyToClipboard: (String) -> Unit, onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit, onQrImageShare: (ImageBitmap) -> Unit,
screenBrightnessState: ScreenBrightnessState, screenBrightnessState: ScreenBrightnessState,
versionInfo: VersionInfo, versionInfo: VersionInfo,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( if (screenBrightnessState == ScreenBrightnessState.FULL) {
modifier = BrightenScreen()
Modifier DisableScreenTimeout()
.fillMaxHeight() }
.verticalScroll(rememberScrollState())
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (screenBrightnessState == ScreenBrightnessState.FULL) {
BrightenScreen()
DisableScreenTimeout()
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall)) val state by remember {
derivedStateOf {
Address( listOfNotNull(
walletAddress = walletAddress.unified, walletAddresses.unified,
onAddressCopyToClipboard = onAddressCopyToClipboard, walletAddresses.transparent,
onQrImageShare = onQrImageShare,
)
if (versionInfo.isTestnet) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
Address(
walletAddress = walletAddress.sapling,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
) )
} }
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
Address(
walletAddress = walletAddress.transparent,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
)
} }
}
private val DEFAULT_QR_CODE_SIZE = 320.dp val pagerState = rememberPagerState { state.size }
@Suppress("LongMethod") Column(
@Composable modifier = modifier,
private fun Address( ) {
walletAddress: WalletAddress, Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit, PagerTabs(
modifier: Modifier = Modifier, modifier = Modifier.fillMaxWidth(),
) { pagerState = pagerState,
Column(modifier = modifier) { tabs =
SubHeader( state.map {
text = stringResource(
stringResource( when (it) {
id =
when (walletAddress) {
is WalletAddress.Unified -> R.string.receive_wallet_address_unified is WalletAddress.Unified -> R.string.receive_wallet_address_unified
is WalletAddress.Sapling -> R.string.receive_wallet_address_sapling is WalletAddress.Sapling -> R.string.receive_wallet_address_sapling
is WalletAddress.Transparent -> R.string.receive_wallet_address_transparent is WalletAddress.Transparent -> R.string.receive_wallet_address_transparent
} }
), )
modifier = Modifier.align(Alignment.CenterHorizontally) }.toPersistentList(),
) )
HorizontalPager(
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny)) modifier = Modifier.fillMaxSize(),
state = pagerState,
val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt() userScrollEnabled = false
val qrCodeImage = ) { index ->
remember { AddressPage(
qrCodeForAddress( walletAddresses = walletAddresses,
address = walletAddress.address, modifier = Modifier.fillMaxSize(),
size = sizePixels walletAddress = state[index],
) versionInfo = versionInfo,
} onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
QrCode(
qrCodeImage = qrCodeImage,
onQrImageBitmapShare = onQrImageShare,
contentDescription =
stringResource(
id =
when (walletAddress) {
is WalletAddress.Unified -> R.string.receive_unified_content_description
is WalletAddress.Sapling -> R.string.receive_sapling_content_description
is WalletAddress.Transparent -> R.string.receive_transparent_content_description
}
),
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
Text(
text = walletAddress.address,
style = ZcashTheme.extendedTypography.addressStyle,
color = ZcashTheme.colors.textDescription,
textAlign = TextAlign.Center,
modifier =
Modifier
.align(Alignment.CenterHorizontally)
.clickable { onAddressCopyToClipboard(walletAddress.address) }
.padding(horizontal = ZcashTheme.dimens.spacingLarge)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Reference(
text = stringResource(id = R.string.receive_copy),
onClick = { onAddressCopyToClipboard(walletAddress.address) },
textAlign = TextAlign.Center,
imageVector = ImageVector.vectorResource(R.drawable.copy),
imageContentDescription = null,
modifier =
Modifier
.wrapContentSize()
.padding(all = ZcashTheme.dimens.spacingDefault),
)
Reference(
text = stringResource(id = R.string.receive_share),
onClick = { onQrImageShare(qrCodeImage) },
textAlign = TextAlign.Center,
imageVector = ImageVector.vectorResource(R.drawable.share),
imageContentDescription = null,
modifier =
Modifier
.wrapContentSize()
.padding(all = ZcashTheme.dimens.spacingDefault),
) )
} }
} }
} }
@Suppress("LongMethod", "LongParameterList")
@Composable
private fun AddressPage(
walletAddresses: WalletAddresses,
walletAddress: WalletAddress,
versionInfo: VersionInfo,
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier =
modifier
.verticalScroll(rememberScrollState())
.padding(
horizontal = ZcashTheme.dimens.screenHorizontalSpacingRegular,
vertical = ZcashTheme.dimens.spacingDefault,
),
horizontalAlignment = Alignment.CenterHorizontally
) {
QrCode(walletAddress, onAddressCopyToClipboard, onQrImageShare)
if (versionInfo.isTestnet && walletAddress is WalletAddress.Unified) {
QrCode(walletAddresses.sapling, onAddressCopyToClipboard, onQrImageShare)
}
}
}
@Composable
@Suppress("LongMethod")
private fun ColumnScope.QrCode(
walletAddress: WalletAddress,
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
) {
val sizePixels = with(LocalDensity.current) { DEFAULT_QR_CODE_SIZE.toPx() }.roundToInt()
val qrCodeImage =
remember {
qrCodeForAddress(
address = walletAddress.address,
size = sizePixels
)
}
QrCode(
qrCodeImage = qrCodeImage,
onQrImageBitmapShare = onQrImageShare,
contentDescription =
stringResource(
when (walletAddress) {
is WalletAddress.Unified -> R.string.receive_unified_content_description
is WalletAddress.Sapling -> R.string.receive_sapling_content_description
is WalletAddress.Transparent -> R.string.receive_transparent_content_description
}
),
modifier = Modifier.align(Alignment.CenterHorizontally),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
Text(
text = walletAddress.address,
style = ZcashTheme.extendedTypography.addressStyle,
color = ZcashTheme.colors.textDescription,
textAlign = TextAlign.Center,
modifier =
Modifier
.align(Alignment.CenterHorizontally)
.clickable { onAddressCopyToClipboard(walletAddress.address) }
.padding(horizontal = ZcashTheme.dimens.spacingLarge)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
Reference(
text = stringResource(id = R.string.receive_copy),
onClick = { onAddressCopyToClipboard(walletAddress.address) },
textAlign = TextAlign.Center,
imageVector = ImageVector.vectorResource(R.drawable.copy),
imageContentDescription = null,
modifier =
Modifier
.wrapContentSize()
.padding(all = ZcashTheme.dimens.spacingDefault),
)
Reference(
text = stringResource(id = R.string.receive_share),
onClick = { onQrImageShare(qrCodeImage) },
textAlign = TextAlign.Center,
imageVector = ImageVector.vectorResource(R.drawable.share),
imageContentDescription = null,
modifier =
Modifier
.wrapContentSize()
.padding(all = ZcashTheme.dimens.spacingDefault),
)
}
}
private fun qrCodeForAddress( private fun qrCodeForAddress(
address: String, address: String,
size: Int, size: Int,
@ -339,3 +345,22 @@ private fun QrCode(
.then(modifier) .then(modifier)
) )
} }
@Preview
@Composable
private fun ComposablePreview() =
ZcashTheme(forceDarkMode = false) {
Receive(
screenBrightnessState = ScreenBrightnessState.NORMAL,
walletAddress = runBlocking { WalletAddressesFixture.new() },
snackbarHostState = SnackbarHostState(),
onSettings = {},
onAdjustBrightness = {},
onAddrCopyToClipboard = {},
onQrImageShare = {},
versionInfo = VersionInfoFixture.new(),
topAppBarSubTitleState = TopAppBarSubTitleState.None,
)
}
private val DEFAULT_QR_CODE_SIZE = 320.dp