diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt new file mode 100644 index 0000000000..7510626bb7 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/ModifierExtensions.kt @@ -0,0 +1,33 @@ +package com.x8bit.bitwarden.ui.platform.base.util + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage + +/** + * Adds a performance-optimized background color specified by the given [topAppBarScrollBehavior] + * and its current scroll state. + */ +@OmitFromCoverage +@OptIn(ExperimentalMaterial3Api::class) +@Stable +@Composable +fun Modifier.scrolledContainerBackground( + topAppBarScrollBehavior: TopAppBarScrollBehavior, +): Modifier { + val expandedColor = MaterialTheme.colorScheme.surface + val collapsedColor = MaterialTheme.colorScheme.surfaceContainer + return this then drawBehind { + drawRect( + color = topAppBarScrollBehavior.toScrolledContainerColor( + expandedColor = expandedColor, + collapsedColor = collapsedColor, + ), + ) + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/TopAppBarScrollBehaviorExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/TopAppBarScrollBehaviorExtensions.kt new file mode 100644 index 0000000000..5b3d53c1f4 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/base/util/TopAppBarScrollBehaviorExtensions.kt @@ -0,0 +1,27 @@ +package com.x8bit.bitwarden.ui.platform.base.util + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp + +/** + * Returns the correct color for a scrolled container based on the given [TopAppBarScrollBehavior] + * and target [expandedColor] / [collapsedColor]. + */ +@OptIn(ExperimentalMaterial3Api::class) +fun TopAppBarScrollBehavior.toScrolledContainerColor( + expandedColor: Color, + collapsedColor: Color, +): Color { + val progressFraction = if (this.isPinned) { + this.state.overlappedFraction + } else { + this.state.collapsedFraction + } + return lerp( + start = expandedColor, + stop = collapsedColor, + fraction = progressFraction, + ) +} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt index 5a431e6e15..ec1b3ce51f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/components/BitwardenAccountSwitcher.kt @@ -35,14 +35,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.lerp import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.dp import com.x8bit.bitwarden.R import com.x8bit.bitwarden.ui.platform.base.util.lowercaseWithCurrentLocal +import com.x8bit.bitwarden.ui.platform.base.util.scrolledContainerBackground import com.x8bit.bitwarden.ui.platform.base.util.toSafeOverlayColor import com.x8bit.bitwarden.ui.platform.base.util.toUnscaledTextUnit import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary @@ -185,8 +184,6 @@ private fun AnimatedAccountSwitcher( topAppBarScrollBehavior: TopAppBarScrollBehavior, currentAnimationState: (isVisible: Boolean) -> Unit, ) { - val expandedColor = MaterialTheme.colorScheme.surface - val collapsedColor = MaterialTheme.colorScheme.surfaceContainer val transition = updateTransition( targetState = isVisible, label = "AnimatedAccountSwitcher", @@ -203,20 +200,7 @@ private fun AnimatedAccountSwitcher( // bottom padding. .padding(bottom = 24.dp) // Match the color of the switcher the different states of the app bar. - .drawBehind { - val progressFraction = if (topAppBarScrollBehavior.isPinned) { - topAppBarScrollBehavior.state.overlappedFraction - } else { - topAppBarScrollBehavior.state.collapsedFraction - } - val contentBackgroundColor = - lerp( - start = expandedColor, - stop = collapsedColor, - fraction = progressFraction, - ) - drawRect(contentBackgroundColor) - }, + .scrolledContainerBackground(topAppBarScrollBehavior), ) { items(accountSummaries) { accountSummary -> AccountSummaryItem( diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/util/TopAppBarScrollBehaviorExtensionsTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/util/TopAppBarScrollBehaviorExtensionsTest.kt new file mode 100644 index 0000000000..a86508893b --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/base/util/TopAppBarScrollBehaviorExtensionsTest.kt @@ -0,0 +1,92 @@ +package com.x8bit.bitwarden.ui.platform.base.util + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.ui.graphics.Color +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.jupiter.api.Assertions.assertEquals + +@OptIn(ExperimentalMaterial3Api::class) +class TopAppBarScrollBehaviorExtensionsTest { + @Suppress("MaxLineLength") + @Test + fun `toScrolledContainerColor for pinned states should interpolate based on the overlappedFraction`() { + val expandedColor = Color( + red = 0f, + green = 0f, + blue = 0f, + alpha = 0f, + ) + val collapsedColor = Color( + red = 1f, + green = 1f, + blue = 1f, + alpha = 1f, + ) + var overlappedFraction = 0f + val topAppBarScrollBehavior = mockk { + every { isPinned } returns true + every { state.overlappedFraction } answers { overlappedFraction } + } + + overlappedFraction = 0f + assertEquals( + expandedColor, + topAppBarScrollBehavior.toScrolledContainerColor( + expandedColor = expandedColor, + collapsedColor = collapsedColor, + ), + ) + + overlappedFraction = 1f + assertEquals( + collapsedColor, + topAppBarScrollBehavior.toScrolledContainerColor( + expandedColor = expandedColor, + collapsedColor = collapsedColor, + ), + ) + } + + @Suppress("MaxLineLength") + @Test + fun `toScrolledContainerColor for pinned states should interpolate based on the collapsedFraction`() { + val expandedColor = Color( + red = 0f, + green = 0f, + blue = 0f, + alpha = 0f, + ) + val collapsedColor = Color( + red = 1f, + green = 1f, + blue = 1f, + alpha = 1f, + ) + var collapsedFraction = 0f + val topAppBarScrollBehavior = mockk { + every { isPinned } returns false + every { state.collapsedFraction } answers { collapsedFraction } + } + + collapsedFraction = 0f + assertEquals( + expandedColor, + topAppBarScrollBehavior.toScrolledContainerColor( + expandedColor = expandedColor, + collapsedColor = collapsedColor, + ), + ) + + collapsedFraction = 1f + assertEquals( + collapsedColor, + topAppBarScrollBehavior.toScrolledContainerColor( + expandedColor = expandedColor, + collapsedColor = collapsedColor, + ), + ) + } +}