问题描述
我想为 JavaFX 实现显示高亮效果,该效果可以在 Windows 10 的各个部分看到,尤其是设置和计算器应用。
效果似乎由两部分组成,边框高光 (seen here) 和背景高光 (seen here,虽然由于压缩,但确实看起来更好 ).
我的第一直觉是看看这是否可以在某种像素着色器中完成,但在谷歌搜索之后似乎 JavaFX 确实为类似的东西提供了公共 API?
是否可以在不借助画布和手动绘制整个 UI 的情况下创建这种效果?
解决方法
首先我想说我不知道 Windows 是如何实现这种风格的。但我的一个想法是有多个层:
-
黑色背景。
-
具有从白色到透明的径向渐变的圆圈,随鼠标移动。
-
具有黑色背景和形状的区域,无论选项节点在哪里,都带有孔洞。
-
具有分层背景的选项节点。
-
当鼠标未悬停时:
- 没有插图的透明背景。
- 黑色背景,略有插图。
-
当鼠标悬停时:
- 低不透明度白色背景,没有插图。
- 黑色背景,略有插图。
- 以鼠标为中心的白色到透明径向渐变背景。
-
不幸的是,这意味着许多样式必须在代码中完成,尽管我更愿意将大部分样式放在 CSS 中。这是我快速模拟的概念验证。它的功能并不完整,但显示出您想要的外观是可能的。
OptionsPane.java
import javafx.beans.InvalidationListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Bounds;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.VPos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.scene.text.Font;
public class OptionsPane extends Region {
public static class Option {
private final String title;
private final String subtitle;
private final Node graphic;
public Option(String title,String subtitle,Node graphic) {
this.title = title;
this.subtitle = subtitle;
this.graphic = graphic;
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
public Node getGraphic() {
return graphic;
}
}
private final ObservableList<Option> options = FXCollections.observableArrayList();
private final TilePane topTiles = new TilePane();
private final Region midCover = new Region();
private final Circle underGlow = new Circle();
public OptionsPane() {
setBackground(new Background(new BackgroundFill(Color.BLACK,null,null)));
underGlow.setManaged(false);
underGlow.setRadius(75);
underGlow.visibleProperty().bind(hoverProperty());
underGlow.setFill(
new RadialGradient(
0,0.5,1.0,true,new Stop(0.0,Color.WHITE),new Stop(0.35,Color.TRANSPARENT)));
addEventFilter(
MouseEvent.MOUSE_MOVED,e -> {
underGlow.setCenterX(e.getX());
underGlow.setCenterY(e.getY());
});
midCover.setBackground(new Background(new BackgroundFill(Color.BLACK,null)));
topTiles.setMinSize(0,0);
topTiles.setVgap(20);
topTiles.setHgap(20);
topTiles.setPadding(new Insets(20));
topTiles.setPrefTileWidth(250);
topTiles.setPrefTileHeight(100);
topTiles.setPrefColumns(3);
options.addListener(
(InvalidationListener)
obs -> {
topTiles.getChildren().clear();
options.forEach(opt -> topTiles.getChildren().add(createOptionRegion(opt)));
});
getChildren().addAll(underGlow,midCover,topTiles);
}
public final ObservableList<Option> getOptions() {
return options;
}
@Override
protected void layoutChildren() {
double x = getInsets().getLeft();
double y = getInsets().getTop();
double w = getWidth() - getInsets().getRight() - x;
double h = getHeight() - getInsets().getBottom() - y;
layoutInArea(midCover,x,y,w,h,-1,HPos.CENTER,VPos.CENTER);
layoutInArea(topTiles,VPos.CENTER);
Shape coverShape = new Rectangle(x,h);
for (Node optionNode : topTiles.getChildren()) {
Bounds b = optionNode.getBoundsInParent();
Rectangle rect = new Rectangle(b.getMinX(),b.getMinY(),b.getWidth(),b.getHeight());
coverShape = Shape.subtract(coverShape,rect);
}
midCover.setShape(coverShape);
}
private Region createOptionRegion(Option option) {
Label titleLabel = new Label(option.getTitle());
titleLabel.setTextFill(Color.WHITE);
titleLabel.setFont(Font.font("System",13));
Label subtitleLabel = new Label(option.getSubtitle());
subtitleLabel.setTextFill(Color.DARKGRAY);
subtitleLabel.setFont(Font.font("System",10));
VBox textBox = new VBox(5,titleLabel,subtitleLabel);
HBox.setHgrow(textBox,Priority.ALWAYS);
HBox container = new HBox(10,textBox);
container.setPadding(new Insets(10));
if (option.getGraphic() != null) {
container.getChildren().add(0,option.getGraphic());
}
setNonHoverBackground(container);
container
.hoverProperty()
.addListener(
(obs,ov,nv) -> {
if (!nv) {
setNonHoverBackground(container);
}
});
container.setOnMouseMoved(e -> setHoverBackground(container,e.getX(),e.getY()));
return container;
}
private void setNonHoverBackground(Region region) {
BackgroundFill fill1 = new BackgroundFill(Color.TRANSPARENT,null);
BackgroundFill fill2 = new BackgroundFill(Color.BLACK,new Insets(2));
region.setBackground(new Background(fill1,fill2));
}
private void setHoverBackground(Region region,double x,double y) {
RadialGradient gradient =
new RadialGradient(
0,400,false,new Color(1,1,0.2)),Color.TRANSPARENT));
BackgroundFill fill1 = new BackgroundFill(new Color(1,0.3),new Insets(2));
BackgroundFill fill3 = new BackgroundFill(gradient,null);
region.setBackground(new Background(fill1,fill2,fill3));
}
}
Main.java
import java.util.ArrayList;
import java.util.List;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
OptionsPane pane = new OptionsPane();
List<OptionsPane.Option> options = new ArrayList<>();
for (int i = 0; i < 10; i++) {
Rectangle graphic = new Rectangle(20,20,Color.DARKGRAY);
options.add(
new OptionsPane.Option("Option Title #" + (i + 1),"Description #" + (i + 1),graphic));
}
pane.getOptions().addAll(options);
primaryStage.setScene(new Scene(pane));
primaryStage.show();
}
}
这就是它的样子:
这并不完全相同,但您可以自己进行实验并根据需要进行更改。