Skip to content
134 changes: 110 additions & 24 deletions HMCL/src/main/java/com/jfoenix/controls/JFXRippler.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.css.converter.SizeConverter;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.scene.CacheHint;
import javafx.scene.Group;
Expand All @@ -45,6 +46,8 @@
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
Expand Down Expand Up @@ -76,6 +79,8 @@ public enum RipplerMask {
protected RippleGenerator rippler;
protected Pane ripplerPane;
protected Node control;
private Animation coverAnimation;
private Rectangle hoverOverlay;

protected static final double RIPPLE_MAX_RADIUS = 300;
private static final Interpolator RIPPLE_INTERPOLATOR = Interpolator.SPLINE(0.0825,
Expand Down Expand Up @@ -128,14 +133,53 @@ public JFXRippler(Node control, RipplerMask mask, RipplerPos pos) {
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);

EventHandler<MouseEvent> mouseEventHandler = event -> {
if (coverAnimation != null) {
coverAnimation.stop();
}
boolean isEntered = event.getEventType() == MouseEvent.MOUSE_ENTERED;

if (AnimationUtils.isAnimationEnabled()) {
coverAnimation = new Timeline(new KeyFrame(Motion.SHORT4,
new KeyValue(hoverOverlay.opacityProperty(), isEntered ? 1 : 0, isEntered ? Motion.EASE_IN : Motion.EASE_OUT)));
Comment thread
CiiLu marked this conversation as resolved.
coverAnimation.play();
} else {
interpolateBackground(isEntered ? 1 : 0);
}
};

addEventHandler(MouseEvent.MOUSE_ENTERED, mouseEventHandler);
addEventHandler(MouseEvent.MOUSE_EXITED, mouseEventHandler);
}

private void interpolateBackground(double frac) {
if (hoverOverlay == null) return;
hoverOverlay.setOpacity(frac);
}
Comment thread
CiiLu marked this conversation as resolved.

protected final void createRippleUI() {
// create rippler panels
rippler = new RippleGenerator();
ripplerPane = new StackPane();
ripplerPane.setMouseTransparent(true);
ripplerPane.getChildren().add(rippler);

hoverOverlay = new Rectangle();
hoverOverlay.setManaged(false);
hoverOverlay.setCache(true);
hoverOverlay.setCacheHint(CacheHint.SPEED);
hoverOverlay.setOpacity(0);

hoverOverlay.fillProperty().bind(Bindings.createObjectBinding(() -> {
Paint currentFill = getRipplerFill();
if (currentFill instanceof Color fill) {
return Color.color(fill.getRed(), fill.getGreen(), fill.getBlue(), 0.15);
} else {
return currentFill;
}
}, ripplerFillProperty()));

ripplerPane.getChildren().addAll(hoverOverlay, rippler);
getChildren().add(ripplerPane);
}

Expand Down Expand Up @@ -325,16 +369,25 @@ public void showOverlay() {
rippler.overlayRect.outAnimation.stop();
}
rippler.createOverlay();
rippler.overlayRect.inAnimation.play();

if (AnimationUtils.isAnimationEnabled()) {
rippler.overlayRect.inAnimation.play();
} else {
rippler.overlayRect.inAnimation.stop();
rippler.overlayRect.setOpacity(1);
}
}

public void hideOverlay() {
if (!forceOverlay) {
if (rippler.overlayRect != null) {
rippler.overlayRect.inAnimation.stop();
}
if (rippler.overlayRect != null) {
rippler.overlayRect.outAnimation.play();
if (AnimationUtils.isAnimationEnabled()) {
rippler.overlayRect.outAnimation.play();
} else {
rippler.overlayRect.outAnimation.stop();
rippler.overlayRect.setOpacity(0);
}
}
} else {
System.err.println("Ripple Overlay is forced!");
Expand Down Expand Up @@ -373,32 +426,48 @@ void createRipple() {
}
this.resetClip = false;

// create the ripple effect
final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
getChildren().add(ripple);
ripplesQueue.add(ripple);
if (AnimationUtils.isAnimationEnabled()) {
// create the ripple effect
final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
getChildren().add(ripple);
ripplesQueue.add(ripple);

// animate the ripple
overlayRect.outAnimation.stop();
overlayRect.inAnimation.play();
ripple.inAnimation.play();
// animate the ripple
overlayRect.outAnimation.stop();
overlayRect.inAnimation.play();
ripple.inAnimation.play();
} else {
// apply simple press effect when animation is disabled
overlayRect.outAnimation.stop();
overlayRect.inAnimation.stop();
overlayRect.setOpacity(1);
}
}
}

private void releaseRipple() {
Ripple ripple = ripplesQueue.poll();
if (ripple != null) {
ripple.inAnimation.stop();
ripple.outAnimation = new Timeline(
new KeyFrame(Duration.millis(Math.min(800, (0.9 * 500) / ripple.getScaleX()))
, ripple.outKeyValues));
ripple.outAnimation.setOnFinished((event) -> getChildren().remove(ripple));
ripple.outAnimation.play();
if (generating.getAndSet(false)) {
if (overlayRect != null) {
overlayRect.inAnimation.stop();
if (!forceOverlay) {
if (AnimationUtils.isAnimationEnabled()) {
ripple.outAnimation = new Timeline(
new KeyFrame(Duration.millis(Math.min(800, (0.9 * 500) / ripple.getScaleX()))
, ripple.outKeyValues));
ripple.outAnimation.setOnFinished((event) -> getChildren().remove(ripple));
ripple.outAnimation.play();
} else {
getChildren().remove(ripple);
}
}
if (generating.getAndSet(false)) {
if (overlayRect != null) {
overlayRect.inAnimation.stop();
if (!forceOverlay) {
if (AnimationUtils.isAnimationEnabled()) {
overlayRect.outAnimation.play();
} else {
overlayRect.outAnimation.stop();
overlayRect.setOpacity(0);
}
}
}
Expand Down Expand Up @@ -548,8 +617,14 @@ private void resetOverLay() {
if (rippler.overlayRect != null) {
rippler.overlayRect.inAnimation.stop();
final RippleGenerator.OverLayRipple oldOverlay = rippler.overlayRect;
rippler.overlayRect.outAnimation.setOnFinished((finish) -> rippler.getChildren().remove(oldOverlay));
rippler.overlayRect.outAnimation.play();

if (AnimationUtils.isAnimationEnabled()) {
rippler.overlayRect.outAnimation.setOnFinished((finish) -> rippler.getChildren().remove(oldOverlay));
rippler.overlayRect.outAnimation.play();
} else {
rippler.overlayRect.outAnimation.stop();
rippler.getChildren().remove(oldOverlay);
}
rippler.overlayRect = null;
}
}
Expand All @@ -561,6 +636,17 @@ private void resetClip() {
protected void resetRippler() {
resetOverLay();
resetClip();

if (hoverOverlay != null && control != null) {
Bounds bounds = control.getBoundsInParent();
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
hoverOverlay.setX(bounds.getMinX() + diffMinX - snappedLeftInset());
hoverOverlay.setY(bounds.getMinY() + diffMinY - snappedTopInset());
hoverOverlay.setWidth(control.getLayoutBounds().getWidth());
hoverOverlay.setHeight(control.getLayoutBounds().getHeight());
hoverOverlay.setClip(getMask());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

resetRippler is called frequently during layout changes. Calling getMask() here on every pulse is inefficient because it creates a new Node object each time. This can lead to excessive object allocation and GC pressure during UI transitions (e.g., when the control is resizing). Consider caching the mask node or only updating it when the mask type or significant bounds change.

}
Comment on lines +640 to +649

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

getMaskType() == RipplerMask.RECT 时,hoverOverlay 本身已经是一个大小完全相同的矩形,因此不需要再设置裁剪遮罩(Clip)。在频繁触发的 resetRippler() 中,每次都创建并设置新的 Rectangle 遮罩会带来不必要的性能开销和 GC 压力。建议在遮罩类型为 RECT 时将 Clip 设为 null 以进行优化。

        if (hoverOverlay != null && control != null) {
            Bounds bounds = control.getBoundsInParent();
            double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
            double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
            hoverOverlay.setX(bounds.getMinX() + diffMinX - snappedLeftInset());
            hoverOverlay.setY(bounds.getMinY() + diffMinY - snappedTopInset());
            hoverOverlay.setWidth(control.getLayoutBounds().getWidth());
            hoverOverlay.setHeight(control.getLayoutBounds().getHeight());
            if (getMaskType() == RipplerMask.RECT) {
                hoverOverlay.setClip(null);
            } else {
                hoverOverlay.setClip(getMask());
            }
        }

}

/***************************************************************************
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/java/com/jfoenix/skins/JFXTabPaneSkin.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
import javafx.geometry.Insets;
import javafx.geometry.Side;
import javafx.geometry.VPos;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.control.skin.TabPaneSkin;
Expand Down Expand Up @@ -1126,7 +1125,6 @@ public HeaderControl(ArrowPosition pos) {
StackPane container = new StackPane(arrowButton);
container.getStyleClass().add("container");
container.setPadding(new Insets(7));
container.setCursor(Cursor.HAND);

container.setOnMousePressed(press -> {
offsetProperty.set(header.scrollOffset);
Expand Down
2 changes: 0 additions & 2 deletions HMCL/src/main/java/com/jfoenix/skins/JFXToggleButtonSkin.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
import com.jfoenix.transitions.JFXKeyValue;
import javafx.animation.Interpolator;
import javafx.geometry.Insets;
import javafx.scene.Cursor;
import javafx.scene.control.skin.ToggleButtonSkin;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
Expand Down Expand Up @@ -86,7 +85,6 @@ public JFXToggleButtonSkin(JFXToggleButton toggleButton) {

final StackPane main = new StackPane();
main.getChildren().setAll(line, rippler);
main.setCursor(Cursor.HAND);

// show focus traversal effect
getSkinnable().armedProperty().addListener((o, oldVal, newVal) -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,6 @@ private static final class InstallerItemSkin extends SkinBase<InstallerItem> {
event.consume();
}
});
pane.setCursor(Cursor.HAND);
} else {
container.setOnMouseClicked(null);
pane.setCursor(Cursor.DEFAULT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Cursor;
import javafx.scene.canvas.Canvas;
import javafx.scene.control.Label;
import javafx.scene.control.SkinBase;
Expand Down Expand Up @@ -53,7 +52,6 @@ public AccountListItemSkin(AccountListItem skinnable) {
super(skinnable);

BorderPane root = new BorderPane();
root.setCursor(Cursor.HAND);
FXUtils.onClicked(root, skinnable::fire);

JFXRadioButton chkSelected = new JFXRadioButton();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,7 @@
*/
package org.jackhuang.hmcl.ui.construct;

import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.Parent;
import org.jackhuang.hmcl.ui.FXUtils;

/// @author Glavo
Expand All @@ -34,31 +30,13 @@ public LineButtonBase() {
this.getStyleClass().addAll(LineButtonBase.DEFAULT_STYLE_CLASS);

this.ripplerContainer = new RipplerContainer(container);
container.getChildren().addListener((ListChangeListener<Node>) change -> updateCursor());
disabledProperty().addListener(observable -> updateCursor());
FXUtils.setOverflowHidden(this);
FXUtils.onClicked(this, this::fire);

this.getChildren().setAll(ripplerContainer);
updateCursor();
}

public void fire() {
fireEvent(new ActionEvent(this, this));
}

/// Updates the cursor shown by the row rippler.
private void updateCursor() {
applyCursor(this, isDisabled() ? Cursor.DEFAULT : Cursor.HAND);
}

/// Applies the row cursor to every current child node that may receive mouse hover.
private static void applyCursor(Node node, Cursor cursor) {
node.setCursor(cursor);
if (node instanceof Parent parent) {
for (Node child : parent.getChildrenUnmodifiable()) {
applyCursor(child, cursor);
}
}
}
}
Loading