11package to.bitkit.ui.components
22
33import androidx.compose.animation.AnimatedContent
4+ import androidx.compose.animation.EnterExitState
5+ import androidx.compose.animation.SizeTransform
6+ import androidx.compose.animation.core.animateFloat
47import androidx.compose.foundation.layout.Arrangement
58import androidx.compose.foundation.layout.Column
69import androidx.compose.foundation.layout.Row
@@ -10,9 +13,13 @@ import androidx.compose.foundation.layout.padding
1013import androidx.compose.foundation.layout.size
1114import androidx.compose.material3.Icon
1215import androidx.compose.runtime.Composable
16+ import androidx.compose.runtime.Immutable
1317import androidx.compose.runtime.getValue
18+ import androidx.compose.runtime.remember
1419import androidx.compose.ui.Alignment
1520import androidx.compose.ui.Modifier
21+ import androidx.compose.ui.graphics.TransformOrigin
22+ import androidx.compose.ui.graphics.graphicsLayer
1623import androidx.compose.ui.platform.LocalInspectionMode
1724import androidx.compose.ui.platform.testTag
1825import androidx.compose.ui.res.painterResource
@@ -53,7 +60,7 @@ fun BalanceHeaderView(
5360
5461 if (isPreview) {
5562 BalanceHeader (
56- modifier = modifier ,
63+ isBitcoinPrimary = true ,
5764 smallRowSymbol = " $" ,
5865 smallRowText = " 12.34" ,
5966 largeRowPrefix = prefix,
@@ -66,6 +73,7 @@ fun BalanceHeaderView(
6673 onClick = {},
6774 onToggleHideBalance = {},
6875 testTag = testTag,
76+ modifier = modifier,
6977 )
7078 return
7179 }
@@ -88,7 +96,7 @@ fun BalanceHeaderView(
8896 val isBitcoinPrimary = primaryDisplay == PrimaryDisplay .BITCOIN
8997
9098 BalanceHeader (
91- modifier = modifier ,
99+ isBitcoinPrimary = isBitcoinPrimary ,
92100 smallRowSymbol = if (isBitcoinPrimary) fiat.symbol else btc.symbol,
93101 smallRowText = if (isBitcoinPrimary) fiat.formatted else btc.value,
94102 smallRowIsSymbolSuffix = if (isBitcoinPrimary) fiat.isSymbolSuffix else false ,
@@ -105,13 +113,14 @@ fun BalanceHeaderView(
105113 onClick = onClick ? : { currency.switchUnit() },
106114 onToggleHideBalance = { settings.setHideBalance(! hideBalance) },
107115 testTag = testTag,
116+ modifier = modifier,
108117 )
109118 }
110119}
111120
112121@Composable
113122fun BalanceHeader (
114- modifier : Modifier = Modifier ,
123+ isBitcoinPrimary : Boolean ,
115124 smallRowSymbol : String? = null,
116125 smallRowText : String ,
117126 smallRowIsSymbolSuffix : Boolean = false,
@@ -128,7 +137,24 @@ fun BalanceHeader(
128137 onClick : () -> Unit ,
129138 onToggleHideBalance : () -> Unit = {},
130139 testTag : String? = null,
140+ modifier : Modifier = Modifier ,
131141) {
142+ val smallRowState = remember(
143+ isBitcoinPrimary, smallRowSymbol, smallRowText, smallRowIsSymbolSuffix,
144+ ) {
145+ SmallRowState (isBitcoinPrimary, smallRowSymbol, smallRowText, smallRowIsSymbolSuffix)
146+ }
147+
148+ val largeRowState = remember(
149+ isBitcoinPrimary, largeRowPrefix, largeRowText, largeRowSymbol,
150+ largeRowIsSymbolSuffix, showSymbol,
151+ ) {
152+ LargeRowState (
153+ isBitcoinPrimary, largeRowPrefix, largeRowText, largeRowSymbol,
154+ largeRowIsSymbolSuffix, showSymbol,
155+ )
156+ }
157+
132158 Column (
133159 verticalArrangement = Arrangement .Center ,
134160 horizontalAlignment = Alignment .Start ,
@@ -140,28 +166,62 @@ fun BalanceHeader(
140166 .clickableAlpha { onClick() }
141167 .then(testTag?.let { Modifier .testTag(it) } ? : Modifier )
142168 ) {
143- SmallRow (
144- symbol = smallRowSymbol,
145- text = smallRowText,
146- isSymbolSuffix = smallRowIsSymbolSuffix,
147- hideBalance = hideBalance,
148- modifier = smallRowModifier,
149- )
169+ AnimatedContent (
170+ targetState = smallRowState,
171+ transitionSpec = {
172+ BalanceAnimations .swapSmallRowTransition using
173+ SizeTransform (clip = false )
174+ },
175+ contentKey = { it.isBitcoinPrimary },
176+ label = " smallRowSwapAnimation" ,
177+ ) { state ->
178+ val scale by transition.animateFloat(label = " smallRowScale" ) {
179+ if (it == EnterExitState .Visible ) 1f else SMALL_ROW_SWAP_SCALE
180+ }
181+ SmallRow (
182+ symbol = state.symbol,
183+ text = state.text,
184+ isSymbolSuffix = state.isSymbolSuffix,
185+ hideBalance = hideBalance,
186+ modifier = smallRowModifier.graphicsLayer {
187+ scaleX = scale
188+ scaleY = scale
189+ transformOrigin = TransformOrigin (0f , 0f )
190+ },
191+ )
192+ }
150193
151194 VerticalSpacer (12 .dp)
152195
153196 Row (
154197 verticalAlignment = Alignment .CenterVertically ,
155198 ) {
156- LargeRow (
157- prefix = largeRowPrefix,
158- text = largeRowText,
159- symbol = largeRowSymbol,
160- showSymbol = showSymbol,
161- isSymbolSuffix = largeRowIsSymbolSuffix,
162- hideBalance = hideBalance,
163- modifier = largeRowModifier,
164- )
199+ AnimatedContent (
200+ targetState = largeRowState,
201+ transitionSpec = {
202+ BalanceAnimations .swapLargeRowTransition using
203+ SizeTransform (clip = false )
204+ },
205+ contentKey = { it.isBitcoinPrimary },
206+ label = " largeRowSwapAnimation" ,
207+ ) { state ->
208+ val scale by transition.animateFloat(label = " largeRowScale" ) {
209+ if (it == EnterExitState .Visible ) 1f else LARGE_ROW_SWAP_SCALE
210+ }
211+ LargeRow (
212+ prefix = state.prefix,
213+ text = state.text,
214+ symbol = state.symbol,
215+ showSymbol = state.showSymbol,
216+ isSymbolSuffix = state.isSymbolSuffix,
217+ hideBalance = hideBalance,
218+ modifier = largeRowModifier.graphicsLayer {
219+ scaleX = scale
220+ scaleY = scale
221+ transformOrigin = TransformOrigin (0f , 0f )
222+ },
223+ )
224+ }
165225
166226 if (showEyeIcon) {
167227 Spacer (modifier = Modifier .weight(1f ))
@@ -188,6 +248,28 @@ fun BalanceHeader(
188248 }
189249}
190250
251+ // Matches iOS: .scale(scale: 1.5) for small row, .scale(scale: 0.5) for large row
252+ private const val SMALL_ROW_SWAP_SCALE = 1.5f
253+ private const val LARGE_ROW_SWAP_SCALE = 0.5f
254+
255+ @Immutable
256+ private data class SmallRowState (
257+ val isBitcoinPrimary : Boolean ,
258+ val symbol : String? ,
259+ val text : String ,
260+ val isSymbolSuffix : Boolean ,
261+ )
262+
263+ @Immutable
264+ private data class LargeRowState (
265+ val isBitcoinPrimary : Boolean ,
266+ val prefix : String? ,
267+ val text : String ,
268+ val symbol : String ,
269+ val isSymbolSuffix : Boolean ,
270+ val showSymbol : Boolean ,
271+ )
272+
191273@Composable
192274fun LargeRow (
193275 prefix : String? ,
@@ -295,14 +377,15 @@ private fun SmallRow(
295377private fun Preview () {
296378 AppThemeSurface {
297379 BalanceHeader (
380+ isBitcoinPrimary = true ,
298381 smallRowSymbol = " $" ,
299382 smallRowText = " 27.36" ,
300383 largeRowPrefix = " +" ,
301384 largeRowText = " 136 825" ,
302385 largeRowSymbol = " ₿" ,
303386 showSymbol = true ,
387+ onClick = {},
304388 modifier = Modifier .fillMaxWidth(),
305- onClick = {}
306389 )
307390 }
308391}
@@ -312,6 +395,7 @@ private fun Preview() {
312395private fun PreviewHidden () {
313396 AppThemeSurface {
314397 BalanceHeader (
398+ isBitcoinPrimary = true ,
315399 smallRowSymbol = " $" ,
316400 smallRowText = " 27.36" ,
317401 largeRowPrefix = " +" ,
@@ -320,9 +404,9 @@ private fun PreviewHidden() {
320404 showSymbol = true ,
321405 hideBalance = true ,
322406 isSwipeToHideEnabled = true ,
323- modifier = Modifier .fillMaxWidth(),
324407 onClick = {},
325- onToggleHideBalance = {}
408+ onToggleHideBalance = {},
409+ modifier = Modifier .fillMaxWidth(),
326410 )
327411 }
328412}
0 commit comments