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 чтобы включить двойную буферизацию для этой панели, но оказывается она включена по умолчанию. 

707 10
Alisher Alikulov