Java → Анимация и двойная буферизация при рисовании
В этой статье я создам простое приложение с анимацией. Если кто-то уже пробовал делать анимацию, наверное столкнулись с проблемой моргания при перерисовке. Я также покажу как избавиться от этого моргания используя двойную буфферизацию. Это значит что программа будет рисовать сначала не на экране, а в памяти, только потом будет перерисовывать на экране, и только те пиксели, которые изменились. Таким образом мы избавимся от морганию и сделаем анимацию гладкой и красивой.
Итак, приступим.
Создадим основной класс AnimationApp.
Этот класс будет основным, в котором будет решение задачи и также одновременно иметь точку входа в программу. Так как обычно метод main занимает всего 2 строчки, его сразу можно включить в тот класс, который он запускает.
public class AnimationApp {
public static void main(String[] args) {
AnimationApp app = new AnimationApp();
app.start();
}
private void start() {
}
}
у нас будет метода start() который запустит всю программу. Теперь добавим конструктор этому классу, который будет создавать JFrame, настроит его и установит ему AnimationPanel как основной контейнер. Класс AnimationPanel будет унаследован от класса JPanel и мы будем в нем рисовать анимацию.
import javax.swing.*;
public class AnimationApp {
private JFrame frame;
private AnimationPanel animationPanel;
public static void main(String[] args) {
AnimationApp app = new AnimationApp();
app.start();
}
public AnimationApp(){
frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 300);
animationPanel = new AnimationPanel();
frame.setContentPane(animationPanel);
}
private void start() {
frame.setVisible(true);
}
}
import javax.swing.*;
public class AnimationPanel extends JPanel {
}
Как анимацию возьмем прыгающий шарик. Для этого создадим класс Ball с тремя свойствами: цвет, радиус и высота от земли. Эти свойства мы будем использовать при рисовании этого шара. Для всех свойст создадим геттеры и сеттеры, чтобы все было в стиле ООП. Также у шарика должна быть текущая скорость. Скорость будет определять где окажется шарик в следующий момент, вниз или вверх летит.
import java.awt.*;
public class Ball {
private Color color = Color.YELLOW;
private int elevation = 0;
private int radius = 50;
private double speed = -10;
public void setElevation(int elevation) {
this.elevation = elevation;
}
public int getElevation() {
return elevation;
}
public void setColor(Color color) {
this.color = color;
}
public Color getColor() {
return color;
}
public int getRadius() {
return radius;
}
public void setRadius(int radius) {
this.radius = radius;
}
public double getSpeed() {
return speed;
}
public void setSpeed(double speed) {
this.speed = speed;
}
}
Теперь создадим один шарик и объявим его в классе AnimationApp. Также у класса AnimationPanel создадим метод setBall(), в котором передадим объект шарика в панель, чтобы он отрисовывал его. Затем запустим процесс анимации. Для этого будем использовать таймер, который будет запускать указанную задачу через каждые несколько миллисекунд. Для этого будем использовать классы TimerTask и Timer из пакета java.util (При импорте убедитесь что вы подключили именно те классы import java.util.Timer).
Теперь наш класс AnimationApp выглядит так.
import javax.swing.*;
import java.util.*;
import java.util.Timer;
public class AnimationApp {
private JFrame frame;
private AnimationPanel animationPanel;
private Ball ball;
public static void main(String[] args) {
AnimationApp app = new AnimationApp();
app.start();
}
public AnimationApp(){
frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(600, 300);
animationPanel = new AnimationPanel();
frame.setContentPane(animationPanel);
}
private void start() {
frame.setVisible(true);
ball = new Ball();
animationPanel.setBall(ball);
animateBall();
}
private void animateBall() {
TimerTask task = new TimerTask() {
@Override
public void run() {
ball.move();
animationPanel.repaint();
}
};
Timer timer = new Timer();
timer.schedule(task, 0, 10);
}
}
в методе animateBall() создается задача TimerTask в котором мы переопределяем метод run(), который будет выполняться повторно пока таймер работает. После этого создаем таймер и отдаем ему задачу, чтобы он ее выполнял каждые 10 миллисекунд.
Внутри метода run() мы вызываем метод move() у шарика, чтобы шарик поменял свою позицию. Потом перерисовываем панель. Создаем еще не существующий метод move() для шарика в котором мы будем менять высоту шарика с его скоростью и когда шарик будет доходить до 250, менять скорость обратно, чтобы он летел теперь вниз. И когда шарик доходит до 0, снова менять его скорость чтобы он начал лететь вверх.
public void move() {
elevation += speed;
if(elevation > 250 && speed > 0){
speed = -speed;
}
if(elevation < 0){
elevation = 0;
}
if(elevation == 0 && speed < 0){
speed = -speed;
}
}
Этот метод внутри класса Ball.
Теперь изменим класс AnimationPanel, чтобы он рисовал шарик, который мы ему передали.
import javax.swing.*;
import java.awt.*;
public class AnimationPanel extends JPanel {
private Ball ball;
@Override
public void paint(Graphics g) {
super.paint(g);
setDoubleBuffered(true);
g.setColor(ball.getColor());
g.fillOval(300, ball.getElevation(), ball.getRadius(), ball.getRadius());
}
public void setBall(Ball ball) {
this.ball = ball;
}
}
Все готово. Запускаем класс AnimationApp и видим как прыгает шарик. Этот пример очень сырой, но его достаточно чтобы вы научились делать анимации. Удачи!
Я вызвал метод setDoubleBuffered(true) внутри класс AnimationPanel чтобы включить двойную буферизацию для этой панели, но оказывается она включена по умолчанию.