JavaFX і AnimationTimer

  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 дає надзвичайно просту, але дуже гнучку і корисну фішку. Таймер дозволяє визначити метод, який буде викликатися для кожного кадру. Що цей метод буде робити, не тільки не обмежена, але, як згадано вище, може не мати нічого спільного з анімацією. Єдина вимога — цей метод має бути досить швидким, інакше він просто стане вузьким місцем в системі.
 
Звичайно, існує не єдиний спосіб вирішення такого завдання, але мені сподобався цей таймер — просто і досить зручно. Хто не був з ним знайомий — прошу любити і жалувати.
  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.