Created
September 21, 2025 07:50
-
-
Save root-ansh/455d2dceb0b63d4a330524e53749421d to your computer and use it in GitHub Desktop.
AnimatedContent
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| @HiltViewModel | |
| class TestViewModel @Inject constructor( | |
| ): ViewModel(){ | |
| data class Entry(val value: String, val id: Long) | |
| data class MyScreenState(val list:List<Entry> = listOf()) | |
| sealed interface MyScreenIntent{ data object StartEmission: MyScreenIntent } | |
| private val _uiState = MutableStateFlow(MyScreenState()) | |
| val uiState: StateFlow<MyScreenState> = _uiState.asStateFlow() | |
| fun onIntent(intent: MyScreenIntent){ | |
| viewModelScope.launch { | |
| when(intent){ | |
| MyScreenIntent.StartEmission -> { | |
| randomLetterFlow().collect { | |
| list-> _uiState.update { it.copy(list) } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| private var data = mutableListOf<Entry>() | |
| private var sendEmpty = false | |
| private fun randomLetterFlow(): Flow<List<Entry>> = flow { | |
| fun Int.toAZLetter(): Char { | |
| val normalized = ((this % 26) + 26) % 26 | |
| return 'A' + normalized | |
| } | |
| while (currentCoroutineContext().isActive) { | |
| if (!sendEmpty){ | |
| emit(data.toList()) | |
| sendEmpty = true | |
| } | |
| else{ | |
| val char = (0..25).random().toAZLetter().toString() | |
| data.add(Entry(char, System.currentTimeMillis())) | |
| emit(data.sortedBy { it.value }.toList()) | |
| } | |
| delay(1000) | |
| } | |
| } | |
| } | |
| @Composable | |
| fun TestAnimatedContent( | |
| myViewModel: TestViewModel = hiltViewModel<TestViewModel>() | |
| ) { | |
| val uiState by myViewModel.uiState.collectAsStateWithLifecycle() | |
| LaunchedEffect(Unit) { myViewModel.onIntent(MyScreenIntent.StartEmission) } | |
| MyScreen(uiState) | |
| } | |
| @Composable | |
| fun MyScreen(state: TestViewModel.MyScreenState) { | |
| Column(Modifier.fillMaxSize().background(Color(0xFFFFD180))) { | |
| Text( | |
| modifier = Modifier.fillMaxWidth().background(Color(0xFFE5BB74)).padding(start = 8.dp, end = 8.dp, top = 32.dp, bottom = 16.dp), | |
| text = "Testing Animated Content", | |
| fontSize = 18.sp | |
| ) | |
| // CORRECT : the transition animation from empty to filled only happens once | |
| AnimatedContent(state.list.isEmpty()) { it -> | |
| if(it) MyScreenEmpty() | |
| else MyScreenFilled(state.list) | |
| } | |
| // WRONG : causes flickr | |
| // AnimatedContent(state.list) { it -> | |
| // if(it.isEmpty()) MyScreenEmpty() | |
| // else MyScreenFilled(state.list) | |
| // } | |
| } | |
| } | |
| @Composable | |
| fun MyScreenEmpty() { | |
| val infiniteTransition = rememberInfiniteTransition(label = "iconScale") | |
| val scale by infiniteTransition.animateFloat( | |
| initialValue = 0.8f, | |
| targetValue = 1.2f, | |
| animationSpec = infiniteRepeatable( | |
| animation = tween(durationMillis = 500, easing = LinearOutSlowInEasing), | |
| repeatMode = RepeatMode.Reverse | |
| ), | |
| label = "scaleAnimation" | |
| ) | |
| Column ( | |
| modifier = Modifier.fillMaxSize().background(Color(0xFFFFD180)), | |
| horizontalAlignment = Alignment.CenterHorizontally) { | |
| Spacer(Modifier.size(48.dp)) | |
| Icon( | |
| imageVector = Icons.Default.Warning, | |
| contentDescription = "", | |
| modifier = Modifier.size(200.dp).graphicsLayer { | |
| scaleX = scale | |
| scaleY = scale | |
| }, | |
| tint = Color(0xFFB67200) | |
| ) | |
| Text( | |
| modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 16.dp), | |
| text = "No Entries Found", | |
| fontSize = 36.sp, | |
| textAlign = TextAlign.Center, | |
| fontFamily = FontFamily.Monospace, | |
| fontWeight = FontWeight.ExtraBold | |
| ) | |
| } | |
| } | |
| @Composable | |
| fun MyScreenFilled(list: List<TestViewModel.Entry>) { | |
| LazyVerticalGrid( | |
| modifier = Modifier.fillMaxSize().background(Color(0xFFFFD180)), | |
| columns = GridCells.Fixed(2), | |
| contentPadding = PaddingValues(8.dp) | |
| ) { | |
| items(items = list, key = {it.id}){ value -> | |
| Card( | |
| modifier = Modifier | |
| .animateItem( | |
| fadeInSpec = spring(stiffness = Spring.StiffnessMediumLow), | |
| placementSpec = spring( | |
| stiffness = Spring.StiffnessLow, | |
| visibilityThreshold = IntOffset.VisibilityThreshold | |
| ), | |
| fadeOutSpec = spring(stiffness = Spring.StiffnessMediumLow) | |
| ) | |
| .padding(8.dp), | |
| elevation = CardDefaults.cardElevation(4.dp), | |
| colors = CardDefaults.cardColors(containerColor = Color(0xFFB67200)), | |
| shape = RoundedCornerShape(8.dp) | |
| ) { | |
| Text( | |
| modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 16.dp), | |
| text = value.value, | |
| fontSize = 54.sp, | |
| color = Color(0xFFFFEAC5), | |
| textAlign = TextAlign.Center, | |
| fontFamily = FontFamily.Monospace, | |
| fontWeight = FontWeight.ExtraBold | |
| ) | |
| } | |
| } | |
| } | |
| } | |
| @Preview | |
| @Composable | |
| fun Preview1() { | |
| MyScreen(TestViewModel.MyScreenState(listOf("A", "B", "C").map { TestViewModel.Entry(it,it.toLong()) })) | |
| } | |
| @Preview | |
| @Composable | |
| fun Preview2() { | |
| MyScreenEmpty() | |
| } | |
| @Preview | |
| @Composable | |
| fun Preview3() { | |
| MyScreenFilled((listOf("A", "B", "C").map { TestViewModel.Entry(it,it.toLong()) })) | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment