mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-03 06:55:41 +00:00
Iterate on unread counts again (#7869)
* Fix timestamp color to match the designs. * Don't limit the call and unread row height to 16dp: the font size is not fixed and it might display components in a broken state * Ensure the alignment works * Remove `SemanticColors.unreadIndicator` and use `.iconAccentPrimary` and `.textActionAccent` instead * Simplify and document `UnreadIndicatorAtom` * Use `fontBodySmMedium` for the font size in the unread indicator * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
367cea0213
commit
1564de9b6e
+48
-32
@@ -8,26 +8,32 @@
|
||||
|
||||
package io.element.android.libraries.designsystem.atomic.atoms
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.plus
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.coerceAtLeast
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.SemanticColors
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
|
||||
private const val MAX_COUNT = 99
|
||||
@@ -54,7 +60,7 @@ fun CounterAtom(
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color? = null,
|
||||
contentColor: Color? = null,
|
||||
contentPadding: PaddingValues = PaddingValues.Zero,
|
||||
contentPadding: PaddingValues = PaddingValues(vertical = 0.5.dp),
|
||||
textStyle: TextStyle = CounterAtomDefaults.textStyle,
|
||||
dotSize: Dp = 12.0.dp,
|
||||
isCritical: Boolean = false,
|
||||
@@ -65,40 +71,50 @@ fun CounterAtom(
|
||||
else -> MAX_COUNT_STRING
|
||||
}
|
||||
|
||||
val minSingleDigitSize = if (countAsText?.length == 1) {
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
// Measure the maximum count string size
|
||||
val textLayoutResult = textMeasurer.measure(
|
||||
text = countAsText,
|
||||
val density = LocalDensity.current
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
val minTextSize = remember(density.density, textStyle) {
|
||||
val result = textMeasurer.measure(
|
||||
text = MAX_COUNT_STRING,
|
||||
style = textStyle
|
||||
)
|
||||
val textSize = textLayoutResult.size
|
||||
maxOf(textSize.width, textSize.height)
|
||||
with(density) {
|
||||
minOf(result.size.width, result.size.height).toDp()
|
||||
}
|
||||
}
|
||||
val sizeModifier = if (countAsText?.length == 1) {
|
||||
Modifier.size(minTextSize)
|
||||
} else {
|
||||
0
|
||||
Modifier.sizeIn(minHeight = minTextSize)
|
||||
}
|
||||
|
||||
androidx.compose.material3.Badge(
|
||||
modifier = modifier.then(
|
||||
if (countAsText == null) Modifier.sizeIn(minWidth = dotSize, minHeight = dotSize) else Modifier
|
||||
),
|
||||
containerColor = containerColor ?: if (isCritical) ElementTheme.colors.iconCriticalPrimary else ElementTheme.colors.iconAccentPrimary,
|
||||
contentColor = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||
content = countAsText?.let {
|
||||
@Composable {
|
||||
Text(
|
||||
modifier = Modifier.sizeIn(
|
||||
minWidth = (minSingleDigitSize.toDp() - 8.dp).coerceAtLeast(0.dp),
|
||||
minHeight = minSingleDigitSize.toDp()
|
||||
).padding(contentPadding),
|
||||
text = countAsText,
|
||||
style = textStyle,
|
||||
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
val extraPadding = if (countAsText?.length == 1) {
|
||||
contentPadding
|
||||
} else {
|
||||
contentPadding + PaddingValues(horizontal = 4.dp)
|
||||
}
|
||||
val backgroundColor = containerColor ?: if (isCritical) ElementTheme.colors.iconCriticalPrimary else ElementTheme.colors.iconAccentPrimary
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(100))
|
||||
.background(backgroundColor)
|
||||
.then(
|
||||
if (countAsText == null) Modifier.size(dotSize) else Modifier
|
||||
)
|
||||
) {
|
||||
countAsText?.let {
|
||||
Text(
|
||||
modifier = sizeModifier
|
||||
.align(Alignment.Center)
|
||||
.padding(extraPadding),
|
||||
text = countAsText,
|
||||
style = textStyle,
|
||||
color = contentColor ?: ElementTheme.colors.textOnSolidPrimary,
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object CounterAtomDefaults {
|
||||
@@ -112,7 +128,7 @@ internal fun CounterAtomPreview() = ElementPreview {
|
||||
Row(horizontalArrangement = spacedBy(2.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
CounterAtom(count = 0)
|
||||
CounterAtom(count = 4)
|
||||
CounterAtom(count = 99)
|
||||
CounterAtom(count = 10)
|
||||
CounterAtom(count = 100)
|
||||
CounterAtom(count = 4, isCritical = true)
|
||||
}
|
||||
|
||||
+29
-23
@@ -13,7 +13,6 @@ import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -27,35 +26,42 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
||||
|
||||
/**
|
||||
* A small indicator to show the number of unread messages or notifications.
|
||||
*
|
||||
* @param count The number of unread messages or notifications. If null, the indicator will be hidden, if 0, a notification dot will be shown.
|
||||
* @param modifier The modifier to be applied to the indicator.
|
||||
* @param dotIndicatorSize The size of the dot indicator.
|
||||
* @param color The background color of the indicator.
|
||||
* @param contentDescription The content description for accessibility.
|
||||
* @param border The border to be applied to the indicator.
|
||||
* @param contentPadding The padding inside the indicator.
|
||||
*/
|
||||
@Composable
|
||||
fun UnreadIndicatorAtom(
|
||||
count: Long?,
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = 12.dp,
|
||||
count: Long? = null,
|
||||
color: Color = ElementTheme.colors.unreadIndicator,
|
||||
isVisible: Boolean = true,
|
||||
dotIndicatorSize: Dp = 12.dp,
|
||||
color: Color = ElementTheme.colors.iconAccentPrimary,
|
||||
contentDescription: String? = null,
|
||||
border: BorderStroke? = null,
|
||||
contentPadding: PaddingValues = PaddingValues.Zero,
|
||||
) {
|
||||
when {
|
||||
!isVisible -> Spacer(modifier = modifier.size(size))
|
||||
else -> count?.let {
|
||||
CounterAtom(
|
||||
count = count.toInt(),
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
}
|
||||
.then(if (border != null) Modifier.border(border, RoundedCornerShape(percent = 50)) else Modifier),
|
||||
containerColor = color,
|
||||
contentColor = ElementTheme.colors.bgCanvasDefault,
|
||||
textStyle = ElementTheme.typography.fontBodySmMedium,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
count?.let {
|
||||
CounterAtom(
|
||||
count = count.toInt(),
|
||||
modifier = modifier
|
||||
.semantics {
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
}
|
||||
.then(if (count == 0L) Modifier.size(dotIndicatorSize) else Modifier)
|
||||
.then(if (border != null) Modifier.border(border, RoundedCornerShape(percent = 100)) else Modifier),
|
||||
containerColor = color,
|
||||
contentColor = ElementTheme.colors.bgCanvasDefault,
|
||||
textStyle = ElementTheme.typography.fontBodySmMedium,
|
||||
contentPadding = contentPadding,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +72,7 @@ internal fun UnreadIndicatorAtomPreview() = ElementPreview {
|
||||
UnreadIndicatorAtom(count = null)
|
||||
UnreadIndicatorAtom(count = 0)
|
||||
UnreadIndicatorAtom(count = 1)
|
||||
UnreadIndicatorAtom(count = 10)
|
||||
UnreadIndicatorAtom(count = 20)
|
||||
UnreadIndicatorAtom(count = 99)
|
||||
UnreadIndicatorAtom(count = 999)
|
||||
}
|
||||
|
||||
-4
@@ -32,9 +32,6 @@ val SemanticColors.roomListRoomMessage
|
||||
val SemanticColors.roomListRoomMessageDate
|
||||
get() = textSecondary
|
||||
|
||||
val SemanticColors.unreadIndicator
|
||||
get() = iconAccentTertiary
|
||||
|
||||
val SemanticColors.placeholderBackground
|
||||
get() = bgSubtleSecondary
|
||||
|
||||
@@ -83,7 +80,6 @@ internal fun ColorAliasesPreview() = ElementPreview {
|
||||
"roomListRoomName" to ElementTheme.colors.roomListRoomName,
|
||||
"roomListRoomMessage" to ElementTheme.colors.roomListRoomMessage,
|
||||
"roomListRoomMessageDate" to ElementTheme.colors.roomListRoomMessageDate,
|
||||
"unreadIndicator" to ElementTheme.colors.unreadIndicator,
|
||||
"placeholderBackground" to ElementTheme.colors.placeholderBackground,
|
||||
"messageFromMeBackground" to ElementTheme.colors.messageFromMeBackground,
|
||||
"messageFromOtherBackground" to ElementTheme.colors.messageFromOtherBackground,
|
||||
|
||||
+3
-5
@@ -36,7 +36,7 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.CounterAtom
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.tooltip.PlainTooltip
|
||||
import io.element.android.libraries.designsystem.components.tooltip.TooltipBox
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
@@ -140,12 +140,10 @@ fun HorizontalFloatingToolbarItem(
|
||||
BadgedBox(
|
||||
badge = {
|
||||
counter?.let {
|
||||
CounterAtom(
|
||||
count = it,
|
||||
UnreadIndicatorAtom(
|
||||
count = it.toLong(),
|
||||
// Tweak the offset so it's centered on the icon
|
||||
modifier = Modifier.offset(x = (-16).dp, y = 10.dp),
|
||||
contentPadding = PaddingValues(vertical = 0.5.dp),
|
||||
textStyle = ElementTheme.typography.fontBodySmMedium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user