鼠标滚轮上的 JavaFX 事件已为 ScrollPane 完成
JavaFX event on Mouse Wheel Finished for ScrollPane
我有一个 ScrollPane,上面有很多元素(与这个 相同),最初我打算使用 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");
}
});
然而,这在滚动时不断触发,我正在寻找的是只会在我停止滚动后调用的东西。
我的最终目标是有一个系统,当我滚动时,它会 运行 一个事件,它将遍历我的每个元素,这样我就可以为它们分配一个图像,如果它们在 window 边界,如果不是,他们会删除图像。
这基本上是我的代码,取自之前帮助过我的好用户:
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 {
}
}
您将必须侦听 属性 更改以检测滚动而不会丢失。不过,您不必在每次侦听器触发时都采取繁重的操作:只需记录它发生的时间,然后进行循环过滤并在需要时触发事件。这是:
- 随时注册滚动值更改(或
ScrollPane
调整大小)
- 设置一个循环,以较短的时间间隔(从用户角度)检查是否在超过 1 秒之前注册了更改。
- 发生这种情况时,让
ScrollPane
触发一个事件 - 我们称之为“滴答”- 和 un-register 最后滚动
对于循环,我们将使用一个 Timeline
,其中 KeyFrame
将有一个 onFinished
处理程序大约每 100 毫秒在 JavaFX 应用程序线程上调用一次,以避免处理另一个线程。
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,上面有很多元素(与这个 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");
}
});
然而,这在滚动时不断触发,我正在寻找的是只会在我停止滚动后调用的东西。
我的最终目标是有一个系统,当我滚动时,它会 运行 一个事件,它将遍历我的每个元素,这样我就可以为它们分配一个图像,如果它们在 window 边界,如果不是,他们会删除图像。
这基本上是我的代码,取自之前帮助过我的好用户:
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 {
}
}
您将必须侦听 属性 更改以检测滚动而不会丢失。不过,您不必在每次侦听器触发时都采取繁重的操作:只需记录它发生的时间,然后进行循环过滤并在需要时触发事件。这是:
- 随时注册滚动值更改(或
ScrollPane
调整大小) - 设置一个循环,以较短的时间间隔(从用户角度)检查是否在超过 1 秒之前注册了更改。
- 发生这种情况时,让
ScrollPane
触发一个事件 - 我们称之为“滴答”- 和 un-register 最后滚动
对于循环,我们将使用一个 Timeline
,其中 KeyFrame
将有一个 onFinished
处理程序大约每 100 毫秒在 JavaFX 应用程序线程上调用一次,以避免处理另一个线程。
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);
}
}