I need to bind a variable that changes from my thread to Property from JavaFx. In theory, this should allow me to bind my player to the controls from JavaFx. I even wrote a wrapper for Property, but I still have the feeling that something is wrong, maybe I reinvent the wheel. It’s impossible to set / get the Property value from a JavaFx stream, and Platform.runLater / ChangeEvent should be used for this, but in this case the following problems arise:
- Echo. When old values come through the listener again.
- Out of sync. When in one place one value, and in another another. Occurs when parallel values change.
- Overwrite values. When we take a value from Property, we check it, we think that it has not changed and we write there a new one, and in fact we overwrite the changed value.
Wrapper:
package javafxapplication1; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.function.BiConsumer; import java.util.logging.Level; import java.util.logging.Logger; import javafx.application.Platform; import javafx.beans.property.Property; class FxPropertyAdapter<T> { private static final Logger LOG = Logger.getLogger(FxPropertyAdapter.class.getName()); private volatile T value; private final Property<T> property; private boolean listenerEnabled = true; private final CountDownLatch initializedLatch = new CountDownLatch(1); private volatile Runnable onInitializeHandler = null; private volatile BiConsumer<T, T> onChangeHandler = null; public FxPropertyAdapter(Property<T> property) { Objects.requireNonNull(property); this.property = property; Platform.runLater(() -> { value = property.getValue(); property.addListener((observable, oldValue, newValue) -> { if (listenerEnabled) { synchronized (this) { updateValue(newValue); } } }); synchronized (this) { initializedLatch.countDown(); if (onInitializeHandler != null) { try { onInitializeHandler.run(); } catch (Exception e) { LOG.log(Level.WARNING, "Error in initializeHandler.", e); } } } }); } public Runnable getOnInitializeHandler() { return onInitializeHandler; } public synchronized void setOnInitializeHandler(Runnable onInitializeHandler) { this.onInitializeHandler = onInitializeHandler; if (isInitialized() && this.onInitializeHandler != null) { try { this.onInitializeHandler.run(); } catch (Exception e) { LOG.log(Level.WARNING, "Error in onInitializeHandler.", e); } } } public BiConsumer<T, T> getOnChangeHandler() { return onChangeHandler; } public void setOnChangeHandler(BiConsumer<T, T> onChangeHandler) { this.onChangeHandler = onChangeHandler; } public Property<T> getProperty() { return property; } /** * Позволяет не упустить обновления, произведённые в JavaFx. Если JavaFx * успеет обновить значение, то мы увидим несоответствие, если запланирует * обновление, тогда то значение, которое мы сюда передали, будет затёрто * начатым обновлением из JavaFx. Но местный обработчик изменения все же * сработает. * * @param expect * @param update * @return */ public synchronized boolean compareAndSet(T expect, T update) { if (expect == getValue()) { setValue(update); return true; } else { return false; } } public T getValue() { requireInitialized(); return value; } /** * При установлении значения приоритет всегда на стороне JavaFx. Если * параллельно менять значения в JavaFx и в другом потоке, то из этого * потока значения не будут успевать попадать в постоянно меняющийся * Property. Однако, они будут попадать в onChangeHandler данного класса. * * С другой стороны, у нас есть доступ к самому Property и мы можем * установить ему значение через Platform.runLater. * * @param value */ public synchronized void setValue(T value) { requireInitialized(); if (this.value == value) { return; } T oldValue = this.value; updateValue(value); Platform.runLater(() -> { listenerEnabled = false; /** * Если JavaFx обновит значение до этого метода, то мы просто * обновим значение в обоих потоках. * * Если JavaFx начнёт обновлять значение до вызова метода, но не * успеет захватить доступ к объекту, то проверка спасает от разных * значений. В противном случае было бы запланировано изменение * Property без слушателя, а далее отработал бы ожидающий JavaFx * поток, который бы задал переменной старое значение, а это уже * рассинхронизация. * * Синхронизация нужная, чтобы текущий поток не затёр значение из * JavaFx вызовом updateValue(value), без синхронизации JavaFx может * пролезть между проверкой старого значения и обновлением значения, * в итоге JavaFx значение затрётся, что приведёт к разным * значениям. */ if (property.getValue().equals(oldValue)) { property.setValue(value); } listenerEnabled = true; }); } public boolean isInitialized() { return initializedLatch.getCount() == 0; } private void updateValue(T value) { T oldValue = this.value; this.value = value; if (isInitialized()) { try { onChangeHandler.accept(oldValue, value); } catch (Exception e) { LOG.log(Level.WARNING, "Error in onChangeHandler.", e); } } } private void requireInitialized() { if (!isInitialized()) { throw new RuntimeException("FxPropertyAdapter isn't initialized yet."); } } } Example:
package javafxapplication1; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Application; import javafx.beans.property.SimpleFloatProperty; import javafx.scene.Scene; import javafx.scene.control.ProgressBar; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import javafx.util.Duration; public class JavaFXApplication extends Application { private static final Logger LOG = Logger.getLogger(JavaFXApplication.class.getName()); @Override public void start(Stage primaryStage) { ProgressBar pb = new ProgressBar(); StackPane root = new StackPane(); root.getChildren().add(pb); Scene scene = new Scene(root, 300, 250); primaryStage.setTitle("Test"); primaryStage.setScene(scene); primaryStage.show(); Thread t1 = new Thread(() -> { SimpleFloatProperty slp = new SimpleFloatProperty(); FxPropertyAdapter<Number> fpa = new FxPropertyAdapter<>(slp); fpa.setOnChangeHandler((from, to) -> { System.out.println(String.format("Value have been changed from %s to %s.", from, to)); }); CountDownLatch initializedLatch = new CountDownLatch(1); fpa.setOnInitializeHandler(() -> { pb.progressProperty().bindBidirectional(slp); initializedLatch.countDown(); }); try { initializedLatch.await(); } catch (InterruptedException ex) { LOG.log(Level.SEVERE, null, ex); } int sign = +1; for (float i = .0f; i >= 0; i += .01f * sign) { if (i > 1) { sign = -sign; } fpa.setValue(i); try { TimeUnit.MILLISECONDS.sleep(100 - (int) (i * 100)); } catch (InterruptedException ex) { LOG.log(Level.SEVERE, null, ex); return; } } Timeline timeline = new Timeline(new KeyFrame(Duration.millis(5000), new KeyValue(slp, 1))); timeline.setCycleCount(2); timeline.setAutoReverse(true); timeline.play(); }); t1.setDaemon(true); t1.start(); } public static void main(String[] args) { launch(args); } }