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

Implementing Fluent Design's reveal highlight effect in JavaFX

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

该效果似乎由两部分组成,一个是边框高亮(seen here) and a background highlight (seen here,尽管由于压缩而使人看起来更好)。

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

是否可以在不借助 canvas 并手动绘制整个 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,
            0.5, 0.5,
            1.0,
            true,
            null,
            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, 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, x, y, w, h, -1, HPos.CENTER, VPos.CENTER);

    Shape coverShape = new Rectangle(x, y, w, 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, null);
    BackgroundFill fill2 = new BackgroundFill(Color.BLACK, null, new Insets(2));
    region.setBackground(new Background(fill1, fill2));
  }

  private void setHoverBackground(Region region, double x, double y) {
    RadialGradient gradient =
        new RadialGradient(
            0, 0,
            x, y,
            400,
            false,
            null,
            new Stop(0.0, new Color(1, 1, 1, 0.2)),
            new Stop(0.35, Color.TRANSPARENT));

    BackgroundFill fill1 = new BackgroundFill(new Color(1, 1, 1, 0.3), null, null);
    BackgroundFill fill2 = new BackgroundFill(Color.BLACK, null, new Insets(2));
    BackgroundFill fill3 = new BackgroundFill(gradient, null, 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();
  }
}

这是它的样子:

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