ScrollPane content becomes blurry after dragging

2019-05-03 17:23发布

问题:

JavaFX 8.0 has this bug and I don't know how to solve it.
Example: http://i.imgur.com/tavh6XA.png

If I drag ScrollPane, its content becomes blurry, but if I drag it back, the content restores its sharpness. If I do not modify coords, the content looks pretty well.

Did anybody solve this problem?

Example code:

@Override
public void start(Stage stage) {
            Pane pane = new Pane();

    Scene scene = new Scene(pane, 500, 500);
    pane.prefWidthProperty().bind(scene.widthProperty());
    pane.prefHeightProperty().bind(scene.heightProperty());

    ScrollPane scroll = new ScrollPane();

    // Center ScrollPane
    scroll.layoutXProperty().bind(pane.widthProperty().divide(2).subtract(scroll.widthProperty().divide(2)));
    scroll.layoutYProperty().bind(pane.heightProperty().divide(2).subtract(scroll.heightProperty().divide(2)));
    scroll.setPrefSize(200, 200);

    ListView<String> list = new ListView<>();
    scroll.setContent(list);

    list.getItems().addAll("Resize the window", "and see how this list", "becomes blurry");

    // Label indicates x, y coords of ScrolPane and their ratio.
    TextField size = new TextField();
    size.setPrefWidth(500);
    size.textProperty().bind(
            scroll.layoutXProperty()
            .asString("x: %s; ").concat(
                    scroll.layoutYProperty()
                    .asString("y: %s; ")));

    pane.getChildren().addAll(size, scroll);

    stage.setScene(scene);
    stage.show();
}

回答1:

Recommended Approach - Use built-in layout managers + sizing hints

I recommend not setting the layout values for the node unless you need to. Instead if you use standard layout containers, then it (should) by default snap any non-integer layout co-ordinates to integral values allowing clean layout of the system without potential bluriness caused by anti-aliasing of fractional layout values that cause text and lines to not align directly on the screen pixel grid.

Sample Code Without Fuzziness

When I ran a modified sample on my machine which does not perform layout by binding but instead does layout only using layout managers, I did not observe and blurry values (win 7, JavaFX 8u20). I also did not need to modify the default node caching hints.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;

public class NonBlurryPane extends Application {

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

    @Override
    public void start(Stage stage) {
        VBox pane = new VBox();

        pane.setPrefSize(500, 500);

        Scene scene = new Scene(pane);

        ScrollPane scroll = new ScrollPane();
        StackPane scrollContainer = new StackPane(scroll);
        VBox.setVgrow(scrollContainer, Priority.ALWAYS);

        // Center ScrollPane
        scroll.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
        scroll.setPrefSize(200, 200);
        scroll.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);

        ListView<String> list = new ListView<>();
        scroll.setContent(list);

        list.getItems().addAll("Resize the window", "and see how this list", "does NOT become blurry");

        // Label indicates x, y coords of ScrolPane and their ratio.
        TextField size = new TextField();
        size.setMaxWidth(Double.MAX_VALUE);
        size.setMinHeight(Region.USE_PREF_SIZE);
        size.textProperty().bind(
                scroll.layoutXProperty()
                        .asString("x: %s; ").concat(
                        scroll.layoutYProperty()
                                .asString("y: %s; ")).concat(
                        scroll.layoutXProperty().divide(scroll.layoutYProperty())
                                .asString("x/y: %s; ")).concat(
                        scroll.layoutYProperty().divide(scroll.layoutXProperty())
                                .asString("y/x: %s")));

        pane.getChildren().addAll(size, scrollContainer);

        stage.setScene(scene);
        stage.show();
    }
}

I also noticed that default layout managers round double coords to integer coords. Is that possible to make the number bindings work in this way?

Binding Integer Co-ordinates

Integer co-ordinates are a bit of a simplification. Sometimes you want co-ordinates which are x.5 (see How to draw a crisp, opaque hairline in JavaFX 2.2? for more details). It's a detail you don't need to worry about usually with the standard layout panes as they by default will handle clean snapping.

There is no kind of floor or round function in the standard bindings routines for Java 8. Either you can use invalidation listeners or change listeners and update based on them (instead of binding), or you can develop a custom binding which rounds to the appropriate value. For more information on these options, see the "Exploring Observable, ObservableValue, InvalidationListener, and ChangeListener" and "Using the Low-Level Binding API" sections of the Oracle JavaFX properties tutorial.

Layout Tip

The preferred way to do layout is (from easiest to more complex):

  1. Use a layout manager.
  2. Bindings or change listeners are (usually) OK for very simple layout tweaks.
  3. Override layoutChildren() in a parent for anything modestly custom and complex.

On layoutChildren()

Most of the custom layout calculations in the in-built JavaFX controls and layout managers are handled by over-riding layoutChildren() and not through bindings. You can review the JavaFX source code and study how these are implemented. This is necessary to understand how to use layoutChildren (as I have not seen it thoroughly documented anywhere). Overriding layoutChildren is a little advanced and tricky to do well. To get predictable quality results from your layout algorithm, you need to manually account for things like padding insets, pixel snapping, accurately measuring the size of child components, etc.



回答2:

I tried the thebradness's answer, but I get a java.lang.reflect.InaccessibleObjectException (Java 10) probably because of the Java Platform Module System. Rather than searching a workaround for making the reflection working, I prefered looking for another way to achieve it.

After checking the ScrollPaneSkin internal code, I noticed that the viewport css class is given to the viewRect argument. So I can Node#lookup it via CSS selectors rather than get it via reflection.

It's a simplier and more readable solution (and working in JAVA 9+) :

public void fixBlurryText(ScrollPane scrollPane) {
    StackPane stackPane = (StackPane) scrollPane.lookup("StackPane .viewport");
    stackPane.setCache(false);
}

Hope it will help someone else.



回答3:

I checked the internal skin code and found a workaround, not pretty though. The problem is inside com.sun.javafx.scene.control.skin.ScrollPaneSkin, which calls viewRect.setCache(true). Disabling cache appears to solve the problem.

PS: The cache property is also set to true in TitledPane, which has the same problem since JavaFX 8.

ScrollPane2.java

<!-- language: lang-java -->

import javafx.scene.control.ScrollPane;
import javafx.scene.control.Skin;

public class ScrollPane2 extends ScrollPane
{
    @Override
    protected Skin<?> createDefaultSkin()
    {
        return new ScrollPaneSkin2(this);
    }
}

ScrollPaneSkin2.java

<!-- language: lang-java -->
import java.lang.reflect.Field;

import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;

import com.sun.javafx.scene.control.skin.ScrollPaneSkin;

class ScrollPaneSkin2 extends ScrollPaneSkin
{
    private static Field viewRectField;

    static
    {
        try
        {
            viewRectField = ScrollPaneSkin.class.getDeclaredField("viewRect");
            viewRectField.setAccessible(true);
        }
        catch (ReflectiveOperationException e)
        {
            throw new RuntimeException(e);
        }
    }

    private StackPane viewRect;

    public ScrollPaneSkin2(final ScrollPane scrollpane)
    {
        super(scrollpane);
        try
        {
            this.viewRect = (StackPane) viewRectField.get(this);
        }
        catch (ReflectiveOperationException e)
        {
            throw new RuntimeException(e);
        }
        this.viewRect.setCache(false);
    }
}

It may lower performance to have cache disabled, but I haven't noticed any slowdown.



回答4:

import com.sun.javafx.scene.control.skin.ScrollPaneSkin;
import java.lang.reflect.Field;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;

public class FieldUtils {

    public static void fixBlurryText(Node node) {
        try {
            Field field = ScrollPaneSkin.class.getDeclaredField("viewRect");
            field.setAccessible(true);

            ScrollPane scrollPane = (ScrollPane) node.lookup(".scroll-pane");

            StackPane stackPane = (StackPane) field.get(scrollPane.getSkin());
            stackPane.setCache(false);

        } catch (NoSuchFieldException | SecurityException |  IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
  • Thank you AqD for figuring this out.
  • I used the answer from AqD to write this little utility. The issue I had was with TextAreas inside a PopOver. So this just finds the stackPane deep in the node and switches off caching.
  • Note: Because this issue only occurred inside of a PopOver, I had to call this method after showing the PopOver (because otherwise the node doesn't exist yet and neither do any of its guts).
  • If you're having a problem with WebView, forget it. WebView has no scroll pane (and as far as I can tell no fields that use setCache) and I have no idea how to fix this problem with it.
  • Hopefully this will help someone even though this question is from 3 years ago.



回答5:

If I apply the following style to the ScrollPane, the blur disappears.

.scroll-pane {
       -fx-background-color: -fx-box-border,-fx-background;
       -fx-background-insets: 0, 1;
       -fx-padding: 1.0;
}

This is simply the style from the caspian.css from jfxrt.jar.

I suspected that the "-fx-background-insets: -1.4, 0, 1;" from ".scroll-pane:focused" are responsible for the non-integer offset. Therefore causing the blur. So I worked my way back from the complete caspian style for the ScrollPane. After removing the background insest everything stayed sharp. I tried to reduce the sytle code as much as possible and got to the solution presented above.



标签: java javafx-8