Gluon Mobile ScrollPane 优化
Gluon Mobile ScrollPane Optimization
我正在尝试使用嵌套的 VBox 实现 JavaFX ScrollPane,但在移动设备中滚动速度过快时我遇到了一个奇怪的问题。问题是,如果我以较小但快速向上的手势滚动,滚动窗格会先停顿半秒,然后继续。它在随机滑动手势中变得迟钝。无论如何我可以优化这个吗?
这不会发生在桌面版本上,所以我猜这可能是我的 phone 和 JavaFX 本身的限制。 当我为滚动窗格视口设置背景图像时,这个问题更加明显。下面是一些视图示例代码:
public class StackUserView extends View {
private Label label;
public StackUserView(String name) {
super(name);
initDisplay();
}
private void initDisplay() {
this.label = new Label("10");
label.setFont(Font.font(40d));
VBox box = new VBox();
box.setSpacing(10);
box.setAlignment(Pos.CENTER_RIGHT);
for(int i = 0; i < 100; i++){
Label label = new Label("HELLO");
label.setCache(true);
label.setCacheHint(CacheHint.SPEED);
label.setCacheShape(true);
box.getChildren().add(label);
}
ScrollPane pane = new ScrollPane(box);
pane.setCacheHint(CacheHint.QUALITY);
pane.setFitToHeight(true);
pane.setFitToHeight(true);
this.setCenter(pane);
}
@Override
protected void updateAppBar(AppBar appBar) {
appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> MobileApplication.getInstance().showLayer(SpeedSelect.MENU_LAYER)));
appBar.setTitleText(this.getName());
appBar.getActionItems().add(MaterialDesignIcon.SEARCH.button(e -> System.out.println("Search")));
}
}
最终,我试图让它滚动到尽可能接近实际的本机移动环境。
我已经能够优化需要滚动的情况,创建一个带有 HBox 作为单元格的自定义 CharmListView,但是我无法将 SceneBuilder 用于我的视图,这会影响可维护性。
注意:我正在使用 LG G5 进行测试。
这是该问题的简短视频。滑动后即可看到。滚动停止一秒钟然后继续:
所以我通过使用本机滚动事件并对其进行自定义来解决这个问题。 我基本上使用了一种反复试验的方法来弄清楚我需要什么样的硬值(下面的因素)才能让它滚动正常。
private SimpleDoubleProperty location = new SimpleDoubleProperty();
private SimpleDoubleProperty vValue = new SimpleDoubleProperty(0);
private SimpleLongProperty startTime = new SimpleLongProperty();
private SimpleLongProperty timer = new SimpleLongProperty();
private double height = MobileApplication.getInstance().getGlassPane().getHeight();
private Animation animation;
private Animation callbackAnimation;
private void scrollOverride() {
sp.addEventFilter(ScrollEvent.SCROLL, event -> {
event.consume();
});
sp.setOnTouchPressed(e -> {
if (callbackAnimation != null){
callbackAnimation.stop();
}
if (animation != null) {
animation.stop();
}
vValue.set(sp.vvalueProperty().get());
location.set(e.getTouchPoint().getY());
startTime.set((new Date()).getTime());
});
sp.setOnTouchMoved(e -> {
timer.set((new Date()).getTime());
});
sp.setOnTouchReleased(e -> {
Boolean dontOverride = false;
double deltaTime = (new Date()).getTime() - startTime.get();
double deltaY = sp.vvalueProperty().get() - vValue.get();
if(Math.abs(deltaY) < 0.05){
dontOverride = true;
}
double translation = deltaY / (deltaTime/300);
double value = 0d;
if (Math.abs(timer.get() - startTime.get()) < 500) {
value = sp.vvalueProperty().get() + translation;
} else {
value = sp.vvalueProperty().get();
dontOverride = true;
}
animation = new Timeline(
new KeyFrame(Duration.millis(deltaTime + 100),
new KeyValue(sp.vvalueProperty(), value)));
animation.play();
if (!dontOverride) {
animation.setOnFinished(evt -> {
boolean innerUp = (sp.vvalueProperty().get() - vValue.get()) < 0;
int innerSign = 1;
if (innerUp) {
innerSign = -1;
}
callbackAnimation = new Timeline(
new KeyFrame(Duration.millis(deltaTime + 150),
new KeyValue(sp.vvalueProperty(), sp.vvalueProperty().get() + (0.1 * innerSign), Interpolator.EASE_OUT)));
callbackAnimation.play();
});
}
});
}
您可以通过执行以下操作将此覆盖限制为仅在移动设备上发生:
if(!com.gluonhq.charm.down.Platform.isDesktop()){
scrollOverride();
}
如果有人可以对此进行优化或提出自己的解决方案,那就太好了。同样,我的目标主要是防止滚动时出现延迟停止。
在我的 iPhone 7+ 上的应用程序中,这实际上运行良好。它的效果与我将浏览器注入带有 Web 视图的应用程序时效果相当。
A.Sharma 提供的答案解决了一些问题,因为它完全删除了内置 ScrollPane
事件处理的底层实现。
当然,要彻底解决这个问题,JavaFXPorts中需要一个合适的解决方案。
与此同时,另一种可能的方法,可以通过直接在 ScrollPaneSkin
中解决问题来与现有实施更紧密地合作,而 class 则负责事件处理控件。
class可以找到here。
好消息是这个 class 可以克隆到您的项目(即 MyScrollPaneSkin
),修改并添加为 new 皮肤用于现有的ScrollPane
控制。
可能的修复
经过一些测试,问题的罪魁祸首可以定位在 contentsToViewTimeline
动画中。它执行 1.35 秒暂停以:
block out 'aftershocks'
然后在滚动事件处理程序中有这一行,它检查动画是否已经结束并且仅在结束之后更新滚动条位置:
if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) &&
(contentsToViewTimeline == null ||
contentsToViewTimeline.getStatus() == Status.STOPPED)) {
vsb.setValue(newValue);
...
}
虽然不应删除此动画,但即使动画处于空闲状态也应更新滚动条。
我做了以下更改,将第 517-527 行替换为:
/*
** if there is a repositioning in progress then we only
** set the value for 'real' events
*/
if ((newValue <= vsb.getMax() && newValue >= vsb.getMin())) {
vsb.setValue(newValue);
}
boolean stop = ! ((ScrollEvent) event).isInertia() || ((ScrollEvent) event).isInertia() && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED);
if ((newValue > vsb.getMax() || newValue < vsb.getMin()) && (!mouseDown && !touchDetected) && stop) {
startContentsToViewport();
}
if (stop) {
event.consume();
}
在您的项目中,现在更换内置皮肤:
ScrollPane pane = new ScrollPane(box);
pane.setSkin(new MyScrollPaneSkin(pane));
我已经在 Android 和 iOS 上进行了测试,在这两种情况下,动画都很流畅,没有任何问题的迹象。
此外,这些属性可能会有所帮助(使用添加到 /src/main/resources
的 java.custom.properties
):
gluon.experimental.performance=true
com.sun.javafx.gestures.scroll.inertia.velocity=2000
如果可行,可以将其提交至 JavaFXPorts 问题或作为 PR。
我正在尝试使用嵌套的 VBox 实现 JavaFX ScrollPane,但在移动设备中滚动速度过快时我遇到了一个奇怪的问题。问题是,如果我以较小但快速向上的手势滚动,滚动窗格会先停顿半秒,然后继续。它在随机滑动手势中变得迟钝。无论如何我可以优化这个吗?
这不会发生在桌面版本上,所以我猜这可能是我的 phone 和 JavaFX 本身的限制。 当我为滚动窗格视口设置背景图像时,这个问题更加明显。下面是一些视图示例代码:
public class StackUserView extends View {
private Label label;
public StackUserView(String name) {
super(name);
initDisplay();
}
private void initDisplay() {
this.label = new Label("10");
label.setFont(Font.font(40d));
VBox box = new VBox();
box.setSpacing(10);
box.setAlignment(Pos.CENTER_RIGHT);
for(int i = 0; i < 100; i++){
Label label = new Label("HELLO");
label.setCache(true);
label.setCacheHint(CacheHint.SPEED);
label.setCacheShape(true);
box.getChildren().add(label);
}
ScrollPane pane = new ScrollPane(box);
pane.setCacheHint(CacheHint.QUALITY);
pane.setFitToHeight(true);
pane.setFitToHeight(true);
this.setCenter(pane);
}
@Override
protected void updateAppBar(AppBar appBar) {
appBar.setNavIcon(MaterialDesignIcon.MENU.button(e -> MobileApplication.getInstance().showLayer(SpeedSelect.MENU_LAYER)));
appBar.setTitleText(this.getName());
appBar.getActionItems().add(MaterialDesignIcon.SEARCH.button(e -> System.out.println("Search")));
}
}
最终,我试图让它滚动到尽可能接近实际的本机移动环境。
我已经能够优化需要滚动的情况,创建一个带有 HBox 作为单元格的自定义 CharmListView,但是我无法将 SceneBuilder 用于我的视图,这会影响可维护性。
注意:我正在使用 LG G5 进行测试。
这是该问题的简短视频。滑动后即可看到。滚动停止一秒钟然后继续:
所以我通过使用本机滚动事件并对其进行自定义来解决这个问题。 我基本上使用了一种反复试验的方法来弄清楚我需要什么样的硬值(下面的因素)才能让它滚动正常。
private SimpleDoubleProperty location = new SimpleDoubleProperty();
private SimpleDoubleProperty vValue = new SimpleDoubleProperty(0);
private SimpleLongProperty startTime = new SimpleLongProperty();
private SimpleLongProperty timer = new SimpleLongProperty();
private double height = MobileApplication.getInstance().getGlassPane().getHeight();
private Animation animation;
private Animation callbackAnimation;
private void scrollOverride() {
sp.addEventFilter(ScrollEvent.SCROLL, event -> {
event.consume();
});
sp.setOnTouchPressed(e -> {
if (callbackAnimation != null){
callbackAnimation.stop();
}
if (animation != null) {
animation.stop();
}
vValue.set(sp.vvalueProperty().get());
location.set(e.getTouchPoint().getY());
startTime.set((new Date()).getTime());
});
sp.setOnTouchMoved(e -> {
timer.set((new Date()).getTime());
});
sp.setOnTouchReleased(e -> {
Boolean dontOverride = false;
double deltaTime = (new Date()).getTime() - startTime.get();
double deltaY = sp.vvalueProperty().get() - vValue.get();
if(Math.abs(deltaY) < 0.05){
dontOverride = true;
}
double translation = deltaY / (deltaTime/300);
double value = 0d;
if (Math.abs(timer.get() - startTime.get()) < 500) {
value = sp.vvalueProperty().get() + translation;
} else {
value = sp.vvalueProperty().get();
dontOverride = true;
}
animation = new Timeline(
new KeyFrame(Duration.millis(deltaTime + 100),
new KeyValue(sp.vvalueProperty(), value)));
animation.play();
if (!dontOverride) {
animation.setOnFinished(evt -> {
boolean innerUp = (sp.vvalueProperty().get() - vValue.get()) < 0;
int innerSign = 1;
if (innerUp) {
innerSign = -1;
}
callbackAnimation = new Timeline(
new KeyFrame(Duration.millis(deltaTime + 150),
new KeyValue(sp.vvalueProperty(), sp.vvalueProperty().get() + (0.1 * innerSign), Interpolator.EASE_OUT)));
callbackAnimation.play();
});
}
});
}
您可以通过执行以下操作将此覆盖限制为仅在移动设备上发生:
if(!com.gluonhq.charm.down.Platform.isDesktop()){
scrollOverride();
}
如果有人可以对此进行优化或提出自己的解决方案,那就太好了。同样,我的目标主要是防止滚动时出现延迟停止。
在我的 iPhone 7+ 上的应用程序中,这实际上运行良好。它的效果与我将浏览器注入带有 Web 视图的应用程序时效果相当。
A.Sharma 提供的答案解决了一些问题,因为它完全删除了内置 ScrollPane
事件处理的底层实现。
当然,要彻底解决这个问题,JavaFXPorts中需要一个合适的解决方案。
与此同时,另一种可能的方法,可以通过直接在 ScrollPaneSkin
中解决问题来与现有实施更紧密地合作,而 class 则负责事件处理控件。
class可以找到here。
好消息是这个 class 可以克隆到您的项目(即 MyScrollPaneSkin
),修改并添加为 new 皮肤用于现有的ScrollPane
控制。
可能的修复
经过一些测试,问题的罪魁祸首可以定位在 contentsToViewTimeline
动画中。它执行 1.35 秒暂停以:
block out 'aftershocks'
然后在滚动事件处理程序中有这一行,它检查动画是否已经结束并且仅在结束之后更新滚动条位置:
if (!(((ScrollEvent)event).isInertia()) || (((ScrollEvent)event).isInertia()) &&
(contentsToViewTimeline == null ||
contentsToViewTimeline.getStatus() == Status.STOPPED)) {
vsb.setValue(newValue);
...
}
虽然不应删除此动画,但即使动画处于空闲状态也应更新滚动条。
我做了以下更改,将第 517-527 行替换为:
/*
** if there is a repositioning in progress then we only
** set the value for 'real' events
*/
if ((newValue <= vsb.getMax() && newValue >= vsb.getMin())) {
vsb.setValue(newValue);
}
boolean stop = ! ((ScrollEvent) event).isInertia() || ((ScrollEvent) event).isInertia() && (contentsToViewTimeline == null || contentsToViewTimeline.getStatus() == Status.STOPPED);
if ((newValue > vsb.getMax() || newValue < vsb.getMin()) && (!mouseDown && !touchDetected) && stop) {
startContentsToViewport();
}
if (stop) {
event.consume();
}
在您的项目中,现在更换内置皮肤:
ScrollPane pane = new ScrollPane(box);
pane.setSkin(new MyScrollPaneSkin(pane));
我已经在 Android 和 iOS 上进行了测试,在这两种情况下,动画都很流畅,没有任何问题的迹象。
此外,这些属性可能会有所帮助(使用添加到 /src/main/resources
的 java.custom.properties
):
gluon.experimental.performance=true
com.sun.javafx.gestures.scroll.inertia.velocity=2000
如果可行,可以将其提交至 JavaFXPorts 问题或作为 PR。