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/resourcesjava.custom.properties):

gluon.experimental.performance=true
com.sun.javafx.gestures.scroll.inertia.velocity=2000

如果可行,可以将其提交至 JavaFXPorts 问题或作为 PR。