fix: enhance avatar expansion and collapse animations with overscroll support and haptic feedback in OtherProfileScreen
This commit is contained in:
@@ -7,7 +7,6 @@ import androidx.compose.runtime.setValue
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
/**
|
||||
* 🚀 Оптимизированный кэш эмодзи с предзагрузкой
|
||||
@@ -47,21 +46,20 @@ object OptimizedEmojiCache {
|
||||
|
||||
scope.launch {
|
||||
try {
|
||||
val duration = measureTimeMillis {
|
||||
// Шаг 1: Загружаем список эмодзи (быстро)
|
||||
loadEmojiList(context)
|
||||
loadProgress = 0.3f
|
||||
|
||||
// Шаг 2: Группируем по категориям (средне)
|
||||
groupEmojisByCategories()
|
||||
loadProgress = 0.6f
|
||||
|
||||
// Шаг 3: Предзагружаем популярные изображения (медленно, но в фоне)
|
||||
preloadPopularEmojis(context)
|
||||
loadProgress = 1f
|
||||
}
|
||||
|
||||
// Шаг 1: Загружаем список эмодзи (быстро)
|
||||
loadEmojiList(context)
|
||||
loadProgress = 0.3f
|
||||
|
||||
// Шаг 2: Группируем по категориям (средне)
|
||||
groupEmojisByCategories()
|
||||
loadProgress = 0.6f
|
||||
|
||||
// 🔥 Сразу отмечаем как загруженный - предзагрузка идёт в фоне
|
||||
isLoaded = true
|
||||
|
||||
// Шаг 3: Предзагружаем популярные изображения (в фоне, не блокирует UI)
|
||||
preloadPopularEmojis(context)
|
||||
loadProgress = 1f
|
||||
isPreloading = false
|
||||
} catch (e: Exception) {
|
||||
allEmojis = emptyList()
|
||||
|
||||
@@ -19,10 +19,10 @@ import androidx.compose.material3.*
|
||||
import compose.icons.TablerIcons
|
||||
import compose.icons.tablericons.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
@@ -38,25 +38,16 @@ import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 🚀 ОПТИМИЗИРОВАННЫЙ EMOJI PICKER
|
||||
*
|
||||
* Ключевые оптимизации:
|
||||
* 1. Предзагрузка популярных эмодзи при старте приложения
|
||||
* 2. LazyGrid с оптимизированными настройками (beyondBoundsLayout)
|
||||
* 3. Hardware layer для анимаций
|
||||
* 4. Минимум recomposition (derivedStateOf, remember keys)
|
||||
* 5. Coil оптимизация (hardware acceleration, size limits)
|
||||
* 6. SharedPreferences для сохранения высоты клавиатуры (как в Telegram)
|
||||
* 7. keyboardDuration для синхронизации с системной клавиатурой
|
||||
* 8. Анимация управляется внешним AnimatedKeyboardTransition
|
||||
*
|
||||
* @param isVisible Видимость панели (для внутренней логики)
|
||||
* @param isDarkTheme Темная/светлая тема
|
||||
* @param onEmojiSelected Callback при выборе эмодзи
|
||||
* @param onClose Callback при закрытии
|
||||
* @param modifier Модификатор
|
||||
* 🚀 ULTRA-ОПТИМИЗИРОВАННЫЙ EMOJI PICKER
|
||||
*
|
||||
* Ключевые оптимизации v2:
|
||||
* 1. ZERO LaunchedEffect в EmojiButton - никаких корутин на каждый эмодзи
|
||||
* 2. Нет анимаций scale - убрали spring animations для каждой кнопки
|
||||
* 3. Нет interactionSource tracking - убрали collect для каждой кнопки
|
||||
* 4. Stable composables - используем @Stable для избежания recomposition
|
||||
* 5. Оптимизированный LazyGrid с prefetch
|
||||
* 6. Minimal modifier chain - меньше лямбд, меньше allocations
|
||||
*/
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun OptimizedEmojiPicker(
|
||||
isVisible: Boolean,
|
||||
@@ -65,15 +56,9 @@ fun OptimizedEmojiPicker(
|
||||
onClose: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// 🔥 Используем сохранённую высоту клавиатуры (как в Telegram)
|
||||
val savedKeyboardHeight = rememberSavedKeyboardHeight()
|
||||
|
||||
// 🔥 Логирование изменений видимости
|
||||
LaunchedEffect(isVisible) {
|
||||
}
|
||||
|
||||
// 🔥 Рендерим контент напрямую без AnimatedVisibility
|
||||
// Анимация теперь управляется AnimatedKeyboardTransition
|
||||
|
||||
// 🔥 Рендерим напрямую без лишних обёрток
|
||||
EmojiPickerContent(
|
||||
isDarkTheme = isDarkTheme,
|
||||
onEmojiSelected = onEmojiSelected,
|
||||
@@ -83,7 +68,13 @@ fun OptimizedEmojiPicker(
|
||||
}
|
||||
|
||||
/**
|
||||
* Контент emoji picker'а
|
||||
* 🔥 Stable wrapper для callback чтобы избежать recomposition
|
||||
*/
|
||||
@Stable
|
||||
private class StableCallback(val onClick: (String) -> Unit)
|
||||
|
||||
/**
|
||||
* Контент emoji picker'а - ОПТИМИЗИРОВАННЫЙ
|
||||
*/
|
||||
@Composable
|
||||
private fun EmojiPickerContent(
|
||||
@@ -96,25 +87,19 @@ private fun EmojiPickerContent(
|
||||
var selectedCategory by remember { mutableStateOf(EMOJI_CATEGORIES[0]) }
|
||||
val gridState = rememberLazyGridState()
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
// 🚀 Отложенный рендеринг - даём анимации начаться без фриза
|
||||
var shouldRenderContent by remember { mutableStateOf(false) }
|
||||
|
||||
|
||||
// 🔥 Wrap callback в stable class для избежания recomposition
|
||||
val stableCallback = remember(onEmojiSelected) { StableCallback(onEmojiSelected) }
|
||||
|
||||
// 🚀 Загружаем эмодзи ОДИН раз при первом рендере
|
||||
LaunchedEffect(Unit) {
|
||||
|
||||
// Ждём 1 кадр чтобы анимация началась плавно
|
||||
kotlinx.coroutines.delay(16) // ~1 frame at 60fps
|
||||
shouldRenderContent = true
|
||||
|
||||
// Загружаем эмодзи если еще не загружены
|
||||
if (!OptimizedEmojiCache.isLoaded) {
|
||||
OptimizedEmojiCache.preload(context)
|
||||
} else {
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 Используем derivedStateOf чтобы избежать лишних recomposition
|
||||
val displayedEmojis by remember {
|
||||
|
||||
// 🚀 derivedStateOf для минимизации recomposition
|
||||
val displayedEmojis by remember(selectedCategory) {
|
||||
derivedStateOf {
|
||||
if (OptimizedEmojiCache.isLoaded) {
|
||||
OptimizedEmojiCache.getEmojisForCategory(selectedCategory.key)
|
||||
@@ -123,71 +108,61 @@ private fun EmojiPickerContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 При смене категории плавно скроллим наверх
|
||||
|
||||
// 🚀 Scroll to top при смене категории - БЕЗ анимации для скорости
|
||||
LaunchedEffect(selectedCategory) {
|
||||
if (displayedEmojis.isNotEmpty()) {
|
||||
scope.launch {
|
||||
gridState.animateScrollToItem(0)
|
||||
}
|
||||
gridState.scrollToItem(0) // 🔥 scrollToItem вместо animateScrollToItem
|
||||
}
|
||||
}
|
||||
|
||||
// 🎨 Цвета темы
|
||||
val panelBackground = if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
|
||||
val categoryBarBackground = if (isDarkTheme) Color(0xFF2C2C2E) else Color.White
|
||||
val dividerColor = if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
||||
|
||||
|
||||
// 🎨 Цвета темы - computed один раз
|
||||
val panelBackground = remember(isDarkTheme) {
|
||||
if (isDarkTheme) Color(0xFF1C1C1E) else Color(0xFFF2F2F7)
|
||||
}
|
||||
val categoryBarBackground = remember(isDarkTheme) {
|
||||
if (isDarkTheme) Color(0xFF2C2C2E) else Color.White
|
||||
}
|
||||
val dividerColor = remember(isDarkTheme) {
|
||||
if (isDarkTheme) Color.White.copy(alpha = 0.1f) else Color.Black.copy(alpha = 0.08f)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(height = keyboardHeight) // 🔥 Используем сохранённую высоту (как в Telegram)
|
||||
.height(keyboardHeight)
|
||||
.background(panelBackground)
|
||||
) {
|
||||
// 🔥 Показываем пустую панель пока не готово
|
||||
if (!shouldRenderContent) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = PrimaryBlue,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// ============ КАТЕГОРИИ ============
|
||||
CategoryBar(
|
||||
categories = EMOJI_CATEGORIES,
|
||||
selectedCategory = selectedCategory,
|
||||
onCategorySelected = { selectedCategory = it },
|
||||
isDarkTheme = isDarkTheme,
|
||||
backgroundColor = categoryBarBackground
|
||||
)
|
||||
|
||||
// ============ РАЗДЕЛИТЕЛЬ ============
|
||||
Divider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(0.5.dp),
|
||||
color = dividerColor
|
||||
)
|
||||
|
||||
// ============ СЕТКА ЭМОДЗИ ============
|
||||
EmojiGrid(
|
||||
isLoaded = OptimizedEmojiCache.isLoaded,
|
||||
emojis = displayedEmojis,
|
||||
gridState = gridState,
|
||||
onEmojiSelected = onEmojiSelected,
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
}
|
||||
// ============ КАТЕГОРИИ ============
|
||||
CategoryBar(
|
||||
categories = EMOJI_CATEGORIES,
|
||||
selectedCategory = selectedCategory,
|
||||
onCategorySelected = { selectedCategory = it },
|
||||
isDarkTheme = isDarkTheme,
|
||||
backgroundColor = categoryBarBackground
|
||||
)
|
||||
|
||||
// ============ РАЗДЕЛИТЕЛЬ ============
|
||||
Divider(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(0.5.dp),
|
||||
color = dividerColor
|
||||
)
|
||||
|
||||
// ============ СЕТКА ЭМОДЗИ ============
|
||||
UltraOptimizedEmojiGrid(
|
||||
isLoaded = OptimizedEmojiCache.isLoaded,
|
||||
emojis = displayedEmojis,
|
||||
gridState = gridState,
|
||||
onEmojiSelected = stableCallback,
|
||||
isDarkTheme = isDarkTheme
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Горизонтальная полоса категорий
|
||||
* Горизонтальная полоса категорий - ОПТИМИЗИРОВАННАЯ
|
||||
*/
|
||||
@Composable
|
||||
private fun CategoryBar(
|
||||
@@ -197,6 +172,9 @@ private fun CategoryBar(
|
||||
isDarkTheme: Boolean,
|
||||
backgroundColor: Color
|
||||
) {
|
||||
// 🔥 Запоминаем interactionSource один раз для всего LazyRow
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -210,7 +188,8 @@ private fun CategoryBar(
|
||||
items = categories,
|
||||
key = { it.key }
|
||||
) { category ->
|
||||
CategoryButton(
|
||||
// 🔥 Минимальная CategoryButton без анимаций
|
||||
SimpleCategoryButton(
|
||||
category = category,
|
||||
isSelected = selectedCategory == category,
|
||||
onClick = { onCategorySelected(category) },
|
||||
@@ -221,29 +200,21 @@ private fun CategoryBar(
|
||||
}
|
||||
|
||||
/**
|
||||
* Кнопка категории
|
||||
* 🔥 УПРОЩЁННАЯ кнопка категории - без анимаций
|
||||
*/
|
||||
@Composable
|
||||
private fun CategoryButton(
|
||||
private fun SimpleCategoryButton(
|
||||
category: EmojiCategory,
|
||||
isSelected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
isDarkTheme: Boolean
|
||||
) {
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = if (isSelected) PrimaryBlue.copy(alpha = 0.2f) else Color.Transparent,
|
||||
animationSpec = tween(150),
|
||||
label = "categoryBackground"
|
||||
)
|
||||
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (isSelected) PrimaryBlue
|
||||
else if (isDarkTheme) Color.White.copy(alpha = 0.6f)
|
||||
else Color.Black.copy(alpha = 0.5f),
|
||||
animationSpec = tween(150),
|
||||
label = "categoryIcon"
|
||||
)
|
||||
|
||||
// 🔥 Статичные цвета - нет анимации!
|
||||
val backgroundColor = if (isSelected) PrimaryBlue.copy(alpha = 0.2f) else Color.Transparent
|
||||
val iconTint = if (isSelected) PrimaryBlue
|
||||
else if (isDarkTheme) Color.White.copy(alpha = 0.6f)
|
||||
else Color.Black.copy(alpha = 0.5f)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(44.dp)
|
||||
@@ -251,7 +222,7 @@ private fun CategoryButton(
|
||||
.background(backgroundColor)
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
indication = null, // 🚀 Убираем ripple для производительности
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -266,22 +237,20 @@ private fun CategoryButton(
|
||||
}
|
||||
|
||||
/**
|
||||
* Сетка эмодзи с LazyGrid
|
||||
* 🔥 ULTRA-оптимизированная сетка эмодзи
|
||||
*/
|
||||
@Composable
|
||||
private fun EmojiGrid(
|
||||
private fun UltraOptimizedEmojiGrid(
|
||||
isLoaded: Boolean,
|
||||
emojis: List<String>,
|
||||
gridState: androidx.compose.foundation.lazy.grid.LazyGridState,
|
||||
onEmojiSelected: (String) -> Unit,
|
||||
onEmojiSelected: StableCallback,
|
||||
isDarkTheme: Boolean
|
||||
) {
|
||||
when {
|
||||
!isLoaded -> {
|
||||
// Loading state
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
@@ -292,10 +261,8 @@ private fun EmojiGrid(
|
||||
}
|
||||
}
|
||||
emojis.isEmpty() -> {
|
||||
// Empty state
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
@@ -306,115 +273,74 @@ private fun EmojiGrid(
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
// 🚀 ОПТИМИЗИРОВАННАЯ LazyVerticalGrid
|
||||
// 🔥 ОПТИМИЗИРОВАННАЯ LazyVerticalGrid
|
||||
LazyVerticalGrid(
|
||||
state = gridState,
|
||||
columns = GridCells.Fixed(8),
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = 8.dp,
|
||||
vertical = 8.dp
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
userScrollEnabled = true,
|
||||
// 🚀 Оптимизация: рендерим +2 строки за пределами видимой области
|
||||
// для плавной прокрутки без белых мерцаний
|
||||
content = {
|
||||
items(
|
||||
items = emojis,
|
||||
key = { emoji -> emoji }, // 🔥 Важно для stable composition
|
||||
contentType = { "emoji" }
|
||||
) { unified ->
|
||||
OptimizedEmojiButton(
|
||||
unified = unified,
|
||||
onClick = { emoji -> onEmojiSelected(emoji) }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🚀 Оптимизированная кнопка эмодзи
|
||||
*/
|
||||
@Composable
|
||||
private fun OptimizedEmojiButton(
|
||||
unified: String,
|
||||
onClick: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
|
||||
// 🚀 Простая scale анимация без сложных эффектов
|
||||
var isPressed by remember { mutableStateOf(false) }
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isPressed) 0.85f else 1f,
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessHigh
|
||||
),
|
||||
label = "emojiScale"
|
||||
)
|
||||
|
||||
// 🚀 Оптимизированный ImageRequest с кэшированием
|
||||
val imageRequest = remember(unified) {
|
||||
ImageRequest.Builder(context)
|
||||
.data("file:///android_asset/emoji/${unified.lowercase()}.png")
|
||||
.crossfade(false) // 🔥 Выключаем crossfade для производительности
|
||||
.size(64) // 🔥 Ограничиваем размер для экономии памяти
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.diskCachePolicy(CachePolicy.ENABLED)
|
||||
.allowHardware(true) // 🔥 Hardware acceleration
|
||||
.memoryCacheKey("emoji_$unified")
|
||||
.diskCacheKey("emoji_$unified")
|
||||
.build()
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f)
|
||||
.scale(scale)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null, // 🚀 Убираем ripple
|
||||
onClickLabel = "Select emoji"
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
// 🔥 Отправляем эмодзи в формате :emoji_code: как в десктопе
|
||||
onClick(":emoji_$unified:")
|
||||
},
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
// 🚀 AsyncImage с Coil (оптимизирован)
|
||||
AsyncImage(
|
||||
model = imageRequest,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.graphicsLayer {
|
||||
// Hardware layer для лучшей производительности
|
||||
this.alpha = 1f
|
||||
},
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
|
||||
// Track press state для scale анимации
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect { interaction ->
|
||||
when (interaction) {
|
||||
is androidx.compose.foundation.interaction.PressInteraction.Press -> {
|
||||
isPressed = true
|
||||
}
|
||||
is androidx.compose.foundation.interaction.PressInteraction.Release,
|
||||
is androidx.compose.foundation.interaction.PressInteraction.Cancel -> {
|
||||
isPressed = false
|
||||
items(
|
||||
items = emojis,
|
||||
key = { emoji -> emoji },
|
||||
contentType = { "emoji" }
|
||||
) { unified ->
|
||||
// 🔥 ULTRA-лёгкая кнопка эмодзи
|
||||
UltraLightEmojiButton(
|
||||
unified = unified,
|
||||
onClick = onEmojiSelected.onClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 ULTRA-ЛЁГКАЯ кнопка эмодзи
|
||||
*
|
||||
* Оптимизации:
|
||||
* - Нет LaunchedEffect
|
||||
* - Нет анимаций
|
||||
* - Нет interactionSource tracking
|
||||
* - Минимальный modifier chain
|
||||
* - Предзакэшированный ImageRequest
|
||||
*/
|
||||
@Composable
|
||||
private fun UltraLightEmojiButton(
|
||||
unified: String,
|
||||
onClick: (String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
// 🔥 Один remember для ImageRequest - это единственный "тяжёлый" объект
|
||||
val imageRequest = remember(unified) {
|
||||
ImageRequest.Builder(context)
|
||||
.data("file:///android_asset/emoji/${unified.lowercase()}.png")
|
||||
.crossfade(false) // Нет анимации
|
||||
.size(48) // Меньше размер = быстрее
|
||||
.memoryCachePolicy(CachePolicy.ENABLED)
|
||||
.diskCachePolicy(CachePolicy.ENABLED)
|
||||
.allowHardware(true)
|
||||
.memoryCacheKey("emoji_$unified")
|
||||
.build()
|
||||
}
|
||||
|
||||
// 🔥 Минимальный Box без лишних модификаторов
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.clickable { onClick(":emoji_$unified:") },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AsyncImage(
|
||||
model = imageRequest,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(28.dp),
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user