简化 RipplerContainer (#5363)

This commit is contained in:
Glavo
2026-02-01 18:40:52 +08:00
committed by GitHub
parent 05251cb4e3
commit c45cd74b67

View File

@@ -19,17 +19,12 @@ package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXRippler; import com.jfoenix.controls.JFXRippler;
import javafx.animation.Transition; import javafx.animation.Transition;
import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener;
import javafx.beans.NamedArg;
import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*; import javafx.css.*;
import javafx.css.converter.PaintConverter;
import javafx.event.EventHandler;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background; import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill; import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii; import javafx.scene.layout.CornerRadii;
@@ -37,39 +32,41 @@ import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.scene.paint.Paint; import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
import org.jackhuang.hmcl.theme.Themes; import org.jackhuang.hmcl.theme.Themes;
import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.util.Lang;
import java.util.ArrayList;
import java.util.List; import java.util.List;
@DefaultProperty("container")
public class RipplerContainer extends StackPane { public class RipplerContainer extends StackPane {
private static final String DEFAULT_STYLE_CLASS = "rippler-container"; private static final String DEFAULT_STYLE_CLASS = "rippler-container";
private static final Duration DURATION = Duration.millis(200); private static final CornerRadii DEFAULT_RADII = new CornerRadii(3);
private static final Color DEFAULT_RIPPLER_FILL = Color.rgb(0, 200, 255);
private final ObjectProperty<Node> container = new SimpleObjectProperty<>(this, "container", null); private final Node container;
private final StyleableObjectProperty<Paint> ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL, this, "ripplerFill", null);
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected", false);
private final StackPane buttonContainer = new StackPane(); private final StackPane buttonContainer = new StackPane();
private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) { private final JFXRippler buttonRippler = new JFXRippler(new StackPane()) {
private static final Background DEFAULT_MASK_BACKGROUND = new Background(new BackgroundFill(Color.WHITE, DEFAULT_RADII, Insets.EMPTY));
@Override @Override
protected Node getMask() { protected Node getMask() {
StackPane mask = new StackPane(); StackPane mask = new StackPane();
mask.shapeProperty().bind(buttonContainer.shapeProperty()); mask.shapeProperty().bind(buttonContainer.shapeProperty());
mask.backgroundProperty().bind(Bindings.createObjectBinding(() -> new Background(new BackgroundFill(Color.WHITE, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getRadii() : defaultRadii, buttonContainer.getBackground() != null && buttonContainer.getBackground().getFills().size() > 0 ? buttonContainer.getBackground().getFills().get(0).getInsets() : Insets.EMPTY)), buttonContainer.backgroundProperty())); mask.setBackground(DEFAULT_MASK_BACKGROUND);
mask.resize(buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(), buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset()); mask.resize(
buttonContainer.getWidth() - buttonContainer.snappedRightInset() - buttonContainer.snappedLeftInset(),
buttonContainer.getHeight() - buttonContainer.snappedBottomInset() - buttonContainer.snappedTopInset()
);
return mask; return mask;
} }
}; };
private final CornerRadii defaultRadii = new CornerRadii(3); private Transition coverAnimation;
public RipplerContainer(@NamedArg("container") Node container) { public RipplerContainer(Node container) {
setContainer(container); this.container = container;
getStyleClass().add(DEFAULT_STYLE_CLASS); getStyleClass().add(DEFAULT_STYLE_CLASS);
buttonRippler.setPosition(JFXRippler.RipplerPos.BACK); buttonRippler.setPosition(JFXRippler.RipplerPos.BACK);
@@ -86,71 +83,79 @@ public class RipplerContainer extends StackPane {
setPickOnBounds(false); setPickOnBounds(false);
buttonContainer.setPickOnBounds(false); buttonContainer.setPickOnBounds(false);
buttonRippler.ripplerFillProperty().bind(ripplerFillProperty());
containerProperty().addListener(o -> updateChildren());
updateChildren(); updateChildren();
InvalidationListener listener = o -> { var shape = new Rectangle();
if (isSelected()) setBackground(new Background(new BackgroundFill(getRipplerFill(), defaultRadii, null))); shape.widthProperty().bind(widthProperty());
else setBackground(new Background(new BackgroundFill(Color.TRANSPARENT, defaultRadii, null))); shape.heightProperty().bind(heightProperty());
}; setShape(shape);
selectedProperty().addListener(listener);
selectedProperty().addListener((a, b, newValue) ->
pseudoClassStateChanged(PseudoClass.getPseudoClass("selected"), newValue));
ripplerFillProperty().addListener(listener);
setShape(Lang.apply(new Rectangle(), rectangle -> {
rectangle.widthProperty().bind(widthProperty());
rectangle.heightProperty().bind(heightProperty());
}));
EventHandler<MouseEvent> mouseEventHandler;
if (AnimationUtils.isAnimationEnabled()) { if (AnimationUtils.isAnimationEnabled()) {
setOnMouseEntered(e -> new Transition() { mouseEventHandler = event -> {
{ if (coverAnimation != null) {
setCycleDuration(DURATION); coverAnimation.stop();
setInterpolator(Motion.EASE_IN); coverAnimation = null;
} }
@Override if (event.getEventType() == MouseEvent.MOUSE_ENTERED) {
protected void interpolate(double frac) { coverAnimation = new Transition() {
interpolateBackground(frac); {
} setCycleDuration(Motion.SHORT4);
}.play()); setInterpolator(Motion.EASE_IN);
}
setOnMouseExited(e -> new Transition() { @Override
{ protected void interpolate(double frac) {
setCycleDuration(DURATION); interpolateBackground(frac);
setInterpolator(Motion.EASE_OUT); }
};
} else {
coverAnimation = new Transition() {
{
setCycleDuration(Motion.SHORT4);
setInterpolator(Motion.EASE_OUT);
}
@Override
protected void interpolate(double frac) {
interpolateBackground(1 - frac);
}
};
} }
@Override coverAnimation.play();
protected void interpolate(double frac) { };
interpolateBackground(1 - frac);
}
}.play());
} else { } else {
setOnMouseEntered(e -> interpolateBackground(1)); mouseEventHandler = event ->
setOnMouseExited(e -> interpolateBackground(0)); interpolateBackground(event.getEventType() == MouseEvent.MOUSE_ENTERED ? 1 : 0);
} }
addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEventHandler);
addEventHandler(MouseEvent.MOUSE_EXITED, mouseEventHandler);
} }
private void interpolateBackground(double frac) { private void interpolateBackground(double frac) {
Color onSurface = Themes.getColorScheme().getOnSurface(); if (frac < 0.01) {
setBackground(new Background(new BackgroundFill( setBackground(null);
Color.color(onSurface.getRed(), onSurface.getGreen(), onSurface.getBlue(), frac * 0.04), } else {
CornerRadii.EMPTY, Insets.EMPTY))); Color onSurface = Themes.getColorScheme().getOnSurface();
setBackground(new Background(new BackgroundFill(
Color.color(onSurface.getRed(), onSurface.getGreen(), onSurface.getBlue(), frac * 0.04),
CornerRadii.EMPTY, Insets.EMPTY)));
}
} }
protected void updateChildren() { protected void updateChildren() {
if (buttonRippler.getPosition() == JFXRippler.RipplerPos.BACK) Node container = getContainer();
getChildren().setAll(buttonContainer, getContainer()); if (buttonRippler.getPosition() == JFXRippler.RipplerPos.BACK) {
else getChildren().setAll(buttonContainer, container);
getChildren().setAll(getContainer(), buttonContainer); container.setPickOnBounds(false);
} else {
for (int i = 1; i < getChildren().size(); ++i) getChildren().setAll(container, buttonContainer);
getChildren().get(i).setPickOnBounds(false); buttonContainer.setPickOnBounds(false);
}
} }
public void setPosition(JFXRippler.RipplerPos pos) { public void setPosition(JFXRippler.RipplerPos pos) {
@@ -163,39 +168,41 @@ public class RipplerContainer extends StackPane {
} }
public Node getContainer() { public Node getContainer() {
return container.get();
}
public ObjectProperty<Node> containerProperty() {
return container; return container;
} }
public void setContainer(Node container) { private final StyleableObjectProperty<Paint> ripplerFill = new StyleableObjectProperty<>(DEFAULT_RIPPLER_FILL) {
this.container.set(container); @Override
} public Object getBean() {
return RipplerContainer.this;
}
public Paint getRipplerFill() { @Override
return ripplerFill.get(); public String getName() {
} return "ripplerFill";
}
@Override
public CssMetaData<? extends Styleable, Paint> getCssMetaData() {
return StyleableProperties.RIPPLER_FILL;
}
@Override
protected void invalidated() {
buttonRippler.setRipplerFill(get());
}
};
public StyleableObjectProperty<Paint> ripplerFillProperty() { public StyleableObjectProperty<Paint> ripplerFillProperty() {
return ripplerFill; return ripplerFill;
} }
public Paint getRipplerFill() {
return ripplerFillProperty().get();
}
public void setRipplerFill(Paint ripplerFill) { public void setRipplerFill(Paint ripplerFill) {
this.ripplerFill.set(ripplerFill); ripplerFillProperty().set(ripplerFill);
}
public boolean isSelected() {
return selected.get();
}
public BooleanProperty selectedProperty() {
return selected;
}
public void setSelected(boolean selected) {
this.selected.set(selected);
} }
@Override @Override
@@ -204,12 +211,28 @@ public class RipplerContainer extends StackPane {
} }
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() { public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.FACTORY.getCssMetaData(); return StyleableProperties.STYLEABLES;
} }
private final static class StyleableProperties { private final static class StyleableProperties {
private static final StyleablePropertyFactory<RipplerContainer> FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData()); private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = new CssMetaData<>("-jfx-rippler-fill", PaintConverter.getInstance(), DEFAULT_RIPPLER_FILL) {
@Override
public boolean isSettable(RipplerContainer styleable) {
return styleable.ripplerFill == null || !styleable.ripplerFill.isBound();
}
private static final CssMetaData<RipplerContainer, Paint> RIPPLER_FILL = FACTORY.createPaintCssMetaData("-jfx-rippler-fill", s -> s.ripplerFill, Color.rgb(0, 200, 255)); @Override
public StyleableProperty<Paint> getStyleableProperty(RipplerContainer styleable) {
return styleable.ripplerFillProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> STYLEABLES;
static {
var styleables = new ArrayList<>(StackPane.getClassCssMetaData());
styleables.add(RIPPLER_FILL);
STYLEABLES = List.copyOf(styleables);
}
} }
} }