/* (c) naturalprogramming.com 2023-01-26 File created by Kari Laitinen 2023-02-02 Trying to run the animation through another thread https://stackoverflow.com/questions/62166878/ This simple app contains a Canvas and a Button. There is a blinking ball on the Canvas. Blinking can be controlled with the button. This app shows how to use a ViewModel and a coroutine to control animation. The following links were useful when this app was made: https://developer.android.com/topic/libraries/architecture/viewmodel https://stackoverflow.com/questions/69173460/ This is an Android app made of this single file. If you want to test this app, create an Android Compose project with the package name shown below. Then, store this file to the folder where MainActivity.kt is and make the following change to AndroidManifest.xml: android:name=".AnimationDemoCActivity" Some 'warnings': - tab size in this file is 3 spaces - underscores are used in the names invented by the programmer */ package animation.democ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import animation.democ.ui.theme.AnimationDemoCTheme import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch data class AnimationState( val button_text : String = "STOP", val animation_activated : Boolean = true, val ball_must_be_shown : Boolean = true ) class AnimationViewModel : ViewModel() { // Expose screen UI state private val _uiState = MutableStateFlow( AnimationState() ) // val uiState: StateFlow = _uiState.asStateFlow() // asStateFlow() represents this mutable state flow as a read-only state flow. fun consumableState() = _uiState.asStateFlow() fun reverse_ball_state() { _uiState.update { currentState -> currentState.copy( ball_must_be_shown = ! currentState.ball_must_be_shown ) } } fun start_animation() { _uiState.update { currentState -> currentState.copy( button_text = "STOP", animation_activated = true ) } print( "\n THREAD: " + Thread.currentThread() ) viewModelScope.launch( Dispatchers.IO ) { // cancelled when the ViewModel is cleared print( "\n Coroutine starts ...." ) print( "\n THREAD: " + Thread.currentThread() ) while( _uiState.value.animation_activated == true ) { delay(1_000 ) reverse_ball_state() } print( "\n Coroutine EXITS ..." ) } /* // start a new coroutine in the main thread. viewModelScope.launch { // cancelled when the ViewModel is cleared print( "\n Coroutine starts ...." ) print( "\n THREAD: " + Thread.currentThread() ) while( _uiState.value.animation_activated == true ) { delay(1_000 ) reverse_ball_state() } print( "\n Coroutine EXITS ..." ) } */ } fun stop_animation() { _uiState.update { currentState -> currentState.copy( button_text = "START", animation_activated = false ) } } } class AnimationDemoCActivity : ComponentActivity() { private val viewModel : AnimationViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate( savedInstanceState ) viewModel.start_animation() setContent { AnimationDemoCTheme { val viewState = viewModel.consumableState().collectAsState() // A surface container using the 'background' color from the theme Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { Column( horizontalAlignment = Alignment.CenterHorizontally ) { Box( modifier = Modifier.weight( 9F ) ) { Canvas( modifier = Modifier.fillMaxSize() ) { val canvasWidth = size.width val canvasHeight = size.height if (viewState.value.ball_must_be_shown == true) { drawCircle( color = Color.Cyan, center = Offset(x = canvasWidth / 2, y = canvasHeight / 2), radius = size.minDimension / 4 ) } } } // end Box Button( onClick = { if ( viewState.value.animation_activated == true ) { viewModel.stop_animation() } else { viewModel.start_animation() } }, modifier = Modifier.requiredHeight( 80.dp ). requiredWidth( 160.dp).weight( 1F ), shape = RoundedCornerShape( 20.dp ), colors = ButtonDefaults.buttonColors( backgroundColor = Color.LightGray ) ) { Text( text = viewState.value.button_text, fontSize = 24.sp ) } Spacer( modifier = Modifier.weight( 0.5F ) ) } // end Column } } } } }