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