From 865a6f4fc4e0c861fbbde694fe1c1e3d9079bb22 Mon Sep 17 00:00:00 2001 From: Rafael Date: Fri, 21 Jun 2024 16:57:19 +0600 Subject: [PATCH 1/2] Fix UI bugs in Platform Header --- .../market/platform/MarketPlatformFragment.kt | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/platform/MarketPlatformFragment.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/platform/MarketPlatformFragment.kt index 1b2957d605..b33da1f6a9 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/platform/MarketPlatformFragment.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/market/platform/MarketPlatformFragment.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material.Surface @@ -192,37 +191,34 @@ private fun PlatformScreen( @Composable private fun HeaderContent(title: String, description: String, image: ImageSource) { - Column { - Row( + Row( + modifier = Modifier + .padding(horizontal = 16.dp) + .background(ComposeAppTheme.colors.tyler) + ) { + Column( modifier = Modifier - .height(100.dp) - .padding(horizontal = 16.dp) - .background(ComposeAppTheme.colors.tyler) + .padding(top = 12.dp, bottom = 16.dp) + .weight(1f) ) { - Column( - modifier = Modifier - .padding(top = 12.dp) - .weight(1f) - ) { - title3_leah( - text = title, - ) - subhead2_grey( - text = description, - modifier = Modifier.padding(top = 4.dp), - maxLines = 3, - overflow = TextOverflow.Ellipsis - ) - } - Image( - painter = image.painter(), - contentDescription = null, - modifier = Modifier - .align(Alignment.CenterVertically) - .padding(start = 24.dp) - .size(32.dp), + title3_leah( + text = title, + ) + subhead2_grey( + text = description, + modifier = Modifier.padding(top = 4.dp), + maxLines = 3, + overflow = TextOverflow.Ellipsis ) } + Image( + painter = image.painter(), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterVertically) + .padding(start = 24.dp) + .size(32.dp), + ) } } From 651d87805fd853229336b611d0483e997d5f9b11 Mon Sep 17 00:00:00 2001 From: Rafael Date: Mon, 24 Jun 2024 13:03:10 +0600 Subject: [PATCH 2/2] Use custom ScrollableTabRow for Chart periods --- .../modules/coin/overview/ui/Chart.kt | 31 +- .../modules/multiswap/SuggestionsBar.kt | 2 +- .../restoremnemonic/RestorePhraseScreen.kt | 4 +- .../ui/compose/components/ButtonSecondary.kt | 2 +- .../ui/compose/components/TabButton.kt | 386 ++++++++++++++++-- 5 files changed, 384 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/coin/overview/ui/Chart.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/coin/overview/ui/Chart.kt index 37542aa1a6..db138acc52 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/coin/overview/ui/Chart.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/coin/overview/ui/Chart.kt @@ -24,7 +24,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.CircularProgressIndicator import androidx.compose.material.Icon -import androidx.compose.material.ScrollableTabRow import androidx.compose.material.Tab import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -64,6 +63,7 @@ import io.horizontalsystems.bankwallet.ui.compose.ComposeAppTheme import io.horizontalsystems.bankwallet.ui.compose.components.GraphicBars import io.horizontalsystems.bankwallet.ui.compose.components.GraphicBarsWithNegative import io.horizontalsystems.bankwallet.ui.compose.components.HSpacer +import io.horizontalsystems.bankwallet.ui.compose.components.HsPeriodsScrollableTabRow import io.horizontalsystems.bankwallet.ui.compose.components.RowUniversal import io.horizontalsystems.bankwallet.ui.compose.components.TabButtonSecondaryTransparent import io.horizontalsystems.bankwallet.ui.compose.components.TabItem @@ -702,28 +702,31 @@ fun ChartTab(modifier: Modifier = Modifier, tabItems: List>, onSe val tabIndex = tabItems.indexOfFirst { it.selected } TabPeriod(modifier = modifier) { - ScrollableTabRow( + HsPeriodsScrollableTabRow( selectedTabIndex = tabIndex, modifier = Modifier, backgroundColor = Color.Transparent, - edgePadding = 0.dp, + edgePadding = 12.dp, indicator = {}, divider = {} ) { tabItems.forEachIndexed { index, tabItem -> val selected = tabIndex == index - - Tab( - selected = selected, - onClick = { }, + Row( + modifier = Modifier.padding(horizontal = 4.dp) ) { - TabButtonSecondaryTransparent( - title = tabItem.title, - onSelect = { - onSelect.invoke(tabItem.item) - }, - selected = selected - ) + Tab( + selected = selected, + onClick = { }, + ) { + TabButtonSecondaryTransparent( + title = tabItem.title, + onSelect = { + onSelect.invoke(tabItem.item) + }, + selected = selected + ) + } } } } diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/multiswap/SuggestionsBar.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/multiswap/SuggestionsBar.kt index 277c05dabc..453a49854c 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/multiswap/SuggestionsBar.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/multiswap/SuggestionsBar.kt @@ -38,7 +38,7 @@ fun SuggestionsBar( Text( text = "$percent%", modifier = modifier, - style = ComposeAppTheme.typography.subhead1, + style = ComposeAppTheme.typography.captionSB, color = if (selectEnabled) { ComposeAppTheme.colors.leah } else { diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/modules/restoreaccount/restoremnemonic/RestorePhraseScreen.kt b/app/src/main/java/io/horizontalsystems/bankwallet/modules/restoreaccount/restoremnemonic/RestorePhraseScreen.kt index 63200c3c06..b457000600 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/modules/restoreaccount/restoremnemonic/RestorePhraseScreen.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/modules/restoreaccount/restoremnemonic/RestorePhraseScreen.kt @@ -98,8 +98,8 @@ import io.horizontalsystems.bankwallet.ui.compose.components.SelectorItem import io.horizontalsystems.bankwallet.ui.compose.components.TextImportantWarning import io.horizontalsystems.bankwallet.ui.compose.components.body_grey50 import io.horizontalsystems.bankwallet.ui.compose.components.body_leah +import io.horizontalsystems.bankwallet.ui.compose.components.captionSB_leah import io.horizontalsystems.bankwallet.ui.compose.components.caption_lucian -import io.horizontalsystems.bankwallet.ui.compose.components.subhead1_leah import io.horizontalsystems.bankwallet.ui.compose.observeKeyboardState import io.horizontalsystems.core.helpers.HudHelper import kotlinx.coroutines.CoroutineScope @@ -522,7 +522,7 @@ fun SuggestionsBar( onClick.invoke(wordItem, suggestion) } ) { - subhead1_leah(text = suggestion) + captionSB_leah(text = suggestion) } Spacer(modifier = Modifier.width(12.dp)) } diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/ButtonSecondary.kt b/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/ButtonSecondary.kt index d935651b73..bb391bbacd 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/ButtonSecondary.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/ButtonSecondary.kt @@ -243,7 +243,7 @@ fun ButtonSecondary( enabled = enabled, ) { ProvideTextStyle( - value = ComposeAppTheme.typography.subhead1 + value = ComposeAppTheme.typography.captionSB ) { Row( Modifier diff --git a/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/TabButton.kt b/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/TabButton.kt index 72e09a022c..593a7f5c43 100644 --- a/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/TabButton.kt +++ b/app/src/main/java/io/horizontalsystems/bankwallet/ui/compose/components/TabButton.kt @@ -1,33 +1,68 @@ package io.horizontalsystems.bankwallet.ui.compose.components -import androidx.compose.foundation.layout.* +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material.Button +import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.LocalContentAlpha +import androidx.compose.material.LocalContentColor +import androidx.compose.material.MaterialTheme +import androidx.compose.material.ProvideTextStyle +import androidx.compose.material.ScrollableTabRow +import androidx.compose.material.Surface +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults +import androidx.compose.material.Text +import androidx.compose.material.contentColorFor +import androidx.compose.material.primarySurface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.UiComposable +import androidx.compose.ui.composed +import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastMap import io.horizontalsystems.bankwallet.ui.compose.ComposeAppTheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch -@Composable -fun TabButtonSecondary( - title: String, - onSelect: () -> Unit, - selected: Boolean = false, - enabled: Boolean = true -) { - TabBox( - colors = TabDefaults.textButtonColors( - backgroundColor = ComposeAppTheme.colors.steel20, - ), - content = { Text(title) }, - selected = selected, - enabled = enabled, - onSelect = onSelect - ) -} @Composable fun TabButtonSecondaryTransparent( @@ -58,7 +93,9 @@ fun TabBox( ) { Box { val contentColor by if (enabled) colors.contentColor(selected) else colors.contentColorDisabled() - val backgroundColor by if (enabled) colors.backgroundColor(selected) else colors.backgroundColor(false) + val backgroundColor by if (enabled) colors.backgroundColor(selected) else colors.backgroundColor( + false + ) Surface( onClick = onSelect, color = backgroundColor, @@ -71,7 +108,7 @@ fun TabBox( // style property. CompositionLocalProvider(LocalContentAlpha provides contentColor.alpha) { ProvideTextStyle( - value = ComposeAppTheme.typography.subhead1 + value = ComposeAppTheme.typography.captionSB ) { Row( Modifier @@ -80,7 +117,12 @@ fun TabBox( minHeight = TabDefaults.MinHeight ) .height(TabDefaults.MinHeight) - .padding(contentPadding), + .padding( + PaddingValues( + start = 0.dp, + end = 0.dp, + ) + ), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content @@ -91,6 +133,304 @@ fun TabBox( } } +//Customized version of ScrollableTabRow +//needed to make +@Composable +@UiComposable +fun HsPeriodsScrollableTabRow( + selectedTabIndex: Int, + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colors.primarySurface, + contentColor: Color = contentColorFor(backgroundColor), + edgePadding: Dp = HsTabRowDefaults.ScrollableTabRowPadding, + indicator: @Composable @UiComposable + (tabPositions: List) -> Unit = @Composable { tabPositions -> + HsTabRowDefaults.Indicator( + Modifier.hsTabIndicatorOffset(tabPositions[selectedTabIndex]) + ) + }, + divider: @Composable @UiComposable () -> Unit = + @Composable { + TabRowDefaults.Divider() + }, + tabs: @Composable @UiComposable () -> Unit +) { + Surface( + modifier = modifier, + color = backgroundColor, + contentColor = contentColor + ) { + val scrollState = rememberScrollState() + val coroutineScope = rememberCoroutineScope() + val scrollableTabData = remember(scrollState, coroutineScope) { + HsScrollableTabData( + scrollState = scrollState, + coroutineScope = coroutineScope + ) + } + SubcomposeLayout( + Modifier + .fillMaxWidth() + .wrapContentSize(align = Alignment.CenterStart) + .horizontalScroll(scrollState) + .selectableGroup() + .clipToBounds() + ) { constraints -> + val minTabWidth = 40.dp.roundToPx() + val padding = edgePadding.roundToPx() + val tabConstraints = constraints.copy(minWidth = minTabWidth) + + val tabPlaceables = subcompose(HsTabSlots.Tabs, tabs) + .fastMap { it.measure(tabConstraints) } + + var layoutWidth = padding * 2 + var layoutHeight = 0 + tabPlaceables.fastForEach { + layoutWidth += it.width + layoutHeight = maxOf(layoutHeight, it.height) + } + + // Position the children. + layout(layoutWidth, layoutHeight) { + // Place the tabs + val tabPositions = mutableListOf() + var left = padding + tabPlaceables.fastForEach { + it.placeRelative(left, 0) + tabPositions.add(TabPosition(left = left.toDp(), width = it.width.toDp())) + left += it.width + } + + // The divider is measured with its own height, and width equal to the total width + // of the tab row, and then placed on top of the tabs. + subcompose(HsTabSlots.Divider, divider).fastForEach { + val placeable = it.measure( + constraints.copy( + minHeight = 0, + minWidth = layoutWidth, + maxWidth = layoutWidth + ) + ) + placeable.placeRelative(0, layoutHeight - placeable.height) + } + + // The indicator container is measured to fill the entire space occupied by the tab + // row, and then placed on top of the divider. + subcompose(HsTabSlots.Indicator) { + indicator(tabPositions) + }.fastForEach { + it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0) + } + + scrollableTabData.onLaidOut( + density = this@SubcomposeLayout, + edgeOffset = padding, + tabPositions = tabPositions, + selectedTab = selectedTabIndex + ) + } + } + } +} + +private fun Modifier.hsTabIndicatorOffset( + currentTabPosition: TabPosition +): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "tabIndicatorOffset" + value = currentTabPosition + } +) { + val currentTabWidth by animateDpAsState( + targetValue = currentTabPosition.width, + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) + ) + val indicatorOffset by animateDpAsState( + targetValue = currentTabPosition.left, + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) + ) + fillMaxWidth() + .wrapContentSize(Alignment.BottomStart) + .offset(x = indicatorOffset) + .width(currentTabWidth) +} + +private class HsScrollableTabData( + private val scrollState: ScrollState, + private val coroutineScope: CoroutineScope +) { + private var selectedTab: Int? = null + + fun onLaidOut( + density: Density, + edgeOffset: Int, + tabPositions: List, + selectedTab: Int + ) { + // Animate if the new tab is different from the old tab, or this is called for the first + // time (i.e selectedTab is `null`). + if (this.selectedTab != selectedTab) { + this.selectedTab = selectedTab + tabPositions.getOrNull(selectedTab)?.let { + // Scrolls to the tab with [tabPosition], trying to place it in the center of the + // screen or as close to the center as possible. + val calculatedOffset = it.hsCalculateTabOffset(density, edgeOffset, tabPositions) + if (scrollState.value != calculatedOffset) { + coroutineScope.launch { + scrollState.animateScrollTo( + calculatedOffset, + animationSpec = ScrollableTabRowScrollSpec + ) + } + } + } + } + } + + private fun TabPosition.hsCalculateTabOffset( + density: Density, + edgeOffset: Int, + tabPositions: List + ): Int = with(density) { + val totalTabRowWidth = tabPositions.last().right.roundToPx() + edgeOffset + val visibleWidth = totalTabRowWidth - scrollState.maxValue + val tabOffset = left.roundToPx() + val scrollerCenter = visibleWidth / 2 + val tabWidth = width.roundToPx() + val centeredTabOffset = tabOffset - (scrollerCenter - tabWidth / 2) + // How much space we have to scroll. If the visible width is <= to the total width, then + // we have no space to scroll as everything is always visible. + val availableSpace = (totalTabRowWidth - visibleWidth).coerceAtLeast(0) + return centeredTabOffset.coerceIn(0, availableSpace) + } +} + +@Immutable +class TabPosition internal constructor(val left: Dp, val width: Dp) { + val right: Dp get() = left + width + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TabPosition) return false + + if (left != other.left) return false + if (width != other.width) return false + + return true + } + + override fun hashCode(): Int { + var result = left.hashCode() + result = 31 * result + width.hashCode() + return result + } + + override fun toString(): String { + return "TabPosition(left=$left, right=$right, width=$width)" + } +} + +object HsTabRowDefaults { + /** + * Default [Divider], which will be positioned at the bottom of the [TabRow], underneath the + * indicator. + * + * @param modifier modifier for the divider's layout + * @param thickness thickness of the divider + * @param color color of the divider + */ + @Composable + fun Divider( + modifier: Modifier = Modifier, + thickness: Dp = DividerThickness, + color: Color = LocalContentColor.current.copy(alpha = DividerOpacity) + ) { + androidx.compose.material.Divider(modifier = modifier, thickness = thickness, color = color) + } + + /** + * Default indicator, which will be positioned at the bottom of the [TabRow], on top of the + * divider. + * + * @param modifier modifier for the indicator's layout + * @param height height of the indicator + * @param color color of the indicator + */ + @Composable + fun Indicator( + modifier: Modifier = Modifier, + height: Dp = IndicatorHeight, + color: Color = LocalContentColor.current + ) { + Box( + modifier + .fillMaxWidth() + .height(height) + .background(color = color) + ) + } + + /** + * [Modifier] that takes up all the available width inside the [TabRow], and then animates + * the offset of the indicator it is applied to, depending on the [currentTabPosition]. + * + * @param currentTabPosition [TabPosition] of the currently selected tab. This is used to + * calculate the offset of the indicator this modifier is applied to, as well as its width. + */ + fun Modifier.tabIndicatorOffset( + currentTabPosition: androidx.compose.material.TabPosition + ): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "tabIndicatorOffset" + value = currentTabPosition + } + ) { + val currentTabWidth by animateDpAsState( + targetValue = currentTabPosition.width, + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) + ) + val indicatorOffset by animateDpAsState( + targetValue = currentTabPosition.left, + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) + ) + fillMaxWidth() + .wrapContentSize(Alignment.BottomStart) + .offset(x = indicatorOffset) + .width(currentTabWidth) + } + + /** + * Default opacity for the color of [Divider] + */ + const val DividerOpacity = 0.12f + + /** + * Default thickness for [Divider] + */ + val DividerThickness = 1.dp + + /** + * Default height for [Indicator] + */ + val IndicatorHeight = 2.dp + + /** + * The default padding from the starting edge before a tab in a [ScrollableTabRow]. + */ + val ScrollableTabRowPadding = 52.dp +} + +private val ScrollableTabRowScrollSpec: AnimationSpec = tween( + durationMillis = 250, + easing = FastOutSlowInEasing +) + +private enum class HsTabSlots { + Tabs, + Divider, + Indicator +} + object TabDefaults { private val ButtonHorizontalPadding = 16.dp