BIT-1204: Improve avatar color visiblity (#270)

This commit is contained in:
Brian Yencho 2023-11-22 09:40:21 -06:00 committed by Álison Fernandes
parent 57210cefc5
commit 8c0c606d72
6 changed files with 132 additions and 5 deletions

View file

@ -0,0 +1,39 @@
package com.x8bit.bitwarden.ui.platform.base.util
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
/**
* A fractional luminance value beyond which we will consider the associated color to be light
* enough to require a dark overlay to be used.
*/
private const val DARK_OVERLAY_LUMINANCE_THRESHOLD = 0.65f
/**
* Returns `true` if the given [Color] would require a light color to be used in any kind of
* overlay when high contrast is important.
*/
val Color.isLightOverlayRequired: Boolean
get() = this.luminance() < DARK_OVERLAY_LUMINANCE_THRESHOLD
/**
* Returns a [Color] within the current theme that can safely be overlaid on top of the given
* [Color].
*/
@Composable
fun Color.toSafeOverlayColor(): Color {
val surfaceColor = MaterialTheme.colorScheme.surface
val onSurfaceColor = MaterialTheme.colorScheme.onSurface
val lightColor: Color
val darkColor: Color
if (surfaceColor.luminance() > onSurfaceColor.luminance()) {
lightColor = surfaceColor
darkColor = onSurfaceColor
} else {
lightColor = onSurfaceColor
darkColor = surfaceColor
}
return if (this.isLightOverlayRequired) lightColor else darkColor
}

View file

@ -15,6 +15,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.toSafeOverlayColor
import com.x8bit.bitwarden.ui.platform.base.util.toUnscaledTextUnit import com.x8bit.bitwarden.ui.platform.base.util.toUnscaledTextUnit
/** /**
@ -48,7 +49,7 @@ fun BitwardenAccountActionItem(
fontFamily = FontFamily(Font(R.font.sf_pro)), fontFamily = FontFamily(Font(R.font.sf_pro)),
fontWeight = FontWeight.W400, fontWeight = FontWeight.W400,
), ),
color = colorResource(id = R.color.white), color = color.toSafeOverlayColor(),
) )
} }
} }

View file

@ -29,15 +29,14 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.lerp import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import com.x8bit.bitwarden.R import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.platform.base.util.lowercaseWithCurrentLocal import com.x8bit.bitwarden.ui.platform.base.util.lowercaseWithCurrentLocal
import com.x8bit.bitwarden.ui.platform.base.util.toSafeOverlayColor
import com.x8bit.bitwarden.ui.platform.base.util.toUnscaledTextUnit import com.x8bit.bitwarden.ui.platform.base.util.toUnscaledTextUnit
import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary import com.x8bit.bitwarden.ui.platform.components.model.AccountSummary
import com.x8bit.bitwarden.ui.vault.feature.vault.util.iconRes import com.x8bit.bitwarden.ui.vault.feature.vault.util.iconRes
@ -185,7 +184,7 @@ private fun AccountSummaryItem(
Icon( Icon(
painter = painterResource(id = R.drawable.ic_account_initials_container), painter = painterResource(id = R.drawable.ic_account_initials_container),
contentDescription = null, contentDescription = null,
tint = Color(accountSummary.avatarColorHex.toColorInt()), tint = accountSummary.avatarColor,
modifier = Modifier.size(40.dp), modifier = Modifier.size(40.dp),
) )
@ -194,7 +193,7 @@ private fun AccountSummaryItem(
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
// Do not allow scaling // Do not allow scaling
.copy(fontSize = 16.dp.toUnscaledTextUnit()), .copy(fontSize = 16.dp.toUnscaledTextUnit()),
color = MaterialTheme.colorScheme.surface, color = accountSummary.avatarColor.toSafeOverlayColor(),
modifier = Modifier.clearAndSetSemantics { }, modifier = Modifier.clearAndSetSemantics { },
) )
} }

View file

@ -1,6 +1,8 @@
package com.x8bit.bitwarden.ui.platform.components.model package com.x8bit.bitwarden.ui.platform.components.model
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.ui.graphics.Color
import com.x8bit.bitwarden.ui.platform.base.util.hexToColor
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
/** /**
@ -21,6 +23,12 @@ data class AccountSummary(
val status: Status, val status: Status,
) : Parcelable { ) : Parcelable {
/**
* The [avatarColorHex] represented as a [Color].
*/
val avatarColor: Color
get() = avatarColorHex.hexToColor()
/** /**
* Describes the status of the given account. * Describes the status of the given account.
*/ */

View file

@ -0,0 +1,61 @@
package com.x8bit.bitwarden.data.platform.base.util
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.graphics.Color
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
import com.x8bit.bitwarden.ui.platform.base.util.isLightOverlayRequired
import com.x8bit.bitwarden.ui.platform.base.util.toSafeOverlayColor
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ColorExtensionsTest : BaseComposeTest() {
@Suppress("MaxLineLength")
@Test
fun `isLightOverlayRequired for a color with luminance below the light threshold should return true`() {
assertTrue(Color.Blue.isLightOverlayRequired)
}
@Suppress("MaxLineLength")
@Test
fun `isLightOverlayRequired for a color with luminance above the light threshold should return false`() {
assertFalse(Color.Yellow.isLightOverlayRequired)
}
@Test
fun `toSafeOverlayColor for a dark color in light mode should use the surface color`() =
runTestWithTheme(isDarkTheme = false) {
assertEquals(
MaterialTheme.colorScheme.surface,
Color.Blue.toSafeOverlayColor(),
)
}
@Test
fun `toSafeOverlayColor for a dark color in dark mode should use the onSurface color`() =
runTestWithTheme(isDarkTheme = true) {
assertEquals(
MaterialTheme.colorScheme.onSurface,
Color.Blue.toSafeOverlayColor(),
)
}
@Test
fun `toSafeOverlayColor for a light color in light mode should use the onSurface color`() =
runTestWithTheme(isDarkTheme = false) {
assertEquals(
MaterialTheme.colorScheme.onSurface,
Color.Yellow.toSafeOverlayColor(),
)
}
@Test
fun `toSafeOverlayColor for a light color in dark mode should use the surface color`() =
runTestWithTheme(isDarkTheme = true) {
assertEquals(
MaterialTheme.colorScheme.surface,
Color.Yellow.toSafeOverlayColor(),
)
}
}

View file

@ -1,6 +1,8 @@
package com.x8bit.bitwarden.ui.platform.base package com.x8bit.bitwarden.ui.platform.base
import androidx.compose.runtime.Composable
import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.junit4.createComposeRule
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
import dagger.hilt.android.testing.HiltTestApplication import dagger.hilt.android.testing.HiltTestApplication
import org.junit.Rule import org.junit.Rule
import org.junit.runner.RunWith import org.junit.runner.RunWith
@ -24,4 +26,21 @@ abstract class BaseComposeTest {
init { init {
ShadowLog.stream = System.out ShadowLog.stream = System.out
} }
/**
* Helper for testing a basic Composable function that only requires a Composable environment
* with the [BitwardenTheme].
*/
protected fun runTestWithTheme(
isDarkTheme: Boolean,
test: @Composable () -> Unit,
) {
composeTestRule.setContent {
BitwardenTheme(
darkTheme = isDarkTheme,
) {
test()
}
}
}
} }