创建简单的自定义滚动视图

时间:2018-07-16 20:06:05

标签: java javafx scroll javafx-8 scrollview

这是关于滚动视图的一个一般性问题,我想学习滚动视图的基础知识以及如何独自实现滚动视图,因为它是大多数动态GUI的必要组成部分。您可能会问,为什么不简单使用平台提供的功能呢?我的回答是,除了学习新知识很有趣之外,很高兴看到按照您希望的方式定制的东西。简而言之,我只想创建一个简单的自定义滚动视图,并尝试了解它在后台的工作方式。

继续,我目前在这里要呈现的只是我想到的UI的最简单示例。基本上,它是Pane,它充当整个内容的视口,并且在其右侧边缘上包含一个垂直的滚动条,就像普通的滚动视图一样,但是我只是添加了一点过渡,以动画化滚动条在鼠标上的宽度徘徊。

ScrollContainer

import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;

/**
 * ScrollContainer
 *
 * A container for scrolling large content.
 */
public class ScrollContainer extends Pane {

    private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
    private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content

    /**
     * Construct a new ScrollContainer
     */
    public ScrollContainer() {
        super();

        scrollBar = new VerticalScrollBar();
        getChildren().add(scrollBar);

        rectangle = new Rectangle();
        rectangle.widthProperty().bind(widthProperty());
        rectangle.heightProperty().bind(heightProperty());
        setClip(rectangle);
    }

    @Override
    protected void layoutChildren() {
        super.layoutChildren();

        // Layout scrollbar to the edge of container, and fit the viewport's height as well
        scrollBar.resize(scrollBar.getWidth(), getHeight());
        scrollBar.setLayoutX(getWidth() - scrollBar.getWidth());
    }

    /**
     * VerticalScrollBar
     */
    private class VerticalScrollBar extends Region {
        // Temporary scrubber's height.
        // TODO: Figure out the computation for scrubber's height.
        private static final double SCRUBBER_LENGTH = 100;

        private double initialY; // Initial mouse position when dragging the scrubber
        private Timeline widthTransition; // Transforms width of scrollbar on hover
        private Region scrubber; // Indicator about the content's visible area

        /**
         * Construct a new VerticalScrollBar
         */
        private VerticalScrollBar() {
            super();

            // Scrollbar's initial width
            setPrefWidth(7);

            widthTransition = new Timeline(
                    new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
                    new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
            );

            scrubber = new Region();
            scrubber.setStyle("-fx-background-color: rgba(0,0,0, 0.25)");
            scrubber.setOnMousePressed(event -> initialY = event.getY());
            scrubber.setOnMouseDragged(event -> {
                // Moves the scrubber vertically within the scrollbar.
                // TODO: Figure out the proper way of handling scrubber movement, an onScroll mouse wheel function, ect.
                double initialScrollY = event.getSceneY() - initialY;
                double maxScrollY = getHeight() - SCRUBBER_LENGTH;
                double minScrollY = 0;
                if (initialScrollY >= minScrollY && initialScrollY <= maxScrollY) {
                    scrubber.setTranslateY(initialScrollY);
                }
            });
            getChildren().add(scrubber);

            // Animate scrollbar's width on mouse enter and exit
            setOnMouseEntered(event -> {
                widthTransition.setRate(1);
                widthTransition.play();
            });
            setOnMouseExited(event -> {
                widthTransition.setRate(-1);
                widthTransition.play();
            });
        }

        @Override
        protected void layoutChildren() {
            super.layoutChildren();

            // Layout scrubber to fit the scrollbar's width
            scrubber.resize(getWidth(), SCRUBBER_LENGTH);
        }
    }
}

主要

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

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) {
        Label lorem = new Label();
        lorem.setStyle("-fx-padding: 20px;");
        lorem.setText("Lorem ipsum dolor sit amet, consectetur adipiscing elit. " +
                "Integer ut ornare enim, a rutrum nisl. " +
                "Proin eros felis, rutrum at pharetra viverra, elementum quis lacus. " +
                "Nam sit amet sollicitudin nibh, ac mattis lectus. " +
                "Sed mattis ullamcorper sapien, a pulvinar turpis hendrerit vel. " +
                "Fusce nec diam metus. In vel dui lacus. " +
                "Sed imperdiet ipsum euismod aliquam rhoncus. " +
                "Morbi sagittis mauris ac massa pretium, vel placerat purus porta. " +
                "Suspendisse orci leo, sagittis eu orci vitae, porttitor sagittis odio. " +
                "Proin iaculis enim sed ipsum sodales, at congue ante blandit. " +
                "Etiam mattis erat nec dolor vestibulum, quis interdum sem pellentesque. " +
                "Nullam accumsan ex non lacus sollicitudin interdum.");
        lorem.setWrapText(true);

        StackPane content = new StackPane();
        content.setPrefSize(300, 300);
        content.setMinSize(300, 300);
        content.setMaxSize(300, 300);
        content.setStyle("-fx-background-color: white;");
        content.getChildren().add(lorem);

        ScrollContainer viewport = new ScrollContainer();
        viewport.setStyle("-fx-background-color: whitesmoke");
        viewport.getChildren().add(0, content);

        primaryStage.setScene(new Scene(viewport, 300, 150));
        primaryStage.show();
    }

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

我想看一个工作示例,该示例仅显示滚动的基本技巧;例如处理拇指动画运动的正确方法,滚动条的拇指长度的计算以及最后移动内容所需的总单位或数量。我认为这三个部分是滚动视图核心的关键。

P.S
我还希望看到JavaFX中onScroll事件的使用,现在我所知道的只是常用的鼠标事件。预先谢谢你。


更新

我在下面的@fabian先生的答案中添加了BlockIncrement函数。基本上,将拇指移到指针的当前位置,同时保持[0,1]范围值。所有的感谢和感谢归于他。

  

这是针对那些正在寻找类似这种想法的人的   自定义滚动视图,希望以后对本参考有用。

public class ScrollContainer extends Region {

    private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
    private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content

    /**
     * Construct a new ScrollContainer
     */
    public ScrollContainer() {
        setOnScroll(evt -> {
            double viewportHeight = getHeight();
            double contentHeight = getContentHeight();
            if (contentHeight > viewportHeight) {
                double delta = evt.getDeltaY() / (viewportHeight - contentHeight);
                if (Double.isFinite(delta)) {
                    scrollBar.setValue(scrollBar.getValue() + delta);
                }
            }
        });

        scrollBar = new VerticalScrollBar();
        getChildren().add(scrollBar);

        rectangle = new Rectangle();
        setClip(rectangle);
    }

    private Node content;

    public void setContent(Node content) {
        if (this.content != null) {
            // remove old content
            getChildren().remove(this.content);
        }
        if (content != null) {
            // add new content
            getChildren().add(0, content);
        }
        this.content = content;
    }

    private double getContentHeight() {
        return content == null ? 0 : content.getLayoutBounds().getHeight();
    }

    @Override
    protected void layoutChildren() {
        super.layoutChildren();

        double w = getWidth();
        double h = getHeight();

        double sw = scrollBar.getWidth();

        double viewportWidth = w - sw;
        double viewportHeight = h;

        if (content != null) {
            double contentHeight = getContentHeight();
            double vValue = scrollBar.getValue();

            // position content according to scrollbar value
            content.setLayoutY(Math.min(0, viewportHeight - contentHeight) * vValue);
        }

        // Layout scrollbar to the edge of container, and fit the viewport's height as well
        scrollBar.resize(sw, h);
        scrollBar.setLayoutX(viewportWidth);

        // resize clip
        rectangle.setWidth(w);
        rectangle.setHeight(h);
    }

    /**
     * VerticalScrollBar
     */
    private class VerticalScrollBar extends Region {

        private boolean thumbPressed; // Indicates that the scrubber was pressed

        private double initialValue;
        private double initialY; // Initial mouse position when dragging the scrubber
        private Timeline widthTransition; // Transforms width of scrollbar on hover
        private Region scrubber; // Indicator about the content's visible area

        private double value;

        private void setValue(double v) {
            value = v;
        }

        private double getValue() {
            return value;
        }

        private double calculateScrubberHeight() {
            double h = getHeight();
            return h * h / getContentHeight();
        }

        /**
         * Construct a new VerticalScrollBar
         */
        private VerticalScrollBar() {
            // Scrollbar's initial width
            setPrefWidth(7);

            widthTransition = new Timeline(
                    new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
                    new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
            );

            scrubber = new Region();
            scrubber.setStyle("-fx-background-color: rgba(0,0,0, 0.25)");
            scrubber.setOnMousePressed(event -> {
                initialY = scrubber.localToParent(event.getX(), event.getY()).getY();
                initialValue = value;
                thumbPressed = true;
            });
            scrubber.setOnMouseDragged(event -> {
                if (thumbPressed) {
                    double currentY = scrubber.localToParent(event.getX(), event.getY()).getY();
                    double sH = calculateScrubberHeight();
                    double h = getHeight();

                    // calculate value change and prevent errors
                    double delta = (currentY - initialY) / (h - sH);
                    if (!Double.isFinite(delta)) {
                        delta = 0;
                    }

                    // keep value in range [0, 1]
                    double newValue = Math.max(0, Math.min(1, initialValue + delta));
                    value = newValue;

                    // layout thumb
                    requestLayout();
                }
            });
            scrubber.setOnMouseReleased(event -> thumbPressed = false);
            getChildren().add(scrubber);

            // Added BlockIncrement.
            // Pressing the `track` or the scrollbar itself will move and position the
            // scrubber to the pointer location, as well as the content prior to the
            // value changes.
            setOnMousePressed(event -> {
                if (!thumbPressed) {
                    double sH = calculateScrubberHeight();
                    double h = getHeight();
                    double pointerY = event.getY();
                    double delta = pointerY / (h - sH);
                    double newValue = Math.max(0, Math.min(1, delta));

                    // keep value in range [0, 1]
                    if (delta > 1) {
                        newValue = 1;
                    }
                    value = newValue;

                    requestLayout();
                }
            });

            // Animate scrollbar's width on mouse enter and exit
            setOnMouseEntered(event -> {
                widthTransition.setRate(1);
                widthTransition.play();
            });
            setOnMouseExited(event -> {
                widthTransition.setRate(-1);
                widthTransition.play();
            });
        }

        @Override
        protected void layoutChildren() {
            super.layoutChildren();

            double h = getHeight();
            double cH = getContentHeight();

            if (cH <= h) {
                // full size, if content does not excede viewport size
                scrubber.resize(getWidth(), h);
            } else {
                double sH = calculateScrubberHeight();

                // move thumb to position
                scrubber.setTranslateY(value * (h - sH));

                // Layout scrubber to fit the scrollbar's width
                scrubber.resize(getWidth(), sH);
            }
        }
    }
}

1 个答案:

答案 0 :(得分:1)

有一些方程式可让您计算布局(均假设contentHeight > viewportHeight):

vValue表示拇指在[0, 1]中垂直滚动条中的位置(0 =最高位置,1 =拇指底部在音轨的底部)。

topY = vValue * (contentHeight - viewportHeight)
thumbHeight / trackHeight = viewportHeight / contentHeight
thumbY = vValue * (trackHeight - thumbHeight)

还要注意,提供对子项的访问并在ScrollContainer之外添加内容是不好的做法,因为它要求此类的用户进行应为该类本身保留的修改。这样做很容易导致以下行中断ScrollContainer(内容可能会遮挡拇指):

 // viewport.getChildren().add(0, content);
 viewport.getChildren().add(content);

最好直接扩展Region并使用一种方法来(重新)放置内容。

public class ScrollContainer extends Region {

    private VerticalScrollBar scrollBar; // The scrollbar used for scrolling over the content from viewport
    private Rectangle rectangle; // Object for clipping the viewport to restrict overflowing content

    /**
     * Construct a new ScrollContainer
     */
    public ScrollContainer() {
        setOnScroll(evt -> {
            double viewportHeight = getHeight();
            double contentHeight = getContentHeight();
            if (contentHeight > viewportHeight) {
                double delta = evt.getDeltaY() / (viewportHeight - contentHeight);
                if (Double.isFinite(delta)) {
                    scrollBar.setValue(scrollBar.getValue() + delta);
                }
            }
        });

        scrollBar = new VerticalScrollBar();
        getChildren().add(scrollBar);

        rectangle = new Rectangle();
        setClip(rectangle);
    }

    private Node content;

    public void setContent(Node content) {
        if (this.content != null) {
            // remove old content
            getChildren().remove(this.content);
        }
        if (content != null) {
            // add new content
            getChildren().add(0, content);
        }
        this.content = content;
    }

    private double getContentHeight() {
        return content == null ? 0 : content.getLayoutBounds().getHeight();
    }

    @Override
    protected void layoutChildren() {
        super.layoutChildren();

        double w = getWidth();
        double h = getHeight();

        double sw = scrollBar.getWidth();

        double viewportWidth = w - sw;
        double viewportHeight = h;

        if (content != null) {
            double contentHeight = getContentHeight();
            double vValue = scrollBar.getValue();

            // position content according to scrollbar value
            content.setLayoutY(Math.min(0, viewportHeight - contentHeight) * vValue);
        }

        // Layout scrollbar to the edge of container, and fit the viewport's height as well
        scrollBar.resize(sw, h);
        scrollBar.setLayoutX(viewportWidth);

        // resize clip
        rectangle.setWidth(w);
        rectangle.setHeight(h);
    }

    /**
     * VerticalScrollBar
     */
    private class VerticalScrollBar extends Region {

        private double initialValue;
        private double initialY; // Initial mouse position when dragging the scrubber
        private Timeline widthTransition; // Transforms width of scrollbar on hover
        private Region scrubber; // Indicator about the content's visible area

        private double value;

        public double getValue() {
            return value;
        }

        private double calculateScrubberHeight() {
            double h = getHeight();
            return h * h / getContentHeight();
        }

        /**
         * Construct a new VerticalScrollBar
         */
        private VerticalScrollBar() {
            // Scrollbar's initial width
            setPrefWidth(7);

            widthTransition = new Timeline(
                    new KeyFrame(Duration.ZERO, new KeyValue(prefWidthProperty(), 7)),
                    new KeyFrame(Duration.millis(500), new KeyValue(prefWidthProperty(), 14))
            );

            scrubber = new Region();
            scrubber.setStyle("-fx-background-color: rgba(0,0,0, 0.25)");
            scrubber.setOnMousePressed(event -> {
                initialY = scrubber.localToParent(event.getX(), event.getY()).getY();
                initialValue = value;
            });
            scrubber.setOnMouseDragged(event -> {
                double currentY = scrubber.localToParent(event.getX(), event.getY()).getY();
                double sH = calculateScrubberHeight();
                double h = getHeight();

                // calculate value change and prevent errors
                double delta = (currentY - initialY) / (h - sH);
                if (!Double.isFinite(delta)) {
                    delta = 0;
                }

                // keep value in range [0, 1]
                double newValue = Math.max(0, Math.min(1, initialValue + delta));
                value = newValue;

                // layout thumb
                requestLayout();
            });
            getChildren().add(scrubber);

            // Animate scrollbar's width on mouse enter and exit
            setOnMouseEntered(event -> {
                widthTransition.setRate(1);
                widthTransition.play();
            });
            setOnMouseExited(event -> {
                widthTransition.setRate(-1);
                widthTransition.play();
            });
        }

        @Override
        protected void layoutChildren() {
            super.layoutChildren();

            double h = getHeight();
            double cH = getContentHeight();

            if (cH <= h) {
                // full size, if content does not excede viewport size
                scrubber.resize(getWidth(), h);
            } else {
                double sH = calculateScrubberHeight();

                // move thumb to position
                scrubber.setTranslateY(value * (h - sH));

                // Layout scrubber to fit the scrollbar's width
                scrubber.resize(getWidth(), sH);
            }
        }
    }
}