/* 2022-09-20 File created by Kari Laitinen 2022-09-22 Last modification. (c) naturalprogramming.com This simple app contains two rows of Buttons and a custom view, as well as a menu. You can see how to use ConstraintSet to create the UI without XML. This is an Android app made of this single file. If you want to test this app, create an Android Kotlin 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=".MovingBallNoXMLActivity" Some 'warnings': - tab size in this file is 3 spaces - underscores are used in the names invented by the programmer */ package movingball.noxml import android.content.Context import android.content.res.Resources import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint import androidx.appcompat.app.AppCompatActivity import android.os.Bundle import android.util.AttributeSet import android.view.* import android.widget.Button import android.widget.TextView import android.widget.Toast import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.google.android.material.button.MaterialButton object Values { var screen_width = 0 // These will be set var screen_height = 0 var ball_radius = 0.0F var button_width = 0 var button_height = 0 } class MovingBallView : View { var ball_center_point_x = 100F var ball_center_point_y = 100F var ball_color = Color.RED var movement_in_pixels = 8 constructor(context: Context?) : super(context) { setBackgroundColor( 0xFFE7E7EF.toInt() ) } // The following constructor is needed when MovingBallView object is // specified in an XML file, and is thus created automatically. constructor( context: Context?, attributes: AttributeSet? ) : super( context, attributes ) { setBackgroundColor( 0xFFF5F5F5.toInt() ) // white smoke color } public override fun onSizeChanged( current_width_of_this_view: Int, current_height_of_this_view: Int, old_width_of_this_view: Int, old_height_of_this_view: Int) { ball_center_point_x = current_width_of_this_view.toFloat() / 2 ball_center_point_y = current_height_of_this_view.toFloat() / 2 } fun move_ball_left() { ball_center_point_x -= movement_in_pixels invalidate() } fun move_ball_down() { ball_center_point_y += movement_in_pixels invalidate() } fun move_ball_up() { ball_center_point_y -= movement_in_pixels invalidate() } fun move_ball_right() { ball_center_point_x += movement_in_pixels invalidate() } fun set_ball_color( new_color: Int ) { ball_color = new_color invalidate() } override fun onDraw( canvas: Canvas) { val filling_paint = Paint() filling_paint.style = Paint.Style.FILL filling_paint.color = ball_color canvas.drawCircle( ball_center_point_x, ball_center_point_y, Values.ball_radius, filling_paint) val outline_paint = Paint() outline_paint.style = Paint.Style.STROKE // Default color for a Paint is black. canvas.drawCircle( ball_center_point_x, ball_center_point_y, Values.ball_radius, outline_paint ) val text_paint = Paint() text_paint.style = Paint.Style.STROKE text_paint.textSize = 48F canvas.drawText( "(" + ball_center_point_x + ", " + ball_center_point_y + ")", 20f, 48f, text_paint ) } } class BallControlButton( context : Context, button_text : String ) : MaterialButton( context ) { init { id = generateViewId() setBackgroundColor( 0xFF778899.toInt() ) setTextColor( Color.WHITE ) textSize = 36F text = button_text cornerRadius = 40 // Works only with MaterialButton, I guess. } } class MovingBallConstraintLayout( context : Context) : ConstraintLayout( context ) { lateinit var color_button : Button lateinit var moving_ball_view : MovingBallView init { id = generateViewId() // id for parent val basic_layout_params = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) layoutParams = basic_layout_params setBackgroundColor( Color.LTGRAY ) val constraint_set = ConstraintSet() constraint_set.clone( this ) moving_ball_view = MovingBallView( context ) moving_ball_view.id = generateViewId() addView( moving_ball_view ) val left_button = BallControlButton( context, "\u25C0" ) addView( left_button ) constraint_set.constrainWidth( left_button.id, Values.button_width ) constraint_set.constrainHeight( left_button.id, Values.button_height ) constraint_set.connect( left_button.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM ) left_button.setOnClickListener{ moving_ball_view.move_ball_left() } val down_button = BallControlButton( context, "\u25BC" ) addView( down_button ) constraint_set.constrainWidth( down_button.id, Values.button_width ) constraint_set.constrainHeight( down_button.id, Values.button_height ) constraint_set.connect( down_button.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM ) down_button.setOnClickListener{ moving_ball_view.move_ball_down() } val right_button = BallControlButton( context, "\u25B6" ) addView( right_button ) constraint_set.constrainWidth( right_button.id, Values.button_width ) constraint_set.constrainHeight( right_button.id, Values.button_height ) constraint_set.connect( right_button.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM ) right_button.setOnClickListener{ moving_ball_view.move_ball_right() } val bottom_button_row_chain = intArrayOf( left_button.id, down_button.id, right_button.id ) constraint_set.createHorizontalChain( ConstraintSet.PARENT_ID, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT, bottom_button_row_chain, null, ConstraintSet.CHAIN_SPREAD ) val reset_button = BallControlButton( context, "\u21BB" ) addView( reset_button ) constraint_set.constrainWidth( reset_button.id, Values.button_width ) constraint_set.constrainHeight( reset_button.id, Values.button_height ) constraint_set.connect( reset_button.id, ConstraintSet.BOTTOM, left_button.id, ConstraintSet.TOP ) reset_button.setOnClickListener{ Toast.makeText( context, "Reset/Reload not Implemented", Toast.LENGTH_SHORT ).show() } val up_button = BallControlButton( context, "\u25B2" ) addView( up_button ) constraint_set.constrainWidth( up_button.id, Values.button_width ) constraint_set.constrainHeight( up_button.id, Values.button_height ) constraint_set.connect( up_button.id, ConstraintSet.BOTTOM, down_button.id, ConstraintSet.TOP ) up_button.setOnClickListener{ moving_ball_view.move_ball_up() } color_button = BallControlButton( context, "\uD83C\uDFA8" ) addView( color_button ) constraint_set.constrainWidth( color_button.id, Values.button_width ) constraint_set.constrainHeight( color_button.id, Values.button_height ) constraint_set.connect( color_button.id, ConstraintSet.BOTTOM, right_button.id, ConstraintSet.TOP ) val upper_button_row_chain = intArrayOf( reset_button.id, up_button.id, color_button.id ) constraint_set.createHorizontalChain( ConstraintSet.PARENT_ID, ConstraintSet.LEFT, ConstraintSet.PARENT_ID, ConstraintSet.RIGHT, upper_button_row_chain, null, ConstraintSet.CHAIN_SPREAD ) constraint_set.connect( moving_ball_view.id, ConstraintSet.TOP, ConstraintSet.PARENT_ID, ConstraintSet.TOP ) constraint_set.connect( moving_ball_view.id, ConstraintSet.BOTTOM, up_button.id, ConstraintSet.TOP ) constraint_set.constrainWidth( moving_ball_view.id, Values.screen_width ) constraint_set.centerHorizontally( moving_ball_view.id, ConstraintSet.PARENT_ID ) constraint_set.applyTo( this ) } } class MovingBallNoXMLActivity : AppCompatActivity() { lateinit var moving_ball_constraint_layout : MovingBallConstraintLayout override fun onCreate( savedInstanceState: Bundle? ) { super.onCreate( savedInstanceState ) Values.screen_width = Resources.getSystem().displayMetrics.widthPixels Values.screen_height = Resources.getSystem().displayMetrics.heightPixels if ( Values.screen_width > 1000 ) { Values.ball_radius = 150F Values.button_width = 300 Values.button_height = 240 } else { Values.ball_radius = 100F Values.button_width = 200 Values.button_height = 160 } moving_ball_constraint_layout = MovingBallConstraintLayout( this ) setContentView( moving_ball_constraint_layout ) // with the following line we specify that the 'floating' // context menu must be shown when the 'color' button is long-pressed. registerForContextMenu( moving_ball_constraint_layout.color_button ) } // The following method is called when the 'floating' // context menu needs to be created. val item_id_red = View.generateViewId() val item_id_white = View.generateViewId() val item_id_yellow = View.generateViewId() val item_id_green = View.generateViewId() val item_id_blue = View.generateViewId() val item_id_magenta = View.generateViewId() val item_id_cyan = View.generateViewId() private fun add_menu_items( menu : Menu) { menu.add( 0, item_id_red, 0, "RED" ) menu.add( 0, item_id_white, 0, "WHITE" ) menu.add( 0, item_id_yellow, 0, "YELLOW" ) menu.add( 0, item_id_green, 0, "GREEN" ) menu.add( 0, item_id_blue, 0, "BLUE" ) menu.add( 0, item_id_magenta, 0, "MAGENTA" ) menu.add( 0, item_id_cyan, 0, "CYAN" ) } // The following method is called when the Options // menu needs to be created. override fun onCreateOptionsMenu( menu: Menu ) : Boolean { super.onCreateOptionsMenu( menu ) add_menu_items( menu ) return true } override fun onCreateContextMenu( menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo? ) { super.onCreateContextMenu( menu, v, menuInfo ) menu.setHeaderTitle( "CONTEXT MENU:" ) add_menu_items( menu ) } // The following method is used to process selections from // both menus, the options menu and the context menu. private fun process_menu_selection(menu_item_id: Int ): Boolean { return when ( menu_item_id ) { item_id_red -> { moving_ball_constraint_layout.moving_ball_view.set_ball_color( Color.RED ) true } item_id_white -> { moving_ball_constraint_layout.moving_ball_view.set_ball_color( Color.WHITE ) true } item_id_yellow -> { moving_ball_constraint_layout.moving_ball_view.set_ball_color( Color.YELLOW ) true } item_id_green -> { moving_ball_constraint_layout.moving_ball_view.set_ball_color( Color.GREEN ) true } item_id_blue -> { moving_ball_constraint_layout.moving_ball_view.set_ball_color( Color.BLUE ) true } item_id_magenta -> { moving_ball_constraint_layout.moving_ball_view.set_ball_color( Color.MAGENTA ) true } item_id_cyan -> { moving_ball_constraint_layout.moving_ball_view.set_ball_color( Color.CYAN ) true } else -> false } } // The following method is called when a selection in // the Options menu has been made. override fun onOptionsItemSelected( menu_item: MenuItem ) : Boolean { return if ( process_menu_selection( menu_item.itemId ) ) { true } else { super.onOptionsItemSelected( menu_item ) } } // The following method is called when a selection in // the context menu has been made. override fun onContextItemSelected( menu_item: MenuItem) : Boolean { return if ( process_menu_selection( menu_item.itemId ) ) { true } else { super.onContextItemSelected(menu_item) } } }