// BouncingBallActivity.java (c) Kari Laitinen // http://www.naturalprogramming.com // 2012-09-13 Java file created. // 2021-01-15 Kotlin version created. /* This program shows a ball, or a bouncer, that moves on the screen. The ball bounces when it hits a 'wall'. The bouncer will explode when it is touched. When the bouncer has fully exploded, it will be re-created if the screen is touched again. To run this program set up an Andoid Studio project and set BouncingBallActivity as the name of its main activity. Use the package name given below. Copy this file to the correct subfolder in the Android project. This program does not need any specific .xml files. This program provides the following classes which might be used also in other programs: GraphicalObject - represent an object that has a position, a color Bouncer extends GraphicalObject - represents a ball object that can move and bounce inside a given rectangular area RotatingBouncer extends Bouncer - represents a bouncer that rotates while it is moving ExplodingBouncer extends RotatingBouncer - a rotating bouncer that can be made to explode and disappear */ package bouncing.ball import android.app.Activity import android.content.Context import android.graphics.* import android.os.Bundle import android.view.MotionEvent import android.view.View internal open class GraphicalObject( var object_center_point : PointF, var object_color : Int = Color.RED ) { // object_velocity specifies the number of pixels the object // will be moved in a single movement operation. var object_velocity = 4.0f fun get_object_position(): PointF? { return object_center_point } fun set_color( new_color: Int ) { object_color = new_color } // In the following methods special Kotlin syntax is in use. // See: https://kotlinlang.org/docs/reference/operator-overloading.html fun move_right() { object_center_point[ object_center_point.x + object_velocity ] = object_center_point.y } fun move_left() { object_center_point[ object_center_point.x - object_velocity ] = object_center_point.y } fun move_up() { object_center_point[ object_center_point.x ] = object_center_point.y - object_velocity } fun move_down() { object_center_point[ object_center_point.x ] = object_center_point.y + object_velocity } fun move_this_object( movement_in_direction_x: Float, movement_in_direction_y: Float ) { object_center_point[ object_center_point.x + movement_in_direction_x ] = object_center_point.y + movement_in_direction_y } fun move_to_position( new_position_x: Float, new_position_y: Float ) { object_center_point[ new_position_x ] = new_position_y } } internal open class Bouncer( given_position: PointF, given_color: Int, val bouncing_area: RectF ) : GraphicalObject( given_position, given_color ) { var bouncer_radius = 60F // bouncer_direction is an angle in radians. This angle specifies // the direction where the bouncer will be moved next. var bouncer_direction = Math.random() * Math.PI * 2 fun get_bouncer_radius(): Float { return bouncer_radius } fun shrink() { // The if-construct ensures that the ball does not become // too small. if ( bouncer_radius > 5 ) { bouncer_radius -= 3f } } fun enlarge() { bouncer_radius = bouncer_radius + 3 } fun set_radius( new_radius : Float ) { if ( new_radius > 3 ) { bouncer_radius = new_radius } } fun contains_point( given_point: PointF ) : Boolean { // Here we use the Pythagorean theorem to calculate the distance // from the given point to the center point of the ball. // See the note at the end of this file. val distance_from_given_point_to_ball_center = Math.sqrt( Math.pow( object_center_point.x - given_point.x.toDouble(), 2.0) + Math.pow( object_center_point.y - given_point.y.toDouble(), 2.0)).toFloat() return distance_from_given_point_to_ball_center <= bouncer_radius } // The move() method is supposed to be called something like // 25 times a second. open fun move() { // In the following statement a minus sign is needed when the // y coordinate is calculated. The reason for this is that the // y direction in the graphical coordinate system is 'upside down'. object_center_point.set( object_center_point.x + object_velocity * Math.cos( bouncer_direction ).toFloat(), object_center_point.y - object_velocity * Math.sin( bouncer_direction ).toFloat() ) // The following would be an alternative Kotlin statement to the call to set() above. //object_center_point[object_center_point.x + // object_velocity * Math.cos( bouncer_direction ).toFloat()] = object_center_point.y - // object_velocity * Math.sin( bouncer_direction ).toFloat() // Now, after we have moved this bouncer, we start finding out whether // or not it has hit a wall or some other obstacle. If a hit occurs, // a new direction for the bouncer must be calculated. // The following four if constructs must be four separate ifs. // If they are replaced with an if - else if - else if - else if // construct, the program will not work when the bouncer enters // a corner in an angle of 45 degrees (i.e. Math.PI / 4). if ( object_center_point.y - bouncer_radius <= bouncing_area.top ) { // The bouncer has hit the northern 'wall' of the bouncing area. bouncer_direction = 2 * Math.PI - bouncer_direction } if ( object_center_point.x - bouncer_radius <= bouncing_area.left ) { // The western wall has been reached. bouncer_direction = Math.PI - bouncer_direction } if ( object_center_point.y + bouncer_radius >= bouncing_area.bottom ) { // Southern wall has been reached. bouncer_direction = 2 * Math.PI - bouncer_direction } if ( object_center_point.x + bouncer_radius >= bouncing_area.right ) { // Eastern wall reached. bouncer_direction = Math.PI - bouncer_direction } } open fun draw(canvas: Canvas) { val filling_paint = Paint() filling_paint.style = Paint.Style.FILL filling_paint.color = object_color val outline_paint = Paint() outline_paint.style = Paint.Style.STROKE // Default color for a Paint is black. canvas.drawCircle( object_center_point.x, object_center_point.y, bouncer_radius, filling_paint) canvas.drawCircle( object_center_point.x, object_center_point.y, bouncer_radius, outline_paint) } } internal open class RotatingBouncer( given_position: PointF, given_color: Int, given_bouncing_area: RectF ) : Bouncer( given_position, given_color, given_bouncing_area ) { var current_rotation = 0 var another_ball_paint = Paint() init { another_ball_paint.color = -0xff8100 // dark green } override fun move() { super.move() // run the corresponding upper class method first current_rotation = current_rotation + 2 if ( current_rotation >= 360 ) { current_rotation = 0 } } override fun draw(canvas: Canvas) { super.draw(canvas) // run the upper class draw() first canvas.save() // Save the original canvas state // First we move the zero point of the coordinate system into // the center point of the ball. canvas.translate( object_center_point.x, object_center_point.y ) // Rotate the coordinate system as much as is the value of // the data field current_rotation. canvas.rotate( current_rotation.toFloat() ) // Fill one quarter of the ball with another color. canvas.drawArc( RectF( -bouncer_radius, -bouncer_radius, bouncer_radius, bouncer_radius), 0f, 90f, true, another_ball_paint ) // Fill another quarter of the ball with the new color. canvas.drawArc( RectF( -bouncer_radius, -bouncer_radius, bouncer_radius, bouncer_radius), 180f, 90f, true, another_ball_paint ) // Finally we restore the original coordinate system. canvas.restore() } } internal class ExplodingBouncer( given_position: PointF, given_color: Int, given_bouncing_area: RectF ) : RotatingBouncer( given_position, given_color, given_bouncing_area ) { companion object { const val BALL_ALIVE_AND_WELL = 0 const val BALL_EXPLODING = 1 const val BALL_EXPLODED = 2 } var ball_state = BALL_ALIVE_AND_WELL var explosion_color_alpha_value = 0 fun explode_ball() { ball_state = BALL_EXPLODING enlarge() // make the ball somewhat larger in explosion enlarge() } fun is_exploded() : Boolean { return ball_state == BALL_EXPLODED } override fun move() { // The ball will not move if it is exploding or exploded. if ( ball_state == BALL_ALIVE_AND_WELL ) { super.move() // move the ball with the superclass method } } override fun draw(canvas: Canvas) { if ( ball_state == BALL_ALIVE_AND_WELL ) { super.draw(canvas) // run the upper class draw() first } else if ( ball_state == BALL_EXPLODING ) { if ( explosion_color_alpha_value > 0xFF ) { ball_state = BALL_EXPLODED } else { // The ball will be 'exploded' by drawing a transparent // yellow ball over the original ball. // As the opaqueness of the yellow color gradually increases, // the ball becomes ultimately completely yellow in // the final stage of the explosion. super.draw(canvas) // draw the original ball first val explosion_paint = Paint() explosion_paint.color = Color.YELLOW explosion_paint.alpha = explosion_color_alpha_value explosion_paint.style = Paint.Style.FILL canvas.drawCircle( object_center_point.x, object_center_point.y, bouncer_radius, explosion_paint ) explosion_color_alpha_value += 4 // decrease transparency } } } } class BouncingBallView( context: Context? ) : View( context ), Runnable { var thread_that_moves_the_ball: Thread? = null var thread_must_be_executed = false var view_width = 0F var view_height = 0F private var ball_on_screen: ExplodingBouncer? = null init { setBackgroundColor( -0xf0701 ) // AliceBlue, very light blue } 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) { view_width = current_width_of_this_view.toFloat() view_height = current_height_of_this_view.toFloat() val bouncing_area = RectF( 0F, 0F, view_width, view_height ) ball_on_screen = ExplodingBouncer( PointF( view_width / 2 , view_height / 2 ), Color.GREEN, bouncing_area ) } fun start_animation_thread() { if ( thread_that_moves_the_ball == null ) { thread_must_be_executed = true thread_that_moves_the_ball = Thread( this ) thread_that_moves_the_ball!!.start() } } fun stop_animation_thread() { if ( thread_that_moves_the_ball != null ) { thread_must_be_executed = false thread_that_moves_the_ball!!.interrupt() thread_that_moves_the_ball = null } } override fun run() { while ( thread_must_be_executed == true ) { if ( ball_on_screen != null ) { ball_on_screen!!.move() } postInvalidate() try { Thread.sleep( 40 ) } catch ( caught_exception: InterruptedException ) { // No actions to handle the exception. } } } override fun onTouchEvent( motion_event: MotionEvent ) : Boolean { if ( motion_event.action == MotionEvent.ACTION_DOWN ) { // When the screen is touched we'll create a new ball to the touched // point if the old ball has been exploded. val touched_point = PointF( motion_event.x, motion_event.y ) if ( ball_on_screen!!.is_exploded() ) { val bouncing_area = RectF( 0F, 0F, view_width, view_height ) ball_on_screen = ExplodingBouncer( touched_point, Color.GREEN, bouncing_area ) } else if ( ball_on_screen!!.contains_point( touched_point ) ) { ball_on_screen!!.explode_ball() } } return true } override fun onDraw(canvas: Canvas) { if ( ball_on_screen != null ) { ball_on_screen!!.draw( canvas ) } } } class BouncingBallActivity : Activity() { var bouncing_ball_view: BouncingBallView? = null public override fun onCreate( savedInstanceState: Bundle? ) { super.onCreate( savedInstanceState ) bouncing_ball_view = BouncingBallView( this ) setContentView( bouncing_ball_view ) } public override fun onStart() { super.onStart() bouncing_ball_view!!.start_animation_thread() } public override fun onStop() { super.onStop() bouncing_ball_view!!.stop_animation_thread() } }