[#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
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
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.height
import androidx.compose.foundation.layout.padding
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.verticalScroll
import androidx.compose.material3.Icon
@ -19,6 +23,8 @@ import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
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.design.component.BlankBgScaffold
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.SmallTopAppBar
import co.electriccoin.zcash.ui.design.component.SubHeader
import co.electriccoin.zcash.ui.design.theme.ZcashTheme
import co.electriccoin.zcash.ui.fixture.VersionInfoFixture
import co.electriccoin.zcash.ui.screen.receive.util.AndroidQrCodeImageGenerator
import co.electriccoin.zcash.ui.screen.receive.util.JvmQrCodeGenerator
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.runBlocking
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")
@Composable
fun Receive(
@ -102,18 +91,16 @@ fun Receive(
CircularScreenProgressIndicator()
} else {
ReceiveContents(
walletAddress = walletAddress,
walletAddresses = walletAddress,
onAddressCopyToClipboard = onAddrCopyToClipboard,
onQrImageShare = onQrImageShare,
screenBrightnessState = screenBrightnessState,
versionInfo = versionInfo,
modifier =
Modifier.padding(
top = paddingValues.calculateTopPadding() + ZcashTheme.dimens.spacingDefault,
bottom = paddingValues.calculateBottomPadding() + ZcashTheme.dimens.spacingDefault,
start = ZcashTheme.dimens.screenHorizontalSpacingRegular,
end = ZcashTheme.dimens.screenHorizontalSpacingRegular
)
top = paddingValues.calculateTopPadding()
// We intentionally do not set the rest paddings, those are set by the underlying composable
),
)
}
}
@ -160,154 +147,173 @@ private fun ReceiveTopAppBar(
)
}
@OptIn(ExperimentalFoundationApi::class)
@Suppress("LongParameterList")
@Composable
private fun ReceiveContents(
walletAddress: WalletAddresses,
walletAddresses: WalletAddresses,
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
screenBrightnessState: ScreenBrightnessState,
versionInfo: VersionInfo,
modifier: Modifier = Modifier,
) {
Column(
modifier =
Modifier
.fillMaxHeight()
.verticalScroll(rememberScrollState())
.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (screenBrightnessState == ScreenBrightnessState.FULL) {
BrightenScreen()
DisableScreenTimeout()
}
if (screenBrightnessState == ScreenBrightnessState.FULL) {
BrightenScreen()
DisableScreenTimeout()
}
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingSmall))
Address(
walletAddress = walletAddress.unified,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
)
if (versionInfo.isTestnet) {
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingHuge))
Address(
walletAddress = walletAddress.sapling,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
val state by remember {
derivedStateOf {
listOfNotNull(
walletAddresses.unified,
walletAddresses.transparent,
)
}
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")
@Composable
private fun Address(
walletAddress: WalletAddress,
onAddressCopyToClipboard: (String) -> Unit,
onQrImageShare: (ImageBitmap) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
SubHeader(
text =
stringResource(
id =
when (walletAddress) {
Column(
modifier = modifier,
) {
Spacer(Modifier.height(ZcashTheme.dimens.spacingDefault))
PagerTabs(
modifier = Modifier.fillMaxWidth(),
pagerState = pagerState,
tabs =
state.map {
stringResource(
when (it) {
is WalletAddress.Unified -> R.string.receive_wallet_address_unified
is WalletAddress.Sapling -> R.string.receive_wallet_address_sapling
is WalletAddress.Transparent -> R.string.receive_wallet_address_transparent
}
),
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}.toPersistentList(),
)
Spacer(modifier = Modifier.height(ZcashTheme.dimens.spacingTiny))
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(
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),
HorizontalPager(
modifier = Modifier.fillMaxSize(),
state = pagerState,
userScrollEnabled = false
) { index ->
AddressPage(
walletAddresses = walletAddresses,
modifier = Modifier.fillMaxSize(),
walletAddress = state[index],
versionInfo = versionInfo,
onAddressCopyToClipboard = onAddressCopyToClipboard,
onQrImageShare = onQrImageShare,
)
}
}
}
@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(
address: String,
size: Int,
@ -339,3 +345,22 @@ private fun QrCode(
.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