考虑到所有不同的条件,我能想到的解决此问题的一种方法是将节点包含到 TableView 的内部子级中。这样我就能更好的控制效果。
如果您可以使用查找方法并调整 TableView 的子项,那么您可以考虑以下方法:)。
总体思路是:
- 我们将创建一个所需高度的窗格来显示效果。我使用窗格的背景颜色来更好地控制效果颜色、大小等。
- 然后我们将该节点包含到包含 TableRows 的 Group 中。
- 根据 TableView 视口的尺寸,我们定位该窗格。
- 为了处理效果的可见性,我们访问虚拟滚动条并监听其可见性和滚动值。
总结以上所有内容,下面是一个展示阴影效果的快速演示。请注意,阴影不会与滚动条重叠,因为我们将窗格包含在保存 TableRows 的 Group 节点中。
UPDATE:我更新了下面的演示以包括FadeTransition
以获得更平滑的阴影可见度。
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.beans.InvalidationListener;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class TableViewShadowForScrollDemo extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
ObservableList<Person> persons = FXCollections.observableArrayList();
for (int i = 0; i < 15; i++) {
persons.add(new Person("Name " + i, "Last" + i, "City " + i));
}
TableColumn<Person, String> fnCol = new TableColumn<>("First Name");
fnCol.setCellValueFactory(param -> param.getValue().firstNameProperty());
TableColumn<Person, String> lnCol = new TableColumn<>("Last Name");
lnCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
TableColumn<Person, String> cityCol = new TableColumn<>("City");
cityCol.setCellValueFactory(param -> param.getValue().lastNameProperty());
fnCol.setPrefWidth(150);
lnCol.setPrefWidth(100);
cityCol.setPrefWidth(200);
MyTableView<Person> tableView = new MyTableView<>();
tableView.getColumns().addAll(fnCol, lnCol, cityCol);
tableView.setItems(persons);
StackPane root = new StackPane(tableView);
root.setPadding(new Insets(15));
Scene scene = new Scene(root, 500, 300);
primaryStage.setScene(scene);
primaryStage.setTitle("TableView Scroll Shadow Demo");
primaryStage.show();
}
class MyTableView<S> extends TableView<S> {
private final double SHADOW_HEIGHT = 40;
private Group sheet;
private StackPane shade;
private Rectangle clip;
private BooleanProperty show;
@Override
protected void layoutChildren() {
super.layoutChildren();
// Ensuring that we set the configurations only once.
if (sheet == null) {
show = new SimpleBooleanProperty();
// Create a pane for creating the shadow. I choose to use background-color instead of effect for better control of the effect.
shade = new StackPane();
shade.setMouseTransparent(true);
shade.setStyle("-fx-background-color:linear-gradient(to top, #999999, transparent 70%, transparent);");
// Access the Group node to which the shade has to be included.
sheet = (Group) lookup(".sheet");
final ObservableList<Node> sheetChildren = sheet.getChildren();
sheetChildren.add(shade);
// Ensure to always keep the shade on top of all table row nodes.
sheet.needsLayoutProperty().addListener((obs, old, needsLayout) -> {
if (!needsLayout && sheetChildren.indexOf(shade) != sheetChildren.size() - 1) {
sheetChildren.remove(shade);
sheetChildren.add(shade);
}
});
// Get the clipped container for position calculations of the shade.
Region clipCont = (Region) lookup(".clipped-container");
clip = (Rectangle) clipCont.getClip();
// Add listener to resize/reposition the shade, when ever the table dimensions are changed
InvalidationListener listener = e -> {
shade.resize(sheet.getLayoutBounds().getWidth(), SHADOW_HEIGHT);
shade.setLayoutY(clip.getLayoutY() + clipCont.getHeight() - SHADOW_HEIGHT);
};
clipCont.widthProperty().addListener(listener);
clipCont.heightProperty().addListener(listener);
// Access the vertical scroll bar for deciding the visibility of the shade.
ScrollBar vBar = (ScrollBar) lookup(".scroll-bar:vertical");
FadeTransition fadeTransition = new FadeTransition(Duration.millis(500), shade);
fadeTransition.setOnFinished(e -> shade.setVisible(show.get()));
show.addListener((obs, old, val) -> {
if (val) {
shade.setVisible(true);
}
fadeTransition.setFromValue(val ? 0.0 : 1.0);
fadeTransition.setToValue(val ? 1.0 : 0.0);
fadeTransition.play();
});
show.bind(Bindings.createBooleanBinding(() -> vBar.isVisible() && vBar.getValue() < 1.0, vBar.visibleProperty(), vBar.valueProperty()));
}
}
}
class Person {
private StringProperty firstName = new SimpleStringProperty();
private StringProperty lastName = new SimpleStringProperty();
private StringProperty city = new SimpleStringProperty();
public Person(String fn, String ln, String cty) {
setFirstName(fn);
setLastName(ln);
setCity(cty);
}
public String getFirstName() {
return firstName.get();
}
public StringProperty firstNameProperty() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName.set(firstName);
}
public String getLastName() {
return lastName.get();
}
public StringProperty lastNameProperty() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName.set(lastName);
}
public String getCity() {
return city.get();
}
public StringProperty cityProperty() {
return city;
}
public void setCity(String city) {
this.city.set(city);
}
}
}