Pull to refresh

JavaFX и AnimationTimer

Reading time6 min
Views21K
CountdownВариант применения интересного класса в JavaFX-приложении — предотвращение «заморозки» окна во время длительного процесса. Заодно чуть-чуть об особенностях прорисовки сцен в JavaFX.

Предположим, появилось желание (и/или необходимость) настрогать десктопное приложение на Java с отображением длительного процесса. Ну, мало ли, есть долгий цикл вычислений, и охота посмотреть результат каждой итерации, хотя бы мельком. Убедиться, что процесс идет в нужном направлении, может, гистограмму какую построить.

Предположим, интерфейс решено делать на JavaFX. Как-нибудь так:
файл Main.java
package romeogolf.example1;

import java.util.ArrayList;

import javafx.animation.AnimationTimer;
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;

public class Main extends Application {
	// массив знакомест
	ArrayList<Label> aLabels = new ArrayList<Label>();
	// число знакомест
	final private int digitCount = 5; // цифр будет + 1
	// ограничение процесса
	final private int maxCount = 12345;
	
	public static void main(String[] args) {
	    launch(args);
	}

	@Override
	public void start(Stage primaryStage) {
		primaryStage.setTitle("Example");
		VBox vbox = new VBox();
		vbox.setAlignment(Pos.CENTER);
		vbox.setSpacing(20);
		HBox hbox = new HBox();
		hbox.setAlignment(Pos.CENTER);
		hbox.setSpacing(20);
		for(int i = 0; i <= digitCount; i++){
			aLabels.add(new Label("X"));
			aLabels.get(i).setFont(new Font("Arial", 30));
			aLabels.get(i).setStyle("-fx-padding: 5;"
					+ "-fx-border-color: rgb(49, 89, 23);"
					+ "-fx-border-radius: 5;");
			hbox.getChildren().add(aLabels.get(i));
		}
		// кнопка, запускающая процесс
		Button button = new Button("Start");
		button.setOnAction(new EventHandler<ActionEvent>() {
			@Override public void handle(ActionEvent e) {
				longProcess();
			}
		});

		vbox.getChildren().add(hbox);
		vbox.getChildren().add(button);

		primaryStage.setScene(new Scene(vbox, 55 * (digitCount + 1), 100));
		primaryStage.show();
	}

	// заготовка для процесса
	private void longProcess(){
		// заготовка
	}
}

Окошко

Получится такое окошко, которое пока ничего не умеет. В тестовых целях добавлю-ка туда дико сложные вычисления в виде инкремента переменной-счетчика. Метод longProcess тогда будет выглядеть, допустим, так:
	private void longProcess(){
		int digit;
		for(int i = 0; i <= this.maxCount; i++){
			for(int j = 0; j <= digitCount; j++){
				digit = (int) (i % (Math.pow(10.0, (double)(j + 1))));
				digit = (int) (digit / (Math.pow(10.0, (double)j)));
				this.aLabels.get(digitCount - j).setText(Integer.toString(digit));
			}
		}
	}

Здесь некое число i незамысловато разбирается на цифры и выводится в знакоместа. Запустим это творение безумного программистского гения и нажмем единственную кнопку. Очень быстро получим результат, почти мгновенно:

Результат

Очень радует, что так быстро. Но незадача в том, что задача не в этом. Целью-то было посмотреть весь процесс, хотя бы и медленно. Вот бы как-то притормозить каждый проход цикла и дать команду отобразить результат… Что-то типа repaint() в Swing, или какие-нибудь refresh, update, может, дельфовое Appllication.ProcessMessages.

Не предусмотрено. В Swing при вызове перерисовки она тут же и осуществляется. В JavaFX требование перерисовки отражается в графе сцены, а сцена перерисовывается, когда придет время, когда тикнет pulse. Pulse — это событие, которое сообщает графу сцены, что пришла пора рисоваться. Оно тикает не чаще 60 fps, если запущена анимация, и по необходимости, если в графе сцены произошли изменения. Это так называемый Retained Mode, что нередко переводят, как «абстракный режим», подразумевая, что данный подход повышает уровень абстракции, в отличие от Immediate Mode — непосредственного режима.

Статья Retained Mode Versus Immediate Mode описывает разницу между подходами на примере Direct2D и WPF. Переведу последний абзац:
Retained Mode API проще в использовании, потому что это API выполняет за вас больше работы: инициализацию, поддержание состояния, очистку. Но с другой стороны, такой подход зачастую менее гибок, так как это API навязывает свою собственную модель сцены. Кроме того, Retained Mode API может иметь повышенные требования к памяти, так как это необходимо для обеспечения модели сцены общего назначения. Используя Immediate Mode API вы можете осуществить целевую оптимизацию.

Что же делать? Попробуем так. Вынесем счетчик из переменной цикла, сделаем ее полем класса (допустим, прямо перед новым методом):
	private int counter = 0;

Сделаем новый метод чуть иначе (можно просто изменить старый, но если оставим, то будет проще переключаться между старым и новым вариантами). Уберем внешний цикл совсем:
	private void longProcess2(){
		int digit;
		// операции для отображения процесса
		for(int j = 0; j <= digitCount; j++){
			digit = (int) (counter % (Math.pow(10.0, (double)(j + 1))));
			digit = (int) (digit / (Math.pow(10.0, (double)j)));
			this.aLabels.get(digitCount - j).setText(Integer.toString(digit));
		}
		// *******
		// собственно процесс, подлежащий отображению:
		counter++;
		// *******
	}

Добавим в конце класса Main такой код:
    protected AnimationTimer at = new AnimationTimer(){
        @Override
        public void handle(long now) {
        	longProcess2();
        }
    };

Для запуска процесса в обработке кнопки вместо
				longProcess();

поставим
				at.start();

А для своевременной остановки вставим в сам метод longProcess2(), реализующий процесс, такие строчки (в самое начало):
		if(counter > maxCount){
			at.stop();
			return;
		}

Теперь о том, зачем это все было нужно. В результате создается at — экземпляр класса AnimationTimer. Его метод handle() вызывается каждый раз при перерисовке окна приложения. То есть, вызвав longProcess2(), мы скомандовали перерисовать знакоместа цифр. Вызвана необходимость перерисовки сцены. Вызывается handle(), и снова запускает longProcess2(). И так до тех пор, пока в самом методе longProcess2() не возникнет условие остановки для at.stop().

Теперь при нажатии кнопки в «окошках» будут мелькать циферки. Мелькать с разной скоростью, в зависимости от цены разряда.

Процесс

Ежели, к примеру, увеличить digitCount до 7, а maxCount — до 98765432, разница между вариантами без таймера и с таймером станет существенно заметнее. В начальном варианте придется созерцать крестики в знакоместах до тех пор, пока процесс не будет завершен, и даже закрыть окно традиционным способом («крестиком» в правом верхнем углу) не получится. В варианте с таймером чудесно отображается процесс, окошко можно таскать за заголовок и закрыть в любой момент, но сам процесс растягивется неимоверно. Тут уж вопрос, что больше нужно — скорость или отображение.

Вообще-то, скорость можно несколько увеличить, пропуская отображение отдельных итераций, и чем больше пропускаем, тем быстрее закончим, хотя и меньше увидим. Для этого можно вставить в longProcess2(), в самый конец, вместо
		counter++;

что-то типа
		for(int skip = 0; skip < 100000; skip++){
			if(counter >= maxCount){
				break;
			}
			counter++;
		}

Тогда будет отображаться результат каждой стотысячной итерации, а благодаря условию остановки в конце высветится финальный результат. Правда, в процессе младшие разряды будут тупо нулями, но для наглядности их можно тоже как-нибудь размыть. Впрочем, это детали из области свистелок, к примеру отношения не имеет.

Можно было бы еще добавить обнуление счетчика перед каждым запуском таймера, недопустимость вторичного запуска таймера при уже работающем (а это возможно!) и еще какие-нибудь полезные свистелки, но для демонстрации примера это перебор.

Немного об AnimationTimer: некто Mike в своем блоге написал о нем очень неплохую статью Using the JavaFX AnimationTimer.

Майк считает, что было не особо хорошей идеей так назвать этот класс. Ведь его можно использовать далеко не только для анимации: для измерения fps-rate, для обнаружения коллизий, для подсчета шагов моделирования, в качестве основного цикла в играх и т. д. Лично он чаще всего видит применения AnimationTimer, вообще не имеющие отношения к анимации. AnimationTimer дает чрезвычайно простую, но очень гибкую и полезную фишку. Таймер позволяет определить метод, который будет вызываться для каждого кадра. Что этот метод будет делать, не только не ограничено, но, как упомянуто выше, может не иметь ничего общего с анимацией. Единственное требование — этот метод должен быть достаточно быстрым, иначе он просто станет узким местом в системе.

Конечно, существует не единственный способ решения такой задачи, но мне понравился этот таймер — просто и довольно удобно. Кто не был с ним знаком — прошу любить и жаловать.
Tags:
Hubs:
+6
Comments3

Articles