From 9dfc6155c47f5d39d5426c80edc30cfcc9e0ee83 Mon Sep 17 00:00:00 2001 From: Glavo Date: Mon, 17 Nov 2025 15:42:55 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=20JFXListView=20=E4=B8=8A=E5=90=AF?= =?UTF-8?q?=E7=94=A8=E5=B9=B3=E6=BB=91=E6=BB=9A=E5=8A=A8=20(#4809)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/jfoenix/skins/JFXListViewSkin.java | 2 + .../java/org/jackhuang/hmcl/ui/FXUtils.java | 6 ++ .../org/jackhuang/hmcl/ui/ScrollUtils.java | 71 +++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/HMCL/src/main/java/com/jfoenix/skins/JFXListViewSkin.java b/HMCL/src/main/java/com/jfoenix/skins/JFXListViewSkin.java index e9e350b85..63cccd21f 100644 --- a/HMCL/src/main/java/com/jfoenix/skins/JFXListViewSkin.java +++ b/HMCL/src/main/java/com/jfoenix/skins/JFXListViewSkin.java @@ -26,6 +26,7 @@ import com.jfoenix.effects.JFXDepthManager; import javafx.scene.control.ListCell; import javafx.scene.control.skin.VirtualFlow; import javafx.scene.layout.Region; +import org.jackhuang.hmcl.ui.FXUtils; // https://github.com/HMCL-dev/HMCL/issues/4720 public class JFXListViewSkin extends ListViewSkin { @@ -38,6 +39,7 @@ public class JFXListViewSkin extends ListViewSkin { flow = (VirtualFlow>) getChildren().get(0); JFXDepthManager.setDepth(flow, listView.depthProperty().get()); listView.depthProperty().addListener((o, oldVal, newVal) -> JFXDepthManager.setDepth(flow, newVal)); + FXUtils.smoothScrolling(flow); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java index 765426c91..c8a072b06 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -42,6 +42,7 @@ import javafx.scene.Scene; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.*; +import javafx.scene.control.skin.VirtualFlow; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.input.*; @@ -394,6 +395,11 @@ public final class FXUtils { ScrollUtils.addSmoothScrolling(scrollPane); } + public static void smoothScrolling(VirtualFlow virtualFlow) { + if (AnimationUtils.isAnimationEnabled()) + ScrollUtils.addSmoothScrolling(virtualFlow); + } + /// If the current environment is JavaFX 23 or higher, this method returns [Labeled#textTruncatedProperty()]; /// Otherwise, it returns `null`. public static @Nullable ReadOnlyBooleanProperty textTruncatedProperty(Labeled labeled) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ScrollUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ScrollUtils.java index 268bf50b1..72b2076dd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/ScrollUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/ScrollUtils.java @@ -25,7 +25,9 @@ import javafx.animation.Animation.Status; import javafx.animation.KeyFrame; import javafx.animation.Timeline; import javafx.event.EventHandler; +import javafx.scene.control.IndexedCell; import javafx.scene.control.ScrollPane; +import javafx.scene.control.skin.VirtualFlow; import javafx.scene.input.MouseEvent; import javafx.scene.input.ScrollEvent; import javafx.util.Duration; @@ -139,6 +141,21 @@ final class ScrollUtils { smoothScroll(scrollPane, speed, trackPadAdjustment); } + /// @author Glavo + public static void addSmoothScrolling(VirtualFlow virtualFlow) { + addSmoothScrolling(virtualFlow, 1); + } + + /// @author Glavo + public static void addSmoothScrolling(VirtualFlow virtualFlow, double speed) { + addSmoothScrolling(virtualFlow, speed, 7); + } + + /// @author Glavo + public static void addSmoothScrolling(VirtualFlow virtualFlow, double speed, double trackPadAdjustment) { + smoothScroll(virtualFlow, speed, trackPadAdjustment); + } + private static final double[] FRICTIONS = {0.99, 0.1, 0.05, 0.04, 0.03, 0.02, 0.01, 0.04, 0.01, 0.008, 0.008, 0.008, 0.008, 0.0006, 0.0005, 0.00003, 0.00001}; private static final Duration DURATION = Duration.millis(3); @@ -207,4 +224,58 @@ final class ScrollUtils { timeline.setCycleCount(Animation.INDEFINITE); } + /// @author Glavo + private static void smoothScroll(VirtualFlow virtualFlow, double speed, double trackPadAdjustment) { + if (!virtualFlow.isVertical()) + return; + + final double[] derivatives = new double[FRICTIONS.length]; + + Timeline timeline = new Timeline(); + Holder scrollDirectionHolder = new Holder<>(); + final EventHandler mouseHandler = event -> timeline.stop(); + final EventHandler scrollHandler = event -> { + if (event.getEventType() == ScrollEvent.SCROLL) { + ScrollDirection scrollDirection = determineScrollDirection(event); + if (scrollDirection == ScrollDirection.LEFT || scrollDirection == ScrollDirection.RIGHT) { + return; + } + scrollDirectionHolder.value = scrollDirection; + double currentSpeed = isTrackPad(event, scrollDirection) ? speed / trackPadAdjustment : speed; + + derivatives[0] += scrollDirection.intDirection * currentSpeed; + if (timeline.getStatus() == Status.STOPPED) { + timeline.play(); + } + event.consume(); + } + }; + virtualFlow.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseHandler); + virtualFlow.addEventFilter(ScrollEvent.ANY, scrollHandler); + + timeline.getKeyFrames().add(new KeyFrame(DURATION, event -> { + for (int i = 0; i < derivatives.length; i++) { + derivatives[i] *= FRICTIONS[i]; + } + for (int i = 1; i < derivatives.length; i++) { + derivatives[i] += derivatives[i - 1]; + } + + double dy = derivatives[derivatives.length - 1]; + + int cellCount = virtualFlow.getCellCount(); + IndexedCell firstVisibleCell = virtualFlow.getFirstVisibleCell(); + double height = firstVisibleCell != null ? firstVisibleCell.getHeight() * cellCount : 0.0; + + double delta = height > 0.0 + ? dy / height + : (scrollDirectionHolder.value == ScrollDirection.DOWN ? 0.001 : -0.001); + virtualFlow.setPosition(Math.min(Math.max(virtualFlow.getPosition() + delta, 0), 1)); + + if (Math.abs(dy) < 0.001) { + timeline.stop(); + } + })); + timeline.setCycleCount(Animation.INDEFINITE); + } }