Событие JavaFX на колесе мыши завершено для ScrollPane

У меня есть ScrollPane с множеством элементов (такой же, как этот JavaFX setHgrow/свойство привязки бесконечно расширяется), и изначально я планировал использовать событие setOnScrollFinished(this::scrollFinished);, однако теперь я обнаружил в ходе исследований, что это относится только к сенсорным жестам, и пытаюсь найти компромисс для MouseWheel не был хорошим, и я просто нахожу очень сложные решения, которые на самом деле не решают то, что мне нужно.

Максимум, что у меня есть, это добавить слушателя к изменению полосы прокрутки:

vvalueProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
                System.out.println("scroll time");
            }
        });

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

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

По сути, это мой код, взятый у хорошего пользователя, который помог мне раньше:

import javafx.application.Application;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;

public class ScrollPaneContentDemo extends Application {
    @Override
    public void start(Stage stage) throws Exception {
        List<Item> items = new ArrayList<>();
        IntStream.range(1, 1000).forEach(i -> items.add(new Item()));
        TestPanel root = new TestPanel(items);
        Scene scene = new Scene(root, 500, 500);
        stage.setScene(scene);
        stage.setTitle("ScrollPaneContent Demo");
        stage.show();
    }

    class TestPanel extends ScrollPane {
        private final int SPACING = 5;
        private final int ROW_MAX = 6;
        private DoubleProperty size = new SimpleDoubleProperty();

        public TestPanel(List<Item> items) {
            final VBox root = new VBox();
            root.setSpacing(SPACING);
            HBox row = null;
            int count = 0;
            for (Item item : items) {
                if (count == ROW_MAX || row == null) {
                    row = new HBox();
                    row.setSpacing(SPACING);
                    root.getChildren().add(row);
                    count = 0;
                }

                CustomBox box = new CustomBox(item);
                box.minWidthProperty().bind(size);
                row.getChildren().add(box);
                HBox.setHgrow(box, Priority.ALWAYS);
                count++;
            }
            setFitToWidth(true);
            setContent(root);

            double padding = 4;
            viewportBoundsProperty().addListener((obs, old, bounds) -> {
                size.setValue((bounds.getWidth() - padding - ((ROW_MAX - 1) * SPACING)) / ROW_MAX);
                });


        //setOnScroll(this::showImages);       //The problematic things

        vvalueProperty().addListener(new ChangeListener<Number>() {
        @Override
        public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
            System.out.println("scroll test");           //The problematic things
        }
    });

        }
    }

    class CustomBox extends StackPane {
        private Item item;
        private Rectangle square;
               private int size = 20;

    public CustomBox(Item item) {
        setStyle("-fx-background-color:#99999950;");
        this.item = item;
        setPadding(new Insets(5, 5, 5, 5));
        square = new Rectangle(size, size, Color.RED);
        square.widthProperty().bind(widthProperty());
        square.heightProperty().bind(heightProperty());

        maxHeightProperty().bind(minWidthProperty());
        maxWidthProperty().bind(minWidthProperty());
        minHeightProperty().bind(minWidthProperty());
        getChildren().add(square);
    }
}

    class Item {
    }
}
назначьте им изображение, если они находятся в пределах окна, и удалите изображение, если они не почему?
kleopatra 20.03.2022 11:45

повторение (мой комментарий к вашему предыдущему вопросу): НЕ расширяет узлы (и любые другие классы) без добавления функциональности! Расширение предназначено для улучшения услуг, предлагаемых классом, то есть включает новый API. Простого вызова существующего API в конструкторе для удобства недостаточно.

kleopatra 20.03.2022 11:52

Потому что лагает со всеми загруженными изображениями

fox 20.03.2022 14:46
минимальный воспроизводимый пример пожалуйста..демонстрация лагов.
kleopatra 20.03.2022 15:13
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
4
46
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

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

  1. Регистрируйтесь каждый раз, когда меняются значения прокрутки (или изменяется размер ScrollPane)
  2. Настройте цикл, который будет проверять с короткими интервалами (с точки зрения пользователя), если изменение было зарегистрировано более 1 секунды назад.
  3. Когда это произойдет, пусть ScrollPane запустит событие - назовем это "галочкой"- и отменит регистрацию последней прокрутки.

Для цикла мы будем использовать Timeline, который KeyFrames будет иметь обработчик onFinished, вызываемый в потоке приложения JavaFX примерно каждые 100 мс, чтобы избежать необходимости иметь дело с другим потоком.

class TickingScrollPane extends ScrollPane {

  //Our special event type, to be fired after a delay when scrolling stops
  public static final EventType<Event> SCROLL_TICK = new EventType<>(TickingScrollPane.class.getName() + ".SCROLL_TICK");

  // Strong refs to listener and timeline
  private final ChangeListener<? super Number> scrollListener; //Will register any scrolling
  private final Timeline notifyLoop;  //Will check every 100ms how long ago we last scrolled

  // Last registered scroll timing
  private long lastScroll = 0; // 0 means "no scroll registered"

  public TickingScrollPane() {
    super();

    /* Register any time a scrollbar moves (scrolling by any means or resizing)
     * /!\ will fire once when initially shown because of width/height listener */
    scrollListener = (_observable, _oldValue, _newValue) -> {
      lastScroll = System.currentTimeMillis();
    };
    this.vvalueProperty().addListener(scrollListener);
    this.hvalueProperty().addListener(scrollListener);
    this.widthProperty().addListener(scrollListener);
    this.heightProperty().addListener(scrollListener);
    //ScrollEvent.SCROLL works only for mouse wheel, but you could as well use it 

    /* Every 100ms, check if there's a registered scroll.
     * If so, and it's older than 1000ms, then fire and unregister it.
     * Will therefore fire at most once per second, about 1 second after scroll stopped */
    this.notifyLoop = new Timeline(new KeyFrame(Duration.millis(100), //100ms exec. interval
        e -> {
          if (lastScroll == 0)
            return;
          long now = System.currentTimeMillis();
          if (now - lastScroll > 1000) { //1000ms delay
            lastScroll = 0;
            fireEvent(new Event(this, this, SCROLL_TICK));
          }
        }));
    this.notifyLoop.setCycleCount(Timeline.INDEFINITE);
    this.notifyLoop.play();

  }

}

Если ваш ScrollPane должен быть удален со сцены в любой момент, вы можете добавить метод, чтобы остановить TimeLine, чтобы он не продолжал работать и, возможно, потреблял память.

Полный рабочий демо-код:

package application;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.event.Event;
import javafx.event.EventType;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;

class TickingScrollPane extends ScrollPane {

  //Our special event type, to be fired after a delay when scrolling stops
  public static final EventType<Event> SCROLL_TICK = new EventType<>(TickingScrollPane.class.getName() + ".SCROLL_TICK");

  // Strong refs to listener and timeline
  private final ChangeListener<? super Number> scrollListener; //Will register any scrolling
  private final Timeline notifyLoop;  //Will check every 100ms how long ago we last scrolled

  // Last registered scroll timing
  private long lastScroll = 0; // 0 means "no scroll registered"

  public TickingScrollPane() {
    super();

    /* Register any time a scrollbar moves (scrolling by any means or resizing)
     * /!\ will fire once when initially shown because of width/height listener */
    scrollListener = (_observable, _oldValue, _newValue) -> {
      lastScroll = System.currentTimeMillis();
    };
    this.vvalueProperty().addListener(scrollListener);
    this.hvalueProperty().addListener(scrollListener);
    this.widthProperty().addListener(scrollListener);
    this.heightProperty().addListener(scrollListener);
    //ScrollEvent.SCROLL works only for mouse wheel, but you could as well use it 

    /* Every 100ms, check if there's a registered scroll.
     * If so, and it's older than 1000ms, then fire and unregister it.
     * Will therefore fire at most once per second, about 1 second after scroll stopped */
    this.notifyLoop = new Timeline(new KeyFrame(Duration.millis(100), //100ms exec. interval
        e -> {
          if (lastScroll == 0)
            return;
          long now = System.currentTimeMillis();
          if (now - lastScroll > 1000) { //1000ms delay
            lastScroll = 0;
            fireEvent(new Event(this, this, SCROLL_TICK));
          }
        }));
    this.notifyLoop.setCycleCount(Timeline.INDEFINITE);
    this.notifyLoop.play();

  }

}

public class TickingScrollPaneTest extends Application {
  
  @Override
  public void start(Stage primaryStage) {
    
    try {
      
      //Draw our scrollpane, add a bunch of rectangles in a VBox to fill its contents
      TickingScrollPane root = new TickingScrollPane();
      root.setPadding(new Insets(5));
      VBox vb = new VBox(6);
      root.setContent(vb);
      final int rectsCount = 10;
      for (int i = 0; i < rectsCount; i++) {
        Rectangle r = new Rectangle(Math.random() * 900, 60); //Random width, 60px height
        r.setFill(Color.hsb(360. / rectsCount * i, 1, .85));   //Changing hue (rainbow style)
        vb.getChildren().add(r);
      }
      
      //Log every scroll tick to console
      root.addEventHandler(TickingScrollPane.SCROLL_TICK, e -> {
        System.out.println(String.format(
            "%s:\tScrolled 1s ago to (%s)",
            LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME),
            getViewableBounds(root)
            ));
      });
      
      //Show in a 400x400 window
      Scene scene = new Scene(root, 400, 400);
      primaryStage.setScene(scene);
      primaryStage.setTitle("TickingScrollPane test");
      primaryStage.show();
    } catch (Exception e) {
      e.printStackTrace();
    }
    
  }
  
  
  /**
   * Calculate viewable bounds for contents for ScrollPane
   * given viewport size and scroll position
   */
  private static Bounds getViewableBounds(ScrollPane scrollPane) {
    Bounds vbds = scrollPane.getViewportBounds();
    Bounds cbds = scrollPane.getContent().getLayoutBounds();
    double hoffset = 0;
    if (cbds.getWidth() > vbds.getWidth())
      hoffset = Math.max(0, cbds.getWidth() - vbds.getWidth()) * (scrollPane.getHvalue() - scrollPane.getHmin()) / (scrollPane.getHmax() - scrollPane.getHmin());
    double voffset = 0;
    if (cbds.getHeight() > vbds.getHeight())
      voffset = Math.max(0, cbds.getHeight() - vbds.getHeight()) * (scrollPane.getVvalue() - scrollPane.getVmin()) / (scrollPane.getVmax() - scrollPane.getVmin());
    Bounds viewBounds = new BoundingBox(hoffset, voffset, vbds.getWidth(), vbds.getHeight());
    return viewBounds;
  }

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

Привет, большое спасибо за ответ! Детали, которые вы объяснили, были более чем полезны, я очень ценю это. Однако у меня есть небольшая проблема: я решил изменить свою ScrollPane на ListView<HBox>, так как это безумно исправило мои проблемы с задержкой, поскольку я раньше не знал, что ListView имеет свойства рендеринга. Я действительно хочу реализовать то, что вы сделали, но я просто не могу найти способ получить vvalue из ListView. Каждое решение, которое я нашел в Интернете, просто не сработало, и я действительно борюсь. Хотите знать, знаете ли вы, как я могу получить vvalueProperty из моего нового ListPane? Спасибо еще раз

fox 24.03.2022 21:46

Извините, я не вижу никаких свойств в ListView, которые позволили бы вам узнать положение прокрутки... Может быть, задать другой вопрос?

julien.giband 28.03.2022 11:01

Все в порядке, в итоге я просто использовал событие прокрутки с вашим кодом, оно все еще было безумно полезным для моего проекта и общего обучения. Спасибо еще раз!

fox 28.03.2022 23:52

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

Похожие вопросы