这是关于滚动视图的一个一般性问题,我想学习滚动视图的基础知识以及如何独自实现滚动视图,因为它是大多数动态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);
}
}
}
}
答案 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);
}
}
}
}