Как интерполировать цвет узла с помощью пользовательских переменных CSS?

Есть узел, и мне нужно динамически менять его цвет. Я также хочу сделать это, используя переменные CSS. Проблема в том, что JavaFX, похоже, выполняет поиск CSS только тогда, когда свойство узла (заливка) явно привязано к соответствующему свойству объекта, настраиваемому по стилям, значение которого должно быть получено через CSS. Другими словами, CSS работает только в том случае, если свойство styleable привязано к свойству узла и этот узел существует в графе сцены.

Но если свойство Node уже привязано, я не смогу интерполировать его значение на временной шкале. Есть ли здесь обходной путь? Например, могу ли я каким-то образом вручную запустить поиск переменных CSS до начала временной шкалы?

Минимальный воспроизводимый пример:

public class ExampleApp extends Application {

    public static void main(String[] args) {
        launch();
    }

    @Override
    public void start(Stage stage) {
        var r = new AnimatedRect(200, 200);
        // actual: rect flashes red and blue
        // expected: rect flashes green and orange
        r.setStyle("-color1: green; -color2: orange;");

        var scene = new Scene(new BorderPane(r), 200, 200);
        stage.setScene(scene);
        stage.show();
    }

    static class AnimatedRect extends Rectangle {

        public AnimatedRect(double width, double height) {
            super(width, height);
            setFill(color1.get());

            // if you bind the color property to the rect fill, the CSS variables will start to work,
            // but the timeline will stop because it's forbidden to change a bound value,
            // ... and unfortunately bidirectional binding won't help here either
            // fillProperty().bind(color1);

            var timeline = new Timeline(
                new KeyFrame(Duration.millis(0),
                    new KeyValue(fillProperty(), color1.get(), LINEAR)
                ),
                new KeyFrame(Duration.millis(1000),
                    new KeyValue(fillProperty(), color2.get(), LINEAR)
                )
            );
            timeline.setCycleCount(Timeline.INDEFINITE);
            timeline.setAutoReverse(false);

            sceneProperty().addListener((obs, o, n) -> {
                if (n != null) {
                    timeline.play();
                } else {
                    timeline.stop();
                }
            });
        }

        final StyleableObjectProperty<Paint> color1 = new SimpleStyleableObjectProperty<>(
            StyleableProperties.COLOR1, AnimatedRect.this, "-color1", Color.RED
        );

        final StyleableObjectProperty<Paint> color2 = new SimpleStyleableObjectProperty<>(
            StyleableProperties.COLOR2, AnimatedRect.this, "-color2", Color.BLUE
        );

        static class StyleableProperties {

            private static final CssMetaData<AnimatedRect, Paint> COLOR1 = new CssMetaData<>(
                "-color1", PaintConverter.getInstance(), Color.RED
            ) {
                @Override
                public boolean isSettable(AnimatedRect c) {
                    return !c.color1.isBound();
                }

                @Override
                public StyleableProperty<Paint> getStyleableProperty(AnimatedRect c) {
                    return c.color1;
                }
            };

            private static final CssMetaData<AnimatedRect, Paint> COLOR2 = new CssMetaData<>(
                "-color2", PaintConverter.getInstance(), Color.BLUE
            ) {
                @Override
                public boolean isSettable(AnimatedRect c) {
                    return !c.color2.isBound();
                }

                @Override
                public StyleableProperty<Paint> getStyleableProperty(AnimatedRect c) {
                    return c.color2;
                }
            };

            private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;

            static {
                final List<CssMetaData<? extends Styleable, ?>> styleables =
                    new ArrayList<>(Rectangle.getClassCssMetaData());
                styleables.add(COLOR1);
                styleables.add(COLOR2);
                STYLEABLES = Collections.unmodifiableList(styleables);
            }
        }

        public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
            return StyleableProperties.STYLEABLES;
        }

        @Override
        public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
            return getClassCssMetaData();
        }
    }
}

ОБНОВЛЯТЬ:

Я нашел проблему. JavaFX разрешает переменные CSS после подключения узла к сцене. Мой предыдущий код создает временную шкалу до изменения значений цвета. Поэтому мне нужно следить за изменениями цвета и соответствующим образом обновлять временную шкалу. Поскольку он неизменяем, единственный способ — создать новый объект. Это все еще не оптимально, потому что если я обновлю оба цвета, анимация будет воспроизводиться дважды, но, по крайней мере, сейчас она работает.

static class AnimatedRect extends Rectangle {

SimpleObjectProperty<Timeline> timeline = new SimpleObjectProperty<>();

public AnimatedRect(double width, double height) {
    super(width, height);
    setFill(color1.get());

    color1.addListener((obs, o, v) -> {
        if (timeline.get() != null) {
            timeline.get().stop();
        }
        timeline.set(createTimeline());
        timeline.get().play();
    });

    color2.addListener((obs, o, v) -> {
        if (timeline.get() != null) {
            timeline.get().stop();
        }

        timeline.set(createTimeline());
        timeline.get().play();
    });

    sceneProperty().addListener((obs, o, n) -> {
        if (n != null) {
            if (timeline.get() != null) {
                timeline.get().play();
            }
        } else {
            if (timeline.get() != null) {
                timeline.get().stop();
            }
        }
    });
}

Timeline createTimeline() {
    var timeline = new Timeline(
        new KeyFrame(Duration.millis(0),
            new KeyValue(fillProperty(), color1.getValue(), LINEAR)
        ),
        new KeyFrame(Duration.millis(1000),
            new KeyValue(fillProperty(), color2.getValue(), LINEAR)
        )
    );
    timeline.setCycleCount(Timeline.INDEFINITE);
    timeline.setAutoReverse(false);

    return timeline;
}

// .. the rest of the code

«Я нашел проблему. JavaFX разрешает переменные CSS после подключения узла к сцене». Это правда, но даже если бы это было не так, ваш исходный код все равно демонстрировал бы то же поведение, которое вы наблюдаете. Вы создаете временную шкалу в конструкторе, который вызываете перед установкой стиля, поэтому временная шкала в любом случае будет создана с цветами по умолчанию.

James_D 13.08.2024 15:17

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

James_D 13.08.2024 15:37
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
3
3
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

В исходном коде вы ищете значения цветов в конструкторе AnimatedRectangle, который в этот момент обязательно будет иметь значения по умолчанию, и используете их для создания временной шкалы. Как только вы установите цвета с помощью вызова setStyle(...), временная шкала уже создана, и ее «цвета конечной точки» по сути неизменны.

(Проблема усугубляется, как отмечено в обновлении к вопросу, тем фактом, что значения свойств CSS фактически не будут установлены до тех пор, пока applyCSS() не будет вызван для прямоугольника, что обычно происходит на первом проходе макета после того, как прямоугольник будет добавлено в сцену.)

Лучшее решение — просто найти значения цветов конечной точки в каждом кадре анимации. Вы можете сделать это, например, используя Transition вместо Timeline (другое решение — использовать AnimationTimer). Вот модифицированный конструктор, который будет работать так, как вы хотите:

        public AnimatedRect(double width, double height) {
            super(width, height);
            setFill(color1.get());

//            var timeline = new Timeline(
//                    new KeyFrame(Duration.millis(0),
//                            new KeyValue(fillProperty(), color1.get(), Interpolator.LINEAR)
//                    ),
//                    new KeyFrame(Duration.millis(1000),
//                            new KeyValue(fillProperty(), color2.get(), Interpolator.LINEAR)
//                    )
//            );
//            timeline.setCycleCount(Timeline.INDEFINITE);
//            timeline.setAutoReverse(false);

            Transition transition = new Transition() {
                {
                    setCycleDuration(Duration.seconds(1));
                }
                @Override
                protected void interpolate(double v) {
                    Paint p1 = color1.get();
                    Paint p2 = color2.get();
                    // if these are both colors, interpolate them. 
                    // If they're e.g. gradients, just switch halfway:
                    if (p1 instanceof Color c1 && p2 instanceof Color c2) {
                        setFill(c1.interpolate(c2, v));
                    } else {
                        setFill(v <= 0.5 ? p1 : p2);
                    }
                }
            };
            transition.setCycleCount(Animation.INDEFINITE);
            sceneProperty().subscribe(scene -> {
                if (scene != null) {
                    transition.play();
                } else {
                    transition.stop();
                }
            });
        }

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

Также обратите внимание, что я изменил sceneProperty().addListener(...) на sceneProperty().subscribe(), что кажется мне немного чище. Вызов этого также будет работать, если прямоугольник уже является частью сцены (что невозможно в данном случае, но возможно при некотором рефакторинге кода). subscribe() был представлен в JavaFX 21.

Другие вопросы по теме