在 JavaFX 中实现 Fluent Design 的显示高亮效果

问题描述

我想为 JavaFX 实现显示高亮效果,该效果可以在 Windows 10 的各个部分看到,尤其是设置和计算器应用。

效果似乎由两部分组成,边框高光 (seen here) 和背景高光 (seen here虽然由于压缩,但确实看起来更好 ).

我的第一直觉是看看这是否可以在某种像素着色器中完成,但在谷歌搜索之后似乎 JavaFX 确实为类似的东西提供了公共 API?

是否可以在不借助画布和手动绘制整个 UI 的情况下创建这种效果

解决方法

首先我想说我不知道​​ Windows 是如何实现这种风格的。但我的一个想法是有多个层:

  1. 黑色背景。

  2. 具有从白色到透明的径向渐变的圆圈,随鼠标移动。

  3. 具有黑色背景和形状的区域,无论选项节点在哪里,都带有孔洞。

  4. 具有分层背景的选项节点。

    • 当鼠标悬停时:

      1. 没有插图的透明背景。
      2. 黑色背景,略有插图。
    • 当鼠标悬停时:

      1. 低不透明度白色背景,没有插图。
      2. 黑色背景,略有插图。
      3. 以鼠标为中心的白色到透明径向渐变背景。

不幸的是,这意味着许多样式必须在代码中完成,尽管我更愿意将大部分样式放在 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();
  }
}

这就是它的样子:

Demo GIF of code.

这并不完全相同,但您可以自己进行实验并根据需要进行更改。