/* 2022-09-28 File created by Kari Laitinen (c) naturalprogramming.com 2023-01-16 Last modification. This example demonstrates - creation of buttons with a Composable. - using Canvas, menu, 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=".MovingBallCActivity" Some 'warnings': - tab size in this file is 3 spaces - underscores are used in the names invented by the programmer See comments at the end of this file. */ package moving.ballc import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.Composable 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.Canvas import androidx.compose.ui.graphics.Color import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import moving.ballc.ui.theme.MovingBallCTheme // I had to be clever enough to put the following import myself // https://www.valueof.io/blog/jetpack-compose-canvas-custom-component-ondraw-drawscope import androidx.compose.foundation.Canvas import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext @Composable fun BallControlButton( onClick: () -> Unit, modifier: Modifier = Modifier, button_text: String //content: @Composable RowScope.() -> Unit ) { Button( colors = ButtonDefaults.buttonColors( backgroundColor = Color( 0xFF778899.toInt() ) /* Other colors use values from MaterialTheme */ ), shape = RoundedCornerShape( 20.dp ), onClick = onClick, modifier = modifier //content = content ) { Text( text = button_text, fontSize = 36.sp, color = Color.White ) } } class MovingBallCActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { MovingBallCTheme { // A surface container using the 'background' color from the theme Surface( modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { val configuration = LocalConfiguration.current val context = LocalContext.current val screen_height = configuration.screenHeightDp.dp val screenWidth = configuration.screenWidthDp.dp val ball_offset_x = remember { mutableStateOf( 0F ) } val ball_offset_y = remember { mutableStateOf( 0F ) } val ball_color = remember { mutableStateOf( Color.Red ) } val menu_must_be_shown = remember { mutableStateOf( false ) } Column( horizontalAlignment = Alignment.CenterHorizontally ) { // With a Box we can put Text and Canvas to the // same area on the screen. Box( modifier = Modifier .requiredWidth( screenWidth ).weight( 8F ) ) { Text( "Ball offset: ( ${ ball_offset_x.value }" + ", ${ ball_offset_y.value } )", fontSize = 20.sp ) // As the area reserved for Canvas is now determined // by the Box, we can just fill all that area. Canvas( modifier = Modifier.fillMaxSize() ) { val canvasWidth = size.width val canvasHeight = size.height drawCircle( color = ball_color.value, center = Offset( x = canvasWidth / 2 + ball_offset_x.value, y = canvasHeight / 2 + ball_offset_y.value ), radius = size.minDimension / 4 ) drawCircle( color = Color.Black, center = Offset( x = canvasWidth / 2 + ball_offset_x.value, y = canvasHeight / 2 + ball_offset_y.value ), radius = size.minDimension / 4, style = Stroke(width = 4F) ) } } // DropDownMenu is inside a Box Box( contentAlignment = Alignment.Center ) { DropdownMenu( expanded = menu_must_be_shown.value, onDismissRequest = { menu_must_be_shown.value = false } ) { DropdownMenuItem(onClick = { /* Handle send feedback! */ }) { Text("Choose Ball Color") } Divider() DropdownMenuItem( onClick = { ball_color.value = Color.Red menu_must_be_shown.value = false } ) { Text("RED") } DropdownMenuItem( onClick = { ball_color.value = Color.White menu_must_be_shown.value = false } ) { Text("WHITE") } DropdownMenuItem( onClick = { ball_color.value = Color.Green menu_must_be_shown.value = false } ) { Text("GREEN") } DropdownMenuItem( onClick = { ball_color.value = Color.Yellow menu_must_be_shown.value = false } ) { Text("YELLOW") } DropdownMenuItem( onClick = { ball_color.value = Color.Blue menu_must_be_shown.value = false } ) { Text("BLUE") } DropdownMenuItem( onClick = { ball_color.value = Color.Cyan menu_must_be_shown.value = false } ) { Text("CYAN") } } } // Next we'll specify the button rows. // Button heights relate to screen height. // This way the buttons look good also on some // smaller screens. Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight( 2F ) ) { BallControlButton( onClick = // Reset button { Toast.makeText( context, "Reset not yet implemented. Do it yourself.", Toast.LENGTH_SHORT ).show() }, modifier = Modifier .requiredHeight( screen_height / 8 ) .requiredWidth(100.dp) .weight(1F), "↻" ) // Horizontal weight BallControlButton( onClick = // Up button { ball_offset_y.value = ball_offset_y.value - 8 }, modifier = Modifier .requiredHeight( screen_height / 8 ) .requiredWidth(100.dp) .weight(1F), "▲" ) BallControlButton( onClick = // Color button { menu_must_be_shown.value = true }, modifier = Modifier .requiredHeight(screen_height / 8 ) .requiredWidth(100.dp) .weight(1F), "\uD83C\uDFA8" ) } // End row // Bottom row begins Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight( 2F ) ) { BallControlButton( onClick = // Left button { ball_offset_x.value = ball_offset_x.value - 8 }, modifier = Modifier .requiredHeight( screen_height / 8 ) .requiredWidth(100.dp) .weight(1F), "◀" ) BallControlButton( onClick = // Down button { ball_offset_y.value = ball_offset_y.value + 8 }, modifier = Modifier .requiredHeight( screen_height / 8 ) .requiredWidth(100.dp) .weight(1F), "▼" ) BallControlButton( onClick = // Right button { ball_offset_x.value = ball_offset_x.value + 8 }, modifier = Modifier .requiredHeight( screen_height / 8 ) .requiredWidth(100.dp) .weight(1F), "▶" ) } // End row } } } } } } /* // When BallControlButton was defined in the following way, // the button had to be defined as shown below. @Composable fun BallControlButton( onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit ) { Button( colors = ButtonDefaults.buttonColors( backgroundColor = Color( 0xFF778899.toInt() ) /* Other colors use values from MaterialTheme */ ), shape = RoundedCornerShape( 20.dp ), onClick = onClick, modifier = modifier, content = content ) } */ /* BallControlButton( onClick = { if ( changing_text.value == "Hello!" ) { changing_text.value = "Well done!" } else { changing_text.value = "Hello!" } }, modifier = Modifier.requiredHeight( 100.dp ). requiredWidth( 100.dp ).weight( 1F ) ) // Horizontal weight { Text( text = "↻", fontSize = 36.sp ) } */