创建 SVGContainer 控件 (#5464)

This commit is contained in:
Glavo
2026-02-11 21:21:03 +08:00
committed by GitHub
parent ca071fd924
commit 90e6782193
4 changed files with 203 additions and 60 deletions

View File

@@ -18,8 +18,6 @@
package org.jackhuang.hmcl.ui;
import javafx.beans.value.ObservableValue;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.paint.Paint;
import javafx.scene.shape.SVGPath;
@@ -30,6 +28,7 @@ import javafx.scene.shape.SVGPath;
/// with a style of outlined, a weight of 400, a grade of 0, and an optical size of 24 px.
/// The view boxes of all icons are normalized to `0 0 24 24`.
public enum SVG {
NONE(""), // Empty Icon
ADD("M11 13H5V11H11V5H13V11H19V13H13V19H11V13Z"),
ADD_CIRCLE("M11 17H13V13H17V11H13V7H11V11H7V13H11V17ZM12 22Q9.925 22 8.1 21.2125T4.925 19.075Q3.575 17.725 2.7875 15.9T2 12Q2 9.925 2.7875 8.1T4.925 4.925Q6.275 3.575 8.1 2.7875T12 2Q14.075 2 15.9 2.7875T19.075 4.925Q20.425 6.275 21.2125 8.1T22 12Q22 14.075 21.2125 15.9T19.075 19.075Q17.725 20.425 15.9 21.2125T12 22ZM12 20Q15.35 20 17.675 17.675T20 12Q20 8.65 17.675 6.325T12 4Q8.65 4 6.325 6.325T4 12Q4 15.35 6.325 17.675T12 20ZM12 12Z"),
ALPHA_CIRCLE("M11,7H13A2,2 0 0,1 15,9V17H13V13H11V17H9V9A2,2 0 0,1 11,7M11,9V11H13V9H11M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2Z"), // Not Material
@@ -130,6 +129,12 @@ public enum SVG {
public static final double DEFAULT_SIZE = 24;
static void setSize(SVGPath path, double size) {
double scale = size / DEFAULT_SIZE;
path.setScaleX(scale);
path.setScaleY(scale);
}
private final String rawPath;
private String path;
@@ -144,35 +149,20 @@ public enum SVG {
return path;
}
public SVGPath createSVGPath() {
var p = new SVGPath();
p.setContent(getPath());
p.getStyleClass().add("svg");
return p;
public SVGPath createIcon() {
var path = new SVGPath();
path.getStyleClass().add("svg");
path.setContent(getPath());
return path;
}
private static Node createIcon(SVGPath path, double size) {
if (size == DEFAULT_SIZE)
return path;
else {
double scale = size / DEFAULT_SIZE;
path.setScaleX(scale);
path.setScaleY(scale);
return new Group(path);
}
public SVGContainer createIcon(double size) {
return new SVGContainer(this, size);
}
public Node createIcon() {
return createIcon(DEFAULT_SIZE);
}
public Node createIcon(double size) {
return createIcon(createSVGPath(), size);
}
public Node createIcon(ObservableValue<? extends Paint> color) {
SVGPath p = createSVGPath();
p.fillProperty().bind(color);
return createIcon(p, DEFAULT_SIZE);
public SVGPath createIcon(ObservableValue<? extends Paint> color) {
SVGPath path = createIcon();
path.fillProperty().bind(color);
return path;
}
}

View File

@@ -0,0 +1,175 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Parent;
import javafx.scene.shape.SVGPath;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.Motion;
/// A lightweight wrapper for displaying [SVG] icons.
///
/// @author Glavo
public final class SVGContainer extends Parent {
private static final String DEFAULT_STYLE_CLASS = "svg-container";
private final SVGPath path = new SVGPath();
private SVG icon = SVG.NONE;
private double iconSize = SVG.DEFAULT_SIZE;
private SVGPath tempPath;
private Timeline timeline;
{
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
this.path.getStyleClass().add("svg");
}
/// Creates an SVGContainer with the default icon and the default icon size.
public SVGContainer() {
this(SVG.NONE, SVG.DEFAULT_SIZE);
}
/// Creates an SVGContainer showing the given icon using the default icon size.
///
/// @param icon the [SVG] icon to display
public SVGContainer(SVG icon) {
this(icon, SVG.DEFAULT_SIZE);
}
/// Creates an SVGContainer with a custom icon size. The initial icon is
/// [SVG#NONE].
///
/// @param iconSize the icon size
public SVGContainer(double iconSize) {
this(SVG.NONE, iconSize);
}
/// Creates an SVGContainer with the specified icon and size.
///
/// @param icon the [SVG] icon to display
/// @param iconSize the icon size
public SVGContainer(SVG icon, double iconSize) {
setIconSizeImpl(iconSize);
setIcon(icon);
}
public double getIconSize() {
return iconSize;
}
private void setIconSizeImpl(double newSize) {
this.iconSize = newSize;
SVG.setSize(path, newSize);
if (tempPath != null)
SVG.setSize(tempPath, newSize);
}
public void setIconSize(double newSize) {
setIconSizeImpl(newSize);
requestLayout();
}
/// Gets the currently displayed icon.
public SVG getIcon() {
return icon;
}
/// Sets the icon to display without animation.
public void setIcon(SVG newIcon) {
setIcon(newIcon, Duration.ZERO);
}
/// Sets the icon to display with a cross-fade animation.
public void setIcon(SVG newIcon, Duration animationDuration) {
if (timeline != null) {
timeline.stop();
timeline = null;
}
SVG oldIcon = this.icon;
this.icon = newIcon;
if (animationDuration.equals(Duration.ZERO)) {
path.setContent(newIcon.getPath());
path.setOpacity(1);
if (getChildren().size() != 1)
getChildren().setAll(path);
} else {
if (tempPath == null) {
tempPath = new SVGPath();
tempPath.getStyleClass().add("svg");
SVG.setSize(tempPath, iconSize);
} else
tempPath.setOpacity(1);
tempPath.setContent(oldIcon.getPath());
getChildren().setAll(path, tempPath);
path.setOpacity(0);
path.setContent(newIcon.getPath());
timeline = new Timeline(
new KeyFrame(Duration.ZERO,
new KeyValue(path.opacityProperty(), 0, Motion.LINEAR),
new KeyValue(tempPath.opacityProperty(), 1, Motion.LINEAR)
),
new KeyFrame(animationDuration,
new KeyValue(path.opacityProperty(), 1, Motion.LINEAR),
new KeyValue(tempPath.opacityProperty(), 0, Motion.LINEAR)
)
);
timeline.setOnFinished(e -> {
getChildren().setAll(path);
timeline = null;
});
timeline.play();
}
}
// Parent
@Override
public double prefWidth(double height) {
return iconSize;
}
@Override
public double prefHeight(double width) {
return iconSize;
}
@Override
public double minHeight(double width) {
return iconSize;
}
@Override
public double minWidth(double height) {
return iconSize;
}
@Override
protected void layoutChildren() {
// Do nothing
}
}

View File

@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.construct;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.ScrollPane;
@@ -28,8 +27,8 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.SVGContainer;
import org.jackhuang.hmcl.ui.animation.Motion;
import java.util.function.Consumer;
@@ -100,27 +99,17 @@ public class AdvancedListBox extends ScrollPane {
return add(item);
}
@SuppressWarnings("SuspiciousNameCombination")
public AdvancedListBox addNavigationDrawerTab(TabHeader tabHeader, TabControl.Tab<?> tab, String title,
SVG unselectedGraphic, SVG selectedGraphic) {
AdvancedListItem item = createNavigationDrawerItem(title, null);
item.activeProperty().bind(tabHeader.getSelectionModel().selectedItemProperty().isEqualTo(tab));
item.setOnAction(e -> tabHeader.select(tab));
Node unselectedIcon = unselectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
Node selectedIcon = selectedGraphic.createIcon(AdvancedListItem.LEFT_ICON_SIZE);
TransitionPane leftGraphic = new TransitionPane();
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
var leftGraphic = new SVGContainer(item.isActive() ? selectedGraphic : unselectedGraphic, AdvancedListItem.LEFT_ICON_SIZE);
leftGraphic.setMouseTransparent(true);
leftGraphic.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(leftGraphic, AdvancedListItem.LEFT_GRAPHIC_SIZE);
FXUtils.setLimitHeight(leftGraphic, AdvancedListItem.LEFT_ICON_SIZE);
leftGraphic.setPadding(Insets.EMPTY);
leftGraphic.setContent(item.isActive() ? selectedIcon : unselectedIcon, ContainerAnimations.NONE);
FXUtils.onChange(item.activeProperty(), active ->
leftGraphic.setContent(active ? selectedIcon : unselectedIcon, ContainerAnimations.FADE));
AdvancedListItem.setAlignment(leftGraphic, Pos.CENTER);
AdvancedListItem.setMargin(leftGraphic, AdvancedListItem.LEFT_ICON_MARGIN);
FXUtils.onChange(item.activeProperty(), active -> leftGraphic.setIcon(active ? selectedGraphic : unselectedGraphic, Motion.SHORT4));
item.setLeftGraphic(leftGraphic);
return add(item);
}

View File

@@ -22,7 +22,6 @@ import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXListView;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
@@ -32,7 +31,6 @@ import javafx.scene.image.WritableImage;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.SVGPath;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.schematic.LitematicFile;
import org.jackhuang.hmcl.setting.Profile;
@@ -551,8 +549,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
private final HBox right;
private final ImageView iconImageView;
private final SVGPath iconSVG;
private final StackPane iconSVGWrapper;
private final SVGContainer iconSVGView;
private final Tooltip tooltip = new Tooltip();
@@ -568,15 +565,7 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
this.iconImageView = new ImageView();
FXUtils.limitSize(iconImageView, 32, 32);
this.iconSVG = new SVGPath();
iconSVG.getStyleClass().add("svg");
iconSVG.setScaleX(32.0 / SVG.DEFAULT_SIZE);
iconSVG.setScaleY(32.0 / SVG.DEFAULT_SIZE);
this.iconSVGWrapper = new StackPane(new Group(iconSVG));
iconSVGWrapper.setAlignment(Pos.CENTER);
FXUtils.setLimitWidth(iconSVGWrapper, 32);
FXUtils.setLimitHeight(iconSVGWrapper, 32);
this.iconSVGView = new SVGContainer(32);
BorderPane.setAlignment(left, Pos.CENTER);
root.setLeft(left);
@@ -638,8 +627,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
iconImageView.setImage(fileItem.getImage());
left.getChildren().setAll(iconImageView);
} else {
iconSVG.setContent(item.getIcon().getPath());
left.getChildren().setAll(iconSVGWrapper);
iconSVGView.setIcon(item.getIcon());
left.getChildren().setAll(iconSVGView);
}
center.setTitle(item.getName());