viewportBoundsProperty 的 JavaFX ScrollPane 更新
JavaFX ScrollPane update of viewportBoundsProperty
我正在构建一个 tilemap,就像 openstreetmap 或 google 地图一样,使用带有 ScrollPane 和 TilePane 的 JavaFX。
在 tilemap 中,我们只需要加载屏幕当前可见的部分,而不是所有的图块。
JavaFX ScrollPane 似乎更新了它的 viewportBounds
MinX
、MaxX
和 MinY
以及 MaxY
( 让我们称它为 offset
剩下的问题) :
- 使用鼠标交互(单击并拖动)进行平移时。
- 但在使用垂直和水平滚动条或使用触摸板进行平移时不会。
这是一个问题,因为这意味着我可以在单击和拖动时计算内容的当前可见部分,但在使用滚动条或使用触摸板移动时不能。
我需要不断获取此偏移量,因为我的 ScrollPane 的内容是在当前在 ScrollPane“位置”可见时加载的:
这是一个说明情况的最小可复制示例:
public class DemoScrollPane extends javafx.application.Application {
private Logger logger = LoggerFactory.getLogger(DemoScrollPane.class);
public static void run(String[] args) {
javafx.application.Application.launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
// create content
Pane pane = new Pane();
Rectangle rectangle = new Rectangle(1000,1000, Color.AQUA);
Rectangle rectangleInitialVP = new Rectangle(640,480, Color.GRAY);
pane.getChildren().add(rectangle);
pane.getChildren().add(rectangleInitialVP);
// create scrollPane
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(pane);
// add click and drag interaction
scrollPane.setPannable(true);
// register a watcher on scrollPane viewportBoundsProperty
InvalidationListener listener =
o -> {
logger.debug("viewport change {}",scrollPane.getViewportBounds());
};
// This is triggered when moving using click and drag (pan)
// and does correctly update the scrollPane viewportbounds.
// BUT this is not trigered when moving by clocking on hbar or vbar or by using a touchpad (on mac) without clicking.
scrollPane.viewportBoundsProperty().addListener(listener);
// display the app
Scene scene = new Scene(scrollPane, 640, 480);
primaryStage.setScene(scene);
primaryStage.show();
}
@Override
public void stop() throws Exception {
logger.debug("stop");
}
}
有人可以指导我如何在任何滚动操作(单击并拖动平移或使用触摸板或使用水平或垂直条平移)上动态计算(或获取)此偏移量吗?
提前致谢。
感谢 @James_D 的评论,这里有一个解决问题的方法(见下文)。
如果有人对JavaFX有更好的理解ScrollPane
请不要犹豫解释
之间的区别
- 单击并滚动更新
ScrollPane
的视口
- vs 使用不更新
ScrollPane
. 视口的水平和垂直条滚动
这是从 James_D 的评论中摘录的解决方案:
public class DemoScrollPane extends javafx.application.Application {
private Logger logger = LoggerFactory.getLogger(DemoScrollPane.class);
public static void run(String[] args) {
javafx.application.Application.launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
// create content
Pane pane = new Pane();
pane.setMaxSize(1000,1000);
Rectangle rectangle = new Rectangle(1000,1000, Color.AQUA);
Rectangle rectangleInitialVP = new Rectangle(640,480, Color.GRAY);
pane.getChildren().add(rectangle);
pane.getChildren().add(rectangleInitialVP);
// create scrollPane
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(pane);
scrollPane.setVmin(0);
scrollPane.setVmax(pane.getMaxHeight());
scrollPane.setHmin(0);
scrollPane.setHmax(pane.getMaxWidth());
// add click and drag interaction
scrollPane.setPannable(true);
// register a watcher on scrollPane viewportBoundsProperty
InvalidationListener listener =
o -> {
Bounds movingViewPort = new BoundingBox(scrollPane.getHvalue(),
scrollPane.getVvalue(),
0.,
scrollPane.getViewportBounds().getWidth(),
scrollPane.getViewportBounds().getHeight(),
0.
);
logger.debug("viewport {}",movingViewPort);
};
// This is triggered when moving using click and drag (pan)
// and does correctly update the scrollPane viewportbounds.
// BUT this is not trigered when moving by clocking on hbar or vbar or by using a touchpad (on mac)
// without clicking.
// (even tough the viewport obviously change)
scrollPane.vvalueProperty().addListener(listener);
scrollPane.hvalueProperty().addListener(listener);
// display the app
Scene scene = new Scene(scrollPane, 640, 480);
primaryStage.setScene(scene);
primaryStage.show();
}
@Override
public void stop() throws Exception {
logger.debug("stop");
}
}
关于差异问题的简单答案是 :: 这就是它的实现方式!!
首先,每次 scrollPane 尝试布局其子项时,viewPort 边界都会更新。 ScrollPaneSkinclass中的layoutChildren方法偷窥如下:
protected void layoutChildren(double x, double y, double w, double h) {
ScrollPane control = (ScrollPane)this.getSkinnable();
... // All the stuff for layouting
...
control.setViewportBounds(new BoundingBox(this.snapPosition(this.viewContent.getLayoutX()), this.snapPosition(this.viewContent.getLayoutY()), this.snapSize(this.contentWidth), this.snapSize(this.contentHeight)));
}
现在如果我们检查ScrollPane的“pannable”属性的相关实现,代码如下(在ScrollPaneSkin class):
this.viewRect.setOnDragDetected((e) -> {
if (IS_TOUCH_SUPPORTED) {
this.startSBReleasedAnimation();
}
if (((ScrollPane)this.getSkinnable()).isPannable()) {
this.dragDetected = true;
if (this.saveCursor == null) {
this.saveCursor = ((ScrollPane)this.getSkinnable()).getCursor();
if (this.saveCursor == null) {
this.saveCursor = Cursor.DEFAULT;
}
((ScrollPane)this.getSkinnable()).setCursor(Cursor.MOVE);
((ScrollPane)this.getSkinnable()).requestLayout(); // !! THIS LINE !!
}
}
});
this.viewRect.addEventFilter(MouseEvent.MOUSE_RELEASED, (e) -> {
this.mouseDown = false;
if (this.dragDetected) {
if (this.saveCursor != null) {
((ScrollPane)this.getSkinnable()).setCursor(this.saveCursor);
this.saveCursor = null;
((ScrollPane)this.getSkinnable()).requestLayout();// !! THIS LINE !!
}
this.dragDetected = false;
}
if ((this.posY > ((ScrollPane)this.getSkinnable()).getVmax() || this.posY < ((ScrollPane)this.getSkinnable()).getVmin() || this.posX > ((ScrollPane)this.getSkinnable()).getHmax() || this.posX < ((ScrollPane)this.getSkinnable()).getHmin()) && !this.touchDetected) {
this.startContentsToViewport();
}
});
从上面的代码可以看出,当“pannable”属性为真时,如果检测到拖动,它会请求布局。在下一个场景脉冲中,它将调用 layoutChildren 并更新 viewportBounds。当您释放鼠标时(如果检测到拖动)也会发生同样的事情。这就是为什么您只能在平移开始和结束时看到日志,而不是在拖动时看到日志。
现在进入hbar/vbar拖动的代码,更新scrollBar值时的代码如下:(在ScrollPaneSkin class)
InvalidationListener vsbListener = (valueModel) -> {
if (!IS_TOUCH_SUPPORTED) {
this.posY = Utils.clamp(((ScrollPane)this.getSkinnable()).getVmin(), this.vsb.getValue(), ((ScrollPane)this.getSkinnable()).getVmax());
} else {
this.posY = this.vsb.getValue();
}
this.updatePosY();
};
this.vsb.valueProperty().addListener(vsbListener);
InvalidationListener hsbListener = (valueModel) -> {
if (!IS_TOUCH_SUPPORTED) {
this.posX = Utils.clamp(((ScrollPane)this.getSkinnable()).getHmin(), this.hsb.getValue(), ((ScrollPane)this.getSkinnable()).getHmax());
} else {
this.posX = this.hsb.getValue();
}
this.updatePosX();
};
this.hsb.valueProperty().addListener(hsbListener);
值侦听器仅更新 viewContent 的 layoutX/Y 属性 而不是布局请求。 updatePosX方法如下:
private double updatePosX() {
ScrollPane sp = (ScrollPane)this.getSkinnable();
double x = this.isReverseNodeOrientation() ? this.hsb.getMax() - (this.posX - this.hsb.getMin()) : this.posX;
double minX = Math.min(-x / (this.hsb.getMax() - this.hsb.getMin()) * (this.nodeWidth - this.contentWidth), 0.0D);
this.viewContent.setLayoutX(this.snapPosition(minX));
if (!sp.hvalueProperty().isBound()) {
sp.setHvalue(Utils.clamp(sp.getHmin(), this.posX, sp.getHmax()));
}
return this.posX;
}
另一种解法:
+1 @James_D 回答。如果您想保持动态,或者您不知道窗格的最大边界,您可以尝试以下方法。
从上面的代码可以看出,无论是平移还是拖动滚动条,viewContent(scrollPane的内容父节点)的layoutX/Y属性都会更新。因此,您可以将侦听器添加到其 layoutX/Y 属性,这将为您提供准确的位置,而无需将 min/max 值设置为滚动条。
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(pane);
scrollPane.getContent().parentProperty().addListener((obs, old, viewContent) -> {
if (viewContent != null) {
InvalidationListener posListener = e -> {
double x = viewContent.getLayoutX() * -1;
double y = viewContent.getLayoutY() * -1;
double width = scrollPane.getViewportBounds().getWidth();
double height = scrollPane.getViewportBounds().getHeight();
Bounds movingViewPort = new BoundingBox(x, y, width, height);
System.out.println("viewport: " + movingViewPort);
};
viewContent.layoutXProperty().addListener(posListener);
viewContent.layoutYProperty().addListener(posListener);
}
});
scrollPane.setPannable(true);
我正在构建一个 tilemap,就像 openstreetmap 或 google 地图一样,使用带有 ScrollPane 和 TilePane 的 JavaFX。
在 tilemap 中,我们只需要加载屏幕当前可见的部分,而不是所有的图块。
JavaFX ScrollPane 似乎更新了它的 viewportBounds
MinX
、MaxX
和 MinY
以及 MaxY
( 让我们称它为 offset
剩下的问题) :
- 使用鼠标交互(单击并拖动)进行平移时。
- 但在使用垂直和水平滚动条或使用触摸板进行平移时不会。
这是一个问题,因为这意味着我可以在单击和拖动时计算内容的当前可见部分,但在使用滚动条或使用触摸板移动时不能。
我需要不断获取此偏移量,因为我的 ScrollPane 的内容是在当前在 ScrollPane“位置”可见时加载的:
这是一个说明情况的最小可复制示例:
public class DemoScrollPane extends javafx.application.Application {
private Logger logger = LoggerFactory.getLogger(DemoScrollPane.class);
public static void run(String[] args) {
javafx.application.Application.launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
// create content
Pane pane = new Pane();
Rectangle rectangle = new Rectangle(1000,1000, Color.AQUA);
Rectangle rectangleInitialVP = new Rectangle(640,480, Color.GRAY);
pane.getChildren().add(rectangle);
pane.getChildren().add(rectangleInitialVP);
// create scrollPane
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(pane);
// add click and drag interaction
scrollPane.setPannable(true);
// register a watcher on scrollPane viewportBoundsProperty
InvalidationListener listener =
o -> {
logger.debug("viewport change {}",scrollPane.getViewportBounds());
};
// This is triggered when moving using click and drag (pan)
// and does correctly update the scrollPane viewportbounds.
// BUT this is not trigered when moving by clocking on hbar or vbar or by using a touchpad (on mac) without clicking.
scrollPane.viewportBoundsProperty().addListener(listener);
// display the app
Scene scene = new Scene(scrollPane, 640, 480);
primaryStage.setScene(scene);
primaryStage.show();
}
@Override
public void stop() throws Exception {
logger.debug("stop");
}
}
有人可以指导我如何在任何滚动操作(单击并拖动平移或使用触摸板或使用水平或垂直条平移)上动态计算(或获取)此偏移量吗?
提前致谢。
感谢 @James_D 的评论,这里有一个解决问题的方法(见下文)。
如果有人对JavaFX有更好的理解ScrollPane
请不要犹豫解释
- 单击并滚动更新
ScrollPane
的视口
- vs 使用不更新
ScrollPane
. 视口的水平和垂直条滚动
这是从 James_D 的评论中摘录的解决方案:
public class DemoScrollPane extends javafx.application.Application {
private Logger logger = LoggerFactory.getLogger(DemoScrollPane.class);
public static void run(String[] args) {
javafx.application.Application.launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
// create content
Pane pane = new Pane();
pane.setMaxSize(1000,1000);
Rectangle rectangle = new Rectangle(1000,1000, Color.AQUA);
Rectangle rectangleInitialVP = new Rectangle(640,480, Color.GRAY);
pane.getChildren().add(rectangle);
pane.getChildren().add(rectangleInitialVP);
// create scrollPane
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(pane);
scrollPane.setVmin(0);
scrollPane.setVmax(pane.getMaxHeight());
scrollPane.setHmin(0);
scrollPane.setHmax(pane.getMaxWidth());
// add click and drag interaction
scrollPane.setPannable(true);
// register a watcher on scrollPane viewportBoundsProperty
InvalidationListener listener =
o -> {
Bounds movingViewPort = new BoundingBox(scrollPane.getHvalue(),
scrollPane.getVvalue(),
0.,
scrollPane.getViewportBounds().getWidth(),
scrollPane.getViewportBounds().getHeight(),
0.
);
logger.debug("viewport {}",movingViewPort);
};
// This is triggered when moving using click and drag (pan)
// and does correctly update the scrollPane viewportbounds.
// BUT this is not trigered when moving by clocking on hbar or vbar or by using a touchpad (on mac)
// without clicking.
// (even tough the viewport obviously change)
scrollPane.vvalueProperty().addListener(listener);
scrollPane.hvalueProperty().addListener(listener);
// display the app
Scene scene = new Scene(scrollPane, 640, 480);
primaryStage.setScene(scene);
primaryStage.show();
}
@Override
public void stop() throws Exception {
logger.debug("stop");
}
}
关于差异问题的简单答案是 :: 这就是它的实现方式!!
首先,每次 scrollPane 尝试布局其子项时,viewPort 边界都会更新。 ScrollPaneSkinclass中的layoutChildren方法偷窥如下:
protected void layoutChildren(double x, double y, double w, double h) {
ScrollPane control = (ScrollPane)this.getSkinnable();
... // All the stuff for layouting
...
control.setViewportBounds(new BoundingBox(this.snapPosition(this.viewContent.getLayoutX()), this.snapPosition(this.viewContent.getLayoutY()), this.snapSize(this.contentWidth), this.snapSize(this.contentHeight)));
}
现在如果我们检查ScrollPane的“pannable”属性的相关实现,代码如下(在ScrollPaneSkin class):
this.viewRect.setOnDragDetected((e) -> {
if (IS_TOUCH_SUPPORTED) {
this.startSBReleasedAnimation();
}
if (((ScrollPane)this.getSkinnable()).isPannable()) {
this.dragDetected = true;
if (this.saveCursor == null) {
this.saveCursor = ((ScrollPane)this.getSkinnable()).getCursor();
if (this.saveCursor == null) {
this.saveCursor = Cursor.DEFAULT;
}
((ScrollPane)this.getSkinnable()).setCursor(Cursor.MOVE);
((ScrollPane)this.getSkinnable()).requestLayout(); // !! THIS LINE !!
}
}
});
this.viewRect.addEventFilter(MouseEvent.MOUSE_RELEASED, (e) -> {
this.mouseDown = false;
if (this.dragDetected) {
if (this.saveCursor != null) {
((ScrollPane)this.getSkinnable()).setCursor(this.saveCursor);
this.saveCursor = null;
((ScrollPane)this.getSkinnable()).requestLayout();// !! THIS LINE !!
}
this.dragDetected = false;
}
if ((this.posY > ((ScrollPane)this.getSkinnable()).getVmax() || this.posY < ((ScrollPane)this.getSkinnable()).getVmin() || this.posX > ((ScrollPane)this.getSkinnable()).getHmax() || this.posX < ((ScrollPane)this.getSkinnable()).getHmin()) && !this.touchDetected) {
this.startContentsToViewport();
}
});
从上面的代码可以看出,当“pannable”属性为真时,如果检测到拖动,它会请求布局。在下一个场景脉冲中,它将调用 layoutChildren 并更新 viewportBounds。当您释放鼠标时(如果检测到拖动)也会发生同样的事情。这就是为什么您只能在平移开始和结束时看到日志,而不是在拖动时看到日志。
现在进入hbar/vbar拖动的代码,更新scrollBar值时的代码如下:(在ScrollPaneSkin class)
InvalidationListener vsbListener = (valueModel) -> {
if (!IS_TOUCH_SUPPORTED) {
this.posY = Utils.clamp(((ScrollPane)this.getSkinnable()).getVmin(), this.vsb.getValue(), ((ScrollPane)this.getSkinnable()).getVmax());
} else {
this.posY = this.vsb.getValue();
}
this.updatePosY();
};
this.vsb.valueProperty().addListener(vsbListener);
InvalidationListener hsbListener = (valueModel) -> {
if (!IS_TOUCH_SUPPORTED) {
this.posX = Utils.clamp(((ScrollPane)this.getSkinnable()).getHmin(), this.hsb.getValue(), ((ScrollPane)this.getSkinnable()).getHmax());
} else {
this.posX = this.hsb.getValue();
}
this.updatePosX();
};
this.hsb.valueProperty().addListener(hsbListener);
值侦听器仅更新 viewContent 的 layoutX/Y 属性 而不是布局请求。 updatePosX方法如下:
private double updatePosX() {
ScrollPane sp = (ScrollPane)this.getSkinnable();
double x = this.isReverseNodeOrientation() ? this.hsb.getMax() - (this.posX - this.hsb.getMin()) : this.posX;
double minX = Math.min(-x / (this.hsb.getMax() - this.hsb.getMin()) * (this.nodeWidth - this.contentWidth), 0.0D);
this.viewContent.setLayoutX(this.snapPosition(minX));
if (!sp.hvalueProperty().isBound()) {
sp.setHvalue(Utils.clamp(sp.getHmin(), this.posX, sp.getHmax()));
}
return this.posX;
}
另一种解法:
+1 @James_D 回答。如果您想保持动态,或者您不知道窗格的最大边界,您可以尝试以下方法。
从上面的代码可以看出,无论是平移还是拖动滚动条,viewContent(scrollPane的内容父节点)的layoutX/Y属性都会更新。因此,您可以将侦听器添加到其 layoutX/Y 属性,这将为您提供准确的位置,而无需将 min/max 值设置为滚动条。
ScrollPane scrollPane = new ScrollPane();
scrollPane.setContent(pane);
scrollPane.getContent().parentProperty().addListener((obs, old, viewContent) -> {
if (viewContent != null) {
InvalidationListener posListener = e -> {
double x = viewContent.getLayoutX() * -1;
double y = viewContent.getLayoutY() * -1;
double width = scrollPane.getViewportBounds().getWidth();
double height = scrollPane.getViewportBounds().getHeight();
Bounds movingViewPort = new BoundingBox(x, y, width, height);
System.out.println("viewport: " + movingViewPort);
};
viewContent.layoutXProperty().addListener(posListener);
viewContent.layoutYProperty().addListener(posListener);
}
});
scrollPane.setPannable(true);