From 99031589cc125c78fd0728d6039da9584155a738 Mon Sep 17 00:00:00 2001 From: y9san9 / Alex Sokol Date: Tue, 3 Feb 2026 14:20:13 +0300 Subject: [PATCH] refactor(interests): make interests closer to m3 --- .../java/friendly/android/ColorFromString.kt | 101 ++++++------------ .../main/java/friendly/android/FeedCard.kt | 41 +++---- .../java/friendly/android/ProfileScreen.kt | 51 +++++---- .../main/java/friendly/android/UserAvatar.kt | 5 +- 4 files changed, 90 insertions(+), 108 deletions(-) diff --git a/app/src/main/java/friendly/android/ColorFromString.kt b/app/src/main/java/friendly/android/ColorFromString.kt index 6389f8d..e89ddb3 100644 --- a/app/src/main/java/friendly/android/ColorFromString.kt +++ b/app/src/main/java/friendly/android/ColorFromString.kt @@ -2,82 +2,45 @@ package friendly.android import androidx.compose.ui.graphics.Color -private const val DJB_SHIFT_OFFSET = 5 -private const val BITS_PER_CHANNEL = 8 -private const val BYTE_MASK = 0x16 - -private val darkPastelColors = listOf( - Color(19, 37, 54, 255), - Color(68, 83, 36), - Color(43, 73, 63), - Color(90, 13, 5, 255), - Color(58, 88, 84), - Color(58, 42, 8), - Color(100, 66, 108), - Color(23, 17, 80), - Color(127, 36, 126), - Color(120, 30, 32), - Color(59, 56, 100), - Color(149, 52, 53), - Color(50, 47, 94, 255), - Color(14, 11, 27), - Color(121, 32, 59), - Color(93, 42, 18), - Color(124, 59, 70), - Color(55, 62, 22, 255), - Color(54, 116, 78), - Color(100, 36, 71), - Color(44, 61, 19), - Color(34, 32, 171), - Color(63, 7, 78, 255), +private val lightPastels = listOf( + Color(255, 179, 179), + Color(255, 217, 153), + Color(255, 242, 179), + Color(204, 255, 179), + Color(179, 255, 217), + Color(179, 230, 255), + Color(204, 191, 255), + Color(242, 191, 255), + Color(255, 191, 230), + Color(230, 217, 191), ) -private val lightPastelColors = listOf( - Color(71, 104, 188), - Color(197, 214, 160), - Color(168, 203, 192), - Color(190, 119, 81, 255), - Color(189, 212, 209), - Color(232, 187, 89), - Color(220, 204, 224), - Color(220, 107, 107, 255), - Color(235, 184, 234), - Color(104, 234, 173, 255), - Color(193, 191, 220), - Color(241, 215, 215), - Color(226, 162, 224), - Color(121, 99, 193), - Color(234, 174, 192), - Color(232, 165, 135), - Color(232, 206, 211), - Color(106, 125, 177), - Color(197, 228, 209), - Color(223, 168, 198), - Color(173, 193, 10, 255), - Color(212, 211, 247), - Color(89, 165, 180), +private val darkPastels = listOf( + Color(153, 64, 64), + Color(153, 102, 51), + Color(140, 128, 51), + Color(89, 128, 64), + Color(64, 128, 102), + Color(64, 102, 153), + Color(89, 77, 140), + Color(128, 77, 140), + Color(153, 77, 115), + Color(128, 102, 77), ) fun Color.Companion.pastelFromString(string: String, useDark: Boolean): Color { - val utf16String = string.toByteArray(Charsets.UTF_16) - val hash = utf16String.fold(0) { hash, char -> - val shifted = hash shl DJB_SHIFT_OFFSET - char + shifted - hash + val hash = string.toByteArray(Charsets.UTF_8).fold(0) { acc, byte -> + (acc * 31 + byte.toInt()) and 0x7FFFFFFF } - - val num = (hash shr (0 * BITS_PER_CHANNEL)) and BYTE_MASK - - return if (useDark) darkPastelColors[num] else lightPastelColors[num] + val pastels = if (useDark) darkPastels else lightPastels + return pastels[hash % pastels.size] } -fun Color.Companion.pastelFromLong(long: Long, useDark: Boolean): Color { - val utf16String = long.toString().toByteArray(Charsets.UTF_16) - val hash = utf16String.fold(0) { hash, char -> - val shifted = hash shl DJB_SHIFT_OFFSET - char + shifted - hash - } - - val num = (hash shr (0 * BITS_PER_CHANNEL)) and BYTE_MASK +private val knuthHashConstant = 2654435761L - return if (useDark) darkPastelColors[num] else lightPastelColors[num] +fun Color.Companion.pastelFromLong(long: Long, useDark: Boolean): Color { + val pastels = if (useDark) darkPastels else lightPastels + val hash = (long * knuthHashConstant) and 0x7FFFFFF + val index = hash.toInt() % pastels.size + return pastels[index] } diff --git a/app/src/main/java/friendly/android/FeedCard.kt b/app/src/main/java/friendly/android/FeedCard.kt index 26190c4..53688f9 100644 --- a/app/src/main/java/friendly/android/FeedCard.kt +++ b/app/src/main/java/friendly/android/FeedCard.kt @@ -19,16 +19,18 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ElevatedSuggestionChip import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -279,28 +281,29 @@ private fun AvatarWithInterests( LazyRow( modifier = Modifier .align(Alignment.BottomCenter) - .padding(vertical = 8.dp) .fillMaxWidth(), ) { - item { Spacer(Modifier.width(8.dp)) } - - items(interests) { interest -> - Text( - text = interest.string, - modifier = Modifier - .padding(horizontal = 3.dp) - .clip(RoundedCornerShape(6.dp)) - .background( - color = Color.pastelFromString( - string = interest.string, - useDark = isSystemInDarkTheme(), - ), - ) - .padding(4.dp), + itemsIndexed(interests) { i, interest -> + val useDark = isSystemInDarkTheme() + val color = remember(interest.string, useDark) { + Color.pastelFromString( + string = interest.string, + useDark = useDark, + ) + } + Spacer(Modifier.width(8.dp)) + ElevatedSuggestionChip( + onClick = {}, + label = { Text(interest.string) }, + colors = SuggestionChipDefaults.suggestionChipColors( + containerColor = color, + labelColor = MaterialTheme.colorScheme.onSurface, + ), ) + if (i == interests.lastIndex) { + Spacer(Modifier.width(8.dp)) + } } - - item { Spacer(Modifier.width(8.dp)) } } } } diff --git a/app/src/main/java/friendly/android/ProfileScreen.kt b/app/src/main/java/friendly/android/ProfileScreen.kt index 8125a64..d34e8a1 100644 --- a/app/src/main/java/friendly/android/ProfileScreen.kt +++ b/app/src/main/java/friendly/android/ProfileScreen.kt @@ -8,7 +8,6 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExitTransition import androidx.compose.animation.expandIn import androidx.compose.animation.fadeIn -import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -22,8 +21,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.DropdownMenu @@ -36,6 +35,8 @@ import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.SuggestionChipDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar @@ -44,13 +45,13 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue 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.ClipEntry import androidx.compose.ui.platform.Clipboard @@ -417,26 +418,38 @@ fun LoadedProfileState( Spacer(Modifier.height(16.dp)) FlowRow( - horizontalArrangement = Arrangement.spacedBy( - space = 8.dp, - alignment = Alignment.CenterHorizontally, - ), + horizontalArrangement = Arrangement.Center, verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth(), ) { - state.profile.interests.raw.forEach { interest -> - Text( - text = interest.string, - modifier = Modifier - .clip(RoundedCornerShape(6.dp)) - .background( - color = Color.pastelFromString( - string = interest.string, - useDark = isSystemInDarkTheme(), - ), + state.profile.interests.raw.forEachIndexed { i, interest -> + val useDark = isSystemInDarkTheme() + val color = remember(interest.string, useDark) { + Color.pastelFromString( + string = interest.string, + useDark = useDark, + ) + } + val label = MaterialTheme.colorScheme.onSurface + key(interest.string) { + Row { + Spacer(Modifier.width(8.dp)) + SuggestionChip( + onClick = {}, + label = { Text(interest.string) }, + colors = SuggestionChipDefaults + .suggestionChipColors( + containerColor = color, + labelColor = label, + ), + border = null, + modifier = Modifier.height(32.dp), ) - .padding(4.dp), - ) + if (i == interests.size) { + Spacer(Modifier.width(8.dp)) + } + } + } } } diff --git a/app/src/main/java/friendly/android/UserAvatar.kt b/app/src/main/java/friendly/android/UserAvatar.kt index 9f365b2..9f49a30 100644 --- a/app/src/main/java/friendly/android/UserAvatar.kt +++ b/app/src/main/java/friendly/android/UserAvatar.kt @@ -58,6 +58,8 @@ class UserAvatarStyle( } } +private val knuthHashConstant = 2654435761L + @Composable fun UserAvatar( nickname: Nickname, @@ -66,7 +68,8 @@ fun UserAvatar( style: UserAvatarStyle, modifier: Modifier = Modifier, ) { - val shapeIndex = (userId.long % avatarShapes.size).toInt() + val hash = (userId.long * knuthHashConstant) and 0x7FFFFFFF + val shapeIndex = (hash % avatarShapes.size).toInt() val shape = avatarShapes[shapeIndex].toShape() SubcomposeAsyncImage( model = uri,