在播放 UI 动画前应用 UI 布局 (#7076)

This commit is contained in:
Glavo authored and GitHub committed 2026-10-25 19:17:39 +08:00
1 parent fb8a96a170
commit 58eb452e90
4 files changed
+28 -50

No files matched your search

@@ -168,14 +168,6 @@ public final class Controllers {
return gameInstancePage;
}
@FXThread
public static void prepareGameInstancePage() {
if (gameInstancePage == null) {
LOG.info("Prepare the game instance page");
gameInstancePage = FXUtils.prepareNode(new GameInstancePage());
}
}
@FXThread
public static GameListPage getGameListPage() {
return gameListPage.get();
@@ -194,14 +186,6 @@ public final class Controllers {
return settingsPage;
}
@FXThread
public static void prepareSettingsPage() {
if (settingsPage == null) {
LOG.info("Prepare the settings page");
settingsPage = FXUtils.prepareNode(new LauncherSettingsPage());
}
}
@FXThread
public static AccountListPage getAccountListPage() {
return accountListPage.get();
@@ -215,14 +199,6 @@ public final class Controllers {
return downloadPage;
}
@FXThread
public static void prepareDownloadPage() {
if (downloadPage == null) {
LOG.info("Prepare the download page");
downloadPage = FXUtils.prepareNode(new DownloadPage());
}
}
@FXThread
public static Node getTerracottaPage() {
return terracottaPage.get();
@@ -36,7 +36,6 @@ import javafx.collections.ObservableList;
import javafx.collections.ObservableMap;
import javafx.event.Event;
import javafx.event.EventDispatcher;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.geometry.Bounds;
import javafx.geometry.Rectangle2D;
@@ -1226,16 +1225,6 @@ public final class FXUtils {
return node;
}
public static void prepareOnMouseEnter(Node node, Runnable action) {
node.addEventFilter(MouseEvent.MOUSE_ENTERED, new EventHandler<>() {
@Override
public void handle(MouseEvent e) {
node.removeEventFilter(MouseEvent.MOUSE_ENTERED, this);
action.run();
}
});
}
public static <T> void onScroll(Node node, List<T> list,
ToIntFunction<List<T>> finder,
Consumer<T> updater
@@ -26,31 +26,46 @@ import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
/// Displays one page, retaining the preceding page while a transition runs.
/// Content changes must be made on the JavaFX Application Thread.
@NotNullByDefault
public class TransitionPane extends StackPane {
private Node currentNode;
/// The requested page, or `null` before content is assigned.
private @Nullable Node currentNode;
/// Creates an empty container that clips content to its bounds.
public TransitionPane() {
FXUtils.setOverflowHidden(this);
}
public Node getCurrentNode() {
/// Returns the requested page, or `null` if none has been assigned.
public @Nullable Node getCurrentNode() {
return currentNode;
}
/// Replaces the content using [Motion#SHORT4] and [Motion#EASE].
public final void setContent(Node newView, AnimationProducer transition) {
setContent(newView, transition, Motion.SHORT4);
}
/// Replaces the content using the given duration and [Motion#EASE].
public final void setContent(Node newView, AnimationProducer transition, Duration duration) {
setContent(newView, transition, duration, Motion.EASE);
}
/// Replaces the content using the given animation and timing.
/// The first page, repeated assignments of the current page, and replacements
/// without animations are displayed immediately. Otherwise, CSS and layout
/// are processed in this container before the animation starts in a later
/// event-loop task. Mouse input is disabled while the transition is pending
/// or running.
public void setContent(Node newView, AnimationProducer transition,
Duration duration, Interpolator interpolator) {
Node previousNode = currentNode != newView && getWidth() > 0 && getHeight() > 0 ? currentNode : null;
@Nullable Node previousNode = currentNode != newView && getWidth() > 0 && getHeight() > 0 ? currentNode : null;
currentNode = newView;
if (!AnimationUtils.isAnimationEnabled() || previousNode == null || transition == ContainerAnimations.NONE) {
@@ -64,7 +79,7 @@ public class TransitionPane extends StackPane {
setMouseTransparent(true);
transition.init(this, previousNode, newView);
CacheHint cacheHint = newView instanceof Cacheable cacheable
@Nullable CacheHint cacheHint = newView instanceof Cacheable cacheable
? cacheable.getCacheHint(transition)
: null;
@@ -73,8 +88,12 @@ public class TransitionPane extends StackPane {
newView.setCacheHint(cacheHint);
}
// runLater or "init" will not work
// Let navigation handlers finish updating the page before preparing it.
Platform.runLater(() -> {
// runLater alone does not guarantee a CSS or layout pass. Keep that
// first-use work outside the animation's elapsed time.
applyCss();
layout();
Animation newAnimation = transition.animate(
this,
previousNode,
@@ -95,15 +114,19 @@ public class TransitionPane extends StackPane {
}
/// Prepares page properties and creates animations for content replacements.
public interface AnimationProducer {
/// Resets page transforms, making the old page opaque and the new page transparent.
default void init(TransitionPane container, Node previousNode, Node nextNode) {
AnimationUtils.reset(previousNode, true);
AnimationUtils.reset(nextNode, false);
}
/// Creates the transition between the prepared pages using the given timing.
Animation animate(Pane container, Node previousNode, Node nextNode,
Duration duration, Interpolator interpolator);
/// Returns the reverse transition, or `null` if none is specified.
default @Nullable TransitionPane.AnimationProducer opposite() {
return null;
}
@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.account.AccountAdvancedListItem;
import org.jackhuang.hmcl.ui.account.AccountListPopupMenu;
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.AdvancedListItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -131,9 +130,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
@Nullable GameInstanceID currentId = currentGame != null ? currentGame.getId() : null;
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
}, instance -> instance.getRepository().setSelectedInstance(instance));
if (AnimationUtils.isAnimationEnabled()) {
FXUtils.prepareOnMouseEnter(gameListItem, Controllers::prepareGameInstancePage);
}
FXUtils.onSecondaryButtonClicked(gameListItem, () -> showGameListPopupMenu(gameListItem));
// third item in left sidebar
@@ -148,9 +144,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
downloadItem.setLeftIcon(SVG.DOWNLOAD);
downloadItem.setTitle(i18n("download"));
downloadItem.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
if (AnimationUtils.isAnimationEnabled()) {
FXUtils.prepareOnMouseEnter(downloadItem, Controllers::prepareDownloadPage);
}
// fifth item in left sidebar
AdvancedListItem launcherSettingsItem = new AdvancedListItem();
@@ -160,9 +153,6 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
Controllers.getSettingsPage().showGameSettings(GameDirectoryManager.getSelectedRepository());
Controllers.navigate(Controllers.getSettingsPage());
});
if (AnimationUtils.isAnimationEnabled()) {
FXUtils.prepareOnMouseEnter(launcherSettingsItem, Controllers::prepareSettingsPage);
}
// sixth item in left sidebar
AdvancedListItem terracottaItem = new AdvancedListItem();