Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ fun NavigationBar(
state: NavigationBarState,
onButtonClick: (NavBarButton) -> Unit = {},
hazeState: HazeState? = null,
// The You tab wears the account's own photo in place of its glyph once one is set (node
// 9641:17130). This module sits below the profile layer, so the caller supplies the avatar and
// passes null when there is no photo. The modifier handed back already sizes, clips and fades
// the slot; the avatar only has to fill it.
// The You tab wears the account's own photo in place of its glyph once one is set (nodes
// 9641:17130 and 9713:664). This module sits below the profile layer, so the caller supplies the
// avatar and passes null when there is no photo. The modifier handed back already sizes, clips
// and fades the slot; the avatar only has to fill it.
avatar: (@Composable (Modifier) -> Unit)? = null,
) {
val order = NavBarButton.tabs
Expand Down Expand Up @@ -181,6 +181,18 @@ fun NavigationBar(
) {
Box {
if (button == NavBarButton.TipCard && avatar != null) {
// The photo slot has its own unselected state (node 9713:664): the ring
// thins from 2dp to 1dp and drops to white at 50%, which the slot's
// 0.5 alpha then halves again. The photo itself keeps its size — the
// ring is inset, and the padding around it does not change.
val ringWidth by animateDpAsState(
targetValue = if (selected) {
CodeTheme.dimens.thickBorder
} else {
CodeTheme.dimens.border
},
label = "navBarAvatarRingWidth",
)
Box(
modifier = Modifier
.size(iconSize)
Expand All @@ -189,13 +201,15 @@ fun NavigationBar(
) {
avatar(Modifier.fillMaxSize().clip(CircleShape))
// Drawn over the photo rather than behind it, so the ring survives
// whatever background the avatar paints for itself.
// whatever background the avatar paints for itself. iconAlpha is
// the ring's own fade, on top of the slot's — the two compose to
// the 25% the unselected ring reads at.
Box(
modifier = Modifier
.fillMaxSize()
.border(
CodeTheme.dimens.thickBorder,
Color.White,
ringWidth,
Color.White.copy(alpha = iconAlpha),
CircleShape,
),
)
Expand Down Expand Up @@ -262,3 +276,29 @@ private fun NavigationBarPreview() {
onButtonClick = { }
)
}

/**
* The You tab's photo slot in both of its states (node 9713:664) — a flat fill stands in for the
* photo, since a preview has no profile to read.
*/
@Preview(name = "Avatar, You tab unselected")
@PreviewWrapper(FlipcashThemeWrapper::class)
@Composable
private fun NavigationBarAvatarUnselectedPreview() {
NavigationBar(
state = rememberNavigationBarState(selectedTab = NavBarButton.Wallet),
onButtonClick = { },
avatar = { modifier -> Box(modifier.background(Color(0xFF8E6E5B))) },
)
}

@Preview(name = "Avatar, You tab selected")
@PreviewWrapper(FlipcashThemeWrapper::class)
@Composable
private fun NavigationBarAvatarSelectedPreview() {
NavigationBar(
state = rememberNavigationBarState(selectedTab = NavBarButton.TipCard),
onButtonClick = { },
avatar = { modifier -> Box(modifier.background(Color(0xFF8E6E5B))) },
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.flipcash.app.core.ui

import android.graphics.Bitmap
import android.graphics.Canvas
import android.view.View
import androidx.activity.ComponentActivity
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.unit.dp
import com.flipcash.app.core.navigation.NavBarButton
import com.flipcash.app.theme.FlipcashPreview
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
import java.io.File

/**
* Renders the You tab's photo slot in both selection states to a PNG so the ring can be eyeballed
* against node 9713:664 without an emulator. Not an assertion test — it captures to
* `build/screenshots/`, and scales the capture up so a 1dp/2dp ring difference is visible.
*
* Frames are pumped on a paused clock rather than waiting for idle, like
* [TokenCardWatermarkScreenshotTest]: the bar's selection animations keep scheduling frames.
*
* Left at the default mdpi, so a dp is a pixel and the ring measures against the design's px
* values directly.
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(sdk = [34])
class NavigationBarAvatarScreenshotTest {

@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()

@Test
fun rendersYouTabPhotoStates() {
val photo: @Composable (Modifier) -> Unit = { modifier ->
Box(modifier.background(Color(0xFF8E6E5B)))
}
composeRule.mainClock.autoAdvance = false
composeRule.setContent {
FlipcashPreview(showBackground = true) {
Column(
modifier = Modifier
.width(360.dp)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// You unselected, with a photo — the state this test exists for.
NavigationBar(
state = rememberNavigationBarState(selectedTab = NavBarButton.Wallet),
avatar = photo,
)
// You selected, with a photo.
NavigationBar(
state = rememberNavigationBarState(selectedTab = NavBarButton.TipCard),
avatar = photo,
)
// No photo — the glyph, for contrast.
NavigationBar(
state = rememberNavigationBarState(selectedTab = NavBarButton.Wallet),
)
}
}
}
repeat(20) { composeRule.mainClock.advanceTimeByFrame() }

val root: View = composeRule.activity.findViewById(android.R.id.content)
val width = root.width.takeIf { it > 0 } ?: 1080
val height = root.height.takeIf { it > 0 } ?: 1920
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
root.draw(Canvas(bitmap))
// Nearest-neighbour upscale: ring widths are 1-2px at this density, and smoothing them
// would hide exactly what the capture is for.
val scaled = Bitmap.createScaledBitmap(bitmap, width * 3, height * 3, false)

val outDir = File("build/screenshots").apply { mkdirs() }
val file = File(outDir, "nav_bar_you_photo_states.png")
file.outputStream().use { scaled.compress(Bitmap.CompressFormat.PNG, 100, it) }
println("SCREENSHOT_WRITTEN: ${file.absolutePath} (${width}x$height)")
}
}
Loading