// BouncingBallsThreeActivity.java (c) Kari Laitinen // http://www.naturalprogramming.com // 2020-10-15 File created. // /* This is a modified version of BouncingBallActivity.java. Here balls can bounce also when they hit each other. The movements of the balls are not perfect. It is possible that the balls get jammed in corners, etc. */ package bouncing.balls.three ; import android.app.Activity ; import android.os.Bundle ; import android.graphics.* ; // Classes Canvas, Color, Paint, RectF, etc. import android.view.View ; import android.view.MotionEvent ; import android.view.GestureDetector ; import android.content.Context ; import java.util.ArrayList ; class GraphicalObject { PointF object_center_point ; // object_velocity specifies the number of pixels the object // will be moved in a single movement operation. float object_velocity = 4.0F ; int object_color = Color.RED ; public PointF get_object_position() { return object_center_point ; } public void set_color( int new_color ) { object_color = new_color ; } public void move_right() { object_center_point.set( object_center_point.x + object_velocity, object_center_point.y ) ; } public void move_left() { object_center_point.set( object_center_point.x - object_velocity, object_center_point.y ) ; } public void move_up() { object_center_point.set( object_center_point.x, object_center_point.y - object_velocity); } public void move_down() { object_center_point.set( object_center_point.x, object_center_point.y + object_velocity); } public void move_this_object( float movement_in_direction_x, float movement_in_direction_y ) { object_center_point.set( object_center_point.x + movement_in_direction_x, object_center_point.y + movement_in_direction_y ) ; } public void move_to_position( float new_position_x, float new_position_y ) { object_center_point.set( new_position_x, new_position_y ) ; } } // Balls are stored in the following static array which is accessed // both from the Bouncer class and from the 'main' program. class BallStore { static ArrayList all_balls = new ArrayList() ; } class Bouncer extends GraphicalObject { float bouncer_radius = 60 ; // bouncer_direction is an angle in radians. This angle specifies // the direction where the bouncer will be moved next. float bouncer_direction = (float) ( Math.random() * Math.PI * 2 ); RectF bouncing_area ; public Bouncer( PointF given_position, int given_color, RectF given_bouncing_area ) { object_center_point = given_position ; object_color = given_color ; bouncing_area = given_bouncing_area ; } public float get_bouncer_radius() { return bouncer_radius ; } public void shrink() { // The if-construct ensures that the ball does not become // too small. if ( bouncer_radius > 5 ) { bouncer_radius -= 3 ; } } public void enlarge() { bouncer_radius = bouncer_radius + 3 ; } public void set_radius( int new_radius ) { if ( new_radius > 3 ) { bouncer_radius = (float) new_radius ; } } public boolean contains_point( PointF given_point ) { // 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. float distance_from_given_point_to_ball_center = (float) Math.sqrt( Math.pow( object_center_point.x - given_point.x, 2 ) + Math.pow( object_center_point.y - given_point.y, 2 ) ) ; return ( distance_from_given_point_to_ball_center <= bouncer_radius ) ; } public boolean collides_with( Bouncer another_bouncer ) { // 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. float distance_between_center_points = (float) Math.sqrt( Math.pow( object_center_point.x - another_bouncer.object_center_point.x, 2 ) + Math.pow( object_center_point.y - another_bouncer.object_center_point.y, 2 ) ) ; return ( distance_between_center_points <= bouncer_radius + another_bouncer.bouncer_radius ) ; } // The move() method is supposed to be called something like // 25 times a second. public void 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 * (float)Math.cos(bouncer_direction), object_center_point.y - object_velocity * (float)Math.sin(bouncer_direction)); // First we check if other balls are hit. for ( Bouncer bouncer : BallStore.all_balls ) { if ( this != bouncer && collides_with( bouncer ) == true ) { if ( bouncer_direction >= (float) Math.PI ) { bouncer_direction = bouncer_direction - (float) Math.PI ; } else { bouncer_direction = bouncer_direction + (float) Math.PI ; } } } // Then 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 * (float) Math.PI - bouncer_direction ; } if ( object_center_point.x - bouncer_radius <= bouncing_area.left ) { // The western wall has been reached. bouncer_direction = (float) Math.PI - bouncer_direction ; } if ( object_center_point.y + bouncer_radius >= bouncing_area.bottom ) { // Southern wall has been reached. bouncer_direction = 2 * (float) Math.PI - bouncer_direction ; } if ( object_center_point.x + bouncer_radius >= bouncing_area.right ) { // Eastern wall reached. bouncer_direction = (float) Math.PI - bouncer_direction ; } } public void draw( Canvas canvas ) { Paint filling_paint = new Paint() ; filling_paint.setStyle( Paint.Style.FILL ) ; filling_paint.setColor( object_color ) ; Paint outline_paint = new Paint() ; outline_paint.setStyle( 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 ) ; } } class RotatingBouncer extends Bouncer { int current_rotation = 0 ; Paint another_ball_paint = new Paint() ; public RotatingBouncer( PointF given_position, int given_color, RectF given_bouncing_area ) { super( given_position, given_color, given_bouncing_area ) ; another_ball_paint.setColor( 0xFF007F00 ) ; // dark green } public void move() { super.move() ; // run the corresponding upper class method first current_rotation = current_rotation + 2 ; if ( current_rotation >= 360 ) { current_rotation = 0 ; } } public void 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 ) ; // Fill one quarter of the ball with another color. canvas.drawArc( new RectF( -bouncer_radius, -bouncer_radius, bouncer_radius, bouncer_radius ), 0, 90, true, another_ball_paint ) ; // Fill another quarter of the ball with the new color. canvas.drawArc( new RectF( -bouncer_radius, -bouncer_radius, bouncer_radius, bouncer_radius ), 180, 90, true, another_ball_paint ) ; // Finally we restore the original coordinate system. canvas.restore() ; } } class ExplodingBouncer extends RotatingBouncer { static final int BALL_ALIVE_AND_WELL = 0 ; static final int BALL_EXPLODING = 1 ; static final int BALL_EXPLODED = 2 ; int ball_state = BALL_ALIVE_AND_WELL ; int explosion_color_alpha_value = 0 ; public ExplodingBouncer( PointF given_position, int given_color, RectF given_bouncing_area ) { super( given_position, given_color, given_bouncing_area ) ; } public void explode_ball() { ball_state = BALL_EXPLODING ; enlarge() ; // make the ball somewhat larger in explosion enlarge() ; } public boolean is_exploded() { return ( ball_state == BALL_EXPLODED ) ; } public void 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 } } public void 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 Paint explosion_paint = new Paint() ; explosion_paint.setColor( Color.YELLOW ) ; explosion_paint.setAlpha( explosion_color_alpha_value ) ; explosion_paint.setStyle( 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 BouncingBallsView extends View implements Runnable { Thread thread_that_moves_the_ball ; boolean thread_must_be_executed ; int view_width, view_height ; ExplodingBouncer ball_on_screen ; public BouncingBallsView( Context context ) { super( context ) ; setBackgroundColor( 0xFFF0F8FF ) ; // AliceBlue, very light blue } public void onSizeChanged( int current_width_of_this_view, int current_height_of_this_view, int old_width_of_this_view, int old_height_of_this_view ) { view_width = current_width_of_this_view ; view_height = current_height_of_this_view ; RectF bouncing_area = new RectF( 0, 0, view_width, view_height ) ; ball_on_screen = new ExplodingBouncer( new PointF( view_width / 2, view_height / 2 ), Color.GREEN, bouncing_area) ; BallStore.all_balls.add( new ExplodingBouncer( new PointF( view_width / 2, view_height / 4 ), Color.GREEN, bouncing_area ) ) ; BallStore.all_balls.add( new ExplodingBouncer( new PointF( view_width / 2, view_height / 2 ), Color.YELLOW, bouncing_area ) ) ; BallStore.all_balls.add( new ExplodingBouncer( new PointF( view_width / 2, view_height * 3 / 4 ), Color.CYAN, bouncing_area ) ) ; } public void start_animation_thread() { if ( thread_that_moves_the_ball == null ) { thread_must_be_executed = true ; thread_that_moves_the_ball = new Thread( this ) ; thread_that_moves_the_ball.start() ; } } public void 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 ; } } public void run() { while ( thread_must_be_executed == true ) { // Before moving the balls, we'll delete the exploded ones. int ball_index = 0 ; while ( ball_index < BallStore.all_balls.size() ) { ExplodingBouncer ball_to_check = (ExplodingBouncer) BallStore.all_balls.get( ball_index ) ; if ( ball_to_check.is_exploded() ) { BallStore.all_balls.remove( ball_to_check ) ; } ball_index ++ ; } for ( Bouncer bouncer : BallStore.all_balls ) { bouncer.move() ; } postInvalidate() ; try { Thread.sleep( 40 ) ; } catch ( InterruptedException caught_exception ) { // No actions to handle the exception. } } } public boolean onTouchEvent ( MotionEvent motion_event ) { if ( motion_event.getAction() == MotionEvent.ACTION_DOWN ) { // Only one bouncer should contain the touched point. boolean one_bouncer_was_set_to_explode = false ; for ( Bouncer bouncer : BallStore.all_balls ) { if ( bouncer.contains_point( new PointF( motion_event.getX(), motion_event.getY() ) ) ) { ((ExplodingBouncer) bouncer).explode_ball() ; one_bouncer_was_set_to_explode = true ; } } if ( one_bouncer_was_set_to_explode == false ) { // The backgronund was touched. Let's create a new ball. RectF bouncing_area = new RectF( 0, 0, view_width, view_height ) ; BallStore.all_balls.add( new ExplodingBouncer( new PointF( motion_event.getX(), motion_event.getY() ), Color.LTGRAY, bouncing_area ) ) ; } } return true ; } protected void onDraw( Canvas canvas ) { for ( Bouncer bouncer : BallStore.all_balls ) { bouncer.draw( canvas ) ; } } } public class BouncingBallsThreeActivity extends Activity { BouncingBallsView bouncing_ball_view; public void onCreate( Bundle savedInstanceState ) { super.onCreate( savedInstanceState ) ; bouncing_ball_view = new BouncingBallsView( this ) ; setContentView( bouncing_ball_view ) ; } public void onStart() { super.onStart() ; bouncing_ball_view.start_animation_thread() ; } public void onStop() { super.onStop() ; bouncing_ball_view.stop_animation_thread() ; } }