I'm learning to work with java threads, so I decided to make a simple bouncing balls program. However, the program shows multiple threads but only one takes advantage of the window size, other balls are restricted to one area.
I tried setting the size for each balls' JPanel and different layouts which didn't work.
BouncingBall.java
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import javax.swing.*;
public class BouncingBall extends JFrame {
ArrayList<Ball> balls = new ArrayList<Ball>();
//GUI Elements
JLabel lblCount;
JButton btn= new JButton("Stop");
BouncingBall() {
// setDefaultLookAndFeelDecorated(true);
setTitle("BouncingBall");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300, 200);
for (int i = 0; i < 5; i++) {
balls.add(new Ball());
}
setLayout(new FlowLayout());
setContentPane(balls.get(0));
balls.get(0).init();
for (Ball b : balls
) {
System.out.println(b.getHeight());
if (b != balls.get(0)) {
b.init();
balls.get(0).add(b);
}
}
this.add(btn,BorderLayout.SOUTH);
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
for (Ball b :balls
) {
b.stopMoving();
}
}
});
addMouseListener(new MouseListener() {
@Override
public void mouseClicked(MouseEvent e) {
for (Ball b :balls
) {
b.startMoving();
}
}
@Override
public void mousePressed(MouseEvent e) {
}
@Override
public void mouseReleased(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
});
this.setVisible(true);
}
public static void main(String[] args) {
new BouncingBall();
}
}
Ball.java
import javax.swing.*;
import java.awt.*;
public class Ball extends JPanel implements Runnable {
// Box height and width
int width;
int height;
// Ball Size
float radius = 5;
float diameter = radius * 2;
// Center of Call
float X = radius + 50;
float Y = radius + 20;
// Direction
float dx;
float dy;
//Vars
int count = 0;
float[] colorHSB = new float[3];
boolean moving = false;
//Thread
Thread t;
Ball() {
dx = (float) Math.random() * 10;
dy = (float) Math.random() * 10;
width = getWidth();
height = getHeight();
for (int i = 0; i < 3; i++) {
colorHSB[i] = (float) Math.random() * 255;
}
t = new Thread(this);
}
void init() {
t.start();
}
public void run() {
while (true) {
width = getWidth();
height = getHeight();
if (moving){
X = X + dx;
Y = Y + dy;
}
if (X - radius < 0) {
dx = -dx;
X = radius;
addCount();
} else if (X + radius > width) {
dx = -dx;
X = width - radius;
addCount();
}
if (Y - radius < 0) {
dy = -dy;
Y = radius;
addCount();
} else if ((Y + radius) > height) {
dy = -dy;
Y = height - radius;
addCount();
}
repaint();
try {
Thread.sleep(50);
} catch (InterruptedException ex) {
}
}
}
public void startMoving() {
moving = true;
}
public void stopMoving(){
moving=false;
}
public void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.getHSBColor(colorHSB[0], colorHSB[1], colorHSB[2]));
g.fillOval((int) (X - radius), (int) (Y - radius), (int) diameter, (int) diameter);
}
public void addCount() {
count++;
System.out.println(count);
}
}
A photo of the program running
it should show all the balls bouncing around the frame taking advantage of the whole window.
My answer is based on the MCV model. This splits responsibilities between the Model, the View, and the Controller.
Each one (M,V and C) becomes a well defined single-responsibility class.
At first the number of classes, and the relations between them may look puzzling. After studying and understanding the structure you realize that it actually divides the "problem" you are trying to solve into smaller and easier to handle parts.
The ball can be a simple example of a Model. It is actually a pojo that holds all the information the view needs to draw a ball:
//a model representing ball
class Ball {
//Ball attributes
private static final int SIZE = 10; //diameter
private int x, y; // Position
private final Color color;
private Observer observer; //to be notified on changes
Ball() {
Random rnd = new Random();
color = new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
}
Color getColor() {
return color;
}
int getSize(){
return SIZE;
}
synchronized int getX() {
return x;
}
synchronized void setX(int x) {
this.x = x;
notifyObserver();
}
synchronized int getY() {
return y;
}
synchronized void setY(int y) {
this.y = y;
notifyObserver();
}
void registerObserver(Observer observer){
this.observer = observer;
}
void notifyObserver(){
if(observer == null) return;
observer.onObservableChanged();
}
}
Note that you can register an Observer
to a Ball
. An Observer
is defined by:
//listening interface. Implemented by View and used by Ball to notify changes
interface Observer {
void onObservableChanged();
}
It is used by a Ball
to notify the observer that a change has occurred.
A Ball
has also some synchronized
getters and setters so it attributes can be accesses by more than one thread.
We should also define a Model
, another pojo which is the class that encapsulates all the information the view needs:
//view model: hold info that view needs
class Model {
private final ArrayList<Ball> balls;
private final int width, height;
Model(){
balls = new ArrayList<>();
width = 300; height = 200;
}
boolean addBall(Ball ball){
return balls.add(ball);
}
List<Ball> getBalls() {
return new ArrayList<>(balls); //return a copy of balls
}
int getWidth() {
return width;
}
int getHeight() {
return height;
}
}
A View
, as its name suggests is just that:
class View {
private final BallsPane ballsPane;
View(Model model){
ballsPane = new BallsPane(model);
}
void createAndShowGui(){
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.add(ballsPane);
frame.pack();
frame.setResizable(false);
frame.setVisible(true);
}
Observer getObserver(){
return ballsPane;
}
}
class BallsPane extends JPanel implements Observer {
private final Model model;
BallsPane(Model model){
this.model = model;
setPreferredSize(new Dimension(model.getWidth(), model.getHeight()));
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
for(Ball b : model.getBalls()){
g.setColor(b.getColor());
g.fillOval(b.getX(), b.getY(), b.getSize(), b.getSize());
}
}
@Override
public void onObservableChanged() {
repaint(); //when a change was notified
}
}
Note that the View
(actually BallsPane
) implements Observer
. It will observe (or listen to) changes in Ball
, and respond to every change by invoking repaint()
.
Since each Ball
has synchronized
position (x,y) getters and setters, you can alter those attributes:
class BallAnimator implements Runnable{
private final Ball ball;
private final int maxX, maxY;
private final Random rnd;
private boolean moveRight = true, moveDown = true;
private static final int STEP =1, WAIT = 40;
BallAnimator(Ball ball, int maxX, int maxY) {
this.ball = ball;
this.maxX = maxX;
this.maxY = maxY;
rnd = new Random();
ball.setX(rnd.nextInt(maxX - ball.getSize()));
ball.setY(rnd.nextInt(maxY - ball.getSize()));
new Thread(this).start();
}
@Override
public void run() {
while(true){
int dx = moveRight ? STEP : -STEP ;
int dy = moveDown ? STEP : -STEP ;
int newX = ball.getX() + dx;
int newY = ball.getY() + dy;
if(newX + ball.getSize()>= maxX || newX <= 0){
newX = ball.getX() - dx;
moveRight = ! moveRight;
}
if(newY +ball.getSize()>= maxY || newY <= 0){
newY = ball.getY() - dy;
moveDown = ! moveDown;
}
ball.setX(newX);
ball.setY(newY);
try {
Thread.sleep(WAIT);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
Adding a Controller and putting it all together: BouncingBalls
acts as the controller. It "wires" the different parts of the solution .
For convenience and simplicity, the entire following code can be copy-pasted into one file called BouncingBalls.java, and run.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class BouncingBalls{
BouncingBalls(int numOfBalls) {
Model model = new Model();
View view = new View(model);;
for (int i = 0; i < numOfBalls; i++) {
Ball b = new Ball(); //construct a ball
model.addBall(b); //add it to the model
b.registerObserver(view.getObserver()); //register view as an observer to it
new BallAnimator(b, model.getWidth(), model.getHeight()); //start a thread to update it
}
view.createAndShowGui();
}
public static void main(String[] args) {
new BouncingBalls(5);
}
}
//listening interface. Implemented by View and used by Ball to notify changes
interface Observer {
void onObservableChanged();
}
class View {
private final BallsPane ballsPane;
View(Model model){
ballsPane = new BallsPane(model);
}
void createAndShowGui(){
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.add(ballsPane);
frame.pack();
frame.setResizable(false);
frame.setVisible(true);
}
Observer getObserver(){
return ballsPane;
}
}
class BallsPane extends JPanel implements Observer {
private final Model model;
BallsPane(Model model){
this.model = model;
setPreferredSize(new Dimension(model.getWidth(), model.getHeight()));
}
@Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
for(Ball b : model.getBalls()){
g.setColor(b.getColor());
g.fillOval(b.getX(), b.getY(), b.getSize(), b.getSize());
}
}
@Override
public void onObservableChanged() {
repaint(); //when a change was notified
}
}
//view model: hold info that view needs
class Model {
private final ArrayList<Ball> balls;
private final int width, height;
Model(){
balls = new ArrayList<>();
width = 300; height = 200;
}
boolean addBall(Ball ball){
return balls.add(ball);
}
List<Ball> getBalls() {
return new ArrayList<>(balls); //return a copy of balls
}
int getWidth() {
return width;
}
int getHeight() {
return height;
}
}
//a model representing ball
class Ball {
//Ball attributes
private static final int SIZE = 10; //diameter
private int x, y; // Position
private final Color color;
private Observer observer; //to be notified on changes
Ball() {
Random rnd = new Random();
color = new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
}
Color getColor() {
return color;
}
int getSize(){
return SIZE;
}
synchronized int getX() {
return x;
}
synchronized void setX(int x) {
this.x = x;
notifyObserver();
}
synchronized int getY() {
return y;
}
synchronized void setY(int y) {
this.y = y;
notifyObserver();
}
void registerObserver(Observer observer){
this.observer = observer;
}
void notifyObserver(){
if(observer == null) return;
observer.onObservableChanged();
}
}
class BallAnimator implements Runnable{
private final Ball ball;
private final int maxX, maxY;
private final Random rnd;
private boolean moveRight = true, moveDown = true;
private static final int STEP =1, WAIT = 40;
BallAnimator(Ball ball, int maxX, int maxY) {
this.ball = ball;
this.maxX = maxX;
this.maxY = maxY;
rnd = new Random();
ball.setX(rnd.nextInt(maxX - ball.getSize()));
ball.setY(rnd.nextInt(maxY - ball.getSize()));
new Thread(this).start();
}
@Override
public void run() {
while(true){
int dx = moveRight ? STEP : -STEP ;
int dy = moveDown ? STEP : -STEP ;
int newX = ball.getX() + dx;
int newY = ball.getY() + dy;
if(newX + ball.getSize()>= maxX || newX <= 0){
newX = ball.getX() - dx;
moveRight = ! moveRight;
}
if(newY +ball.getSize()>= maxY || newY <= 0){
newY = ball.getY() - dy;
moveDown = ! moveDown;
}
ball.setX(newX);
ball.setY(newY);
try {
Thread.sleep(WAIT);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}