在播放 UI 动画前预渲染页面 (#7160)
This commit is contained in:
1 parent
8047775bf8
commit
f81ba320da
3 files changed
+73
-27
No files matched your search
@@ -183,14 +183,12 @@ public final class Controllers {
|
||||
/// The cached settings page.
|
||||
private static @Nullable LauncherSettingsPage settingsPage;
|
||||
|
||||
/// Creates the settings page, loads the default preset, and attaches it for layout unless already cached.
|
||||
/// Creates and prewarms the settings page unless already cached.
|
||||
@FXThread
|
||||
public static void prepareSettingsPage() {
|
||||
if (settingsPage == null) {
|
||||
LOG.info("Preparing settings page");
|
||||
LauncherSettingsPage page = new LauncherSettingsPage();
|
||||
page.showGameSettings(GameDirectoryManager.getSelectedRepository());
|
||||
settingsPage = getDecorator().prewarmPage(page);
|
||||
settingsPage = getDecorator().prewarmPage(new LauncherSettingsPage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,12 @@ import org.jetbrains.annotations.Nullable;
|
||||
@NotNullByDefault
|
||||
public class TransitionPane extends StackPane {
|
||||
|
||||
/// Marks pages that have completed a rendering preparation or entered their displayed state.
|
||||
private static final Object RENDER_INITIALIZED_KEY = new Object();
|
||||
|
||||
/// Keeps the first page render active while contributing negligible visible opacity.
|
||||
private static final double PREPARATION_OPACITY = 0.00001;
|
||||
|
||||
/// The requested page, or `null` when content is absent.
|
||||
private @Nullable Node currentNode;
|
||||
|
||||
@@ -31,7 +37,7 @@ public class TransitionPane extends StackPane {
|
||||
/// The hidden preparation container, or `null` when no candidate is retained.
|
||||
private @Nullable StackPane preloadPane;
|
||||
|
||||
/// Whether the candidate must survive the next content layout pass.
|
||||
/// Whether the candidate is waiting for its first content layout pass.
|
||||
private boolean preloadLayoutPending;
|
||||
|
||||
/// Creates an empty container that clips content to its bounds.
|
||||
@@ -58,12 +64,12 @@ public class TransitionPane extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches unowned content for hidden CSS and layout prewarming during scene pulses.
|
||||
/// Attaches unowned content for hidden CSS and layout prewarming.
|
||||
/// Replaces any previous candidate without changing the current content. The hidden
|
||||
/// container uses a snapshot of this pane's content-area size and does not affect
|
||||
/// its size requirements. It is retained through the next content layout pass
|
||||
/// and released on a later pass. Any call to [#setContent(Node, AnimationProducer)]
|
||||
/// cancels pending prewarming and releases the hidden container.
|
||||
/// its size requirements. The content is detached after the layout pass.
|
||||
/// Any call to [#setContent(Node, AnimationProducer)] cancels pending prewarming
|
||||
/// and releases the hidden container.
|
||||
/// The caller must retain the content for subsequent use. This method does not wait
|
||||
/// for prewarming to finish and must be called on the JavaFX Application Thread.
|
||||
///
|
||||
@@ -74,14 +80,12 @@ public class TransitionPane extends StackPane {
|
||||
if (content.getParent() != null || content.getScene() != null) {
|
||||
throw new IllegalArgumentException("Content already belongs to a parent or scene");
|
||||
}
|
||||
if (preloadPane == null) {
|
||||
preloadPane = new StackPane();
|
||||
preloadPane.setVisible(false);
|
||||
preloadPane.setManaged(false);
|
||||
preloadPane.setMouseTransparent(true);
|
||||
getChildren().add(0, preloadPane);
|
||||
}
|
||||
preloadPane.getChildren().setAll(content);
|
||||
disposePreloadPane();
|
||||
preloadPane = new StackPane(content);
|
||||
preloadPane.setVisible(false);
|
||||
preloadPane.setManaged(false);
|
||||
preloadPane.setMouseTransparent(true);
|
||||
getChildren().add(0, preloadPane);
|
||||
preloadPane.resizeRelocate(snappedLeftInset(), snappedTopInset(),
|
||||
Math.max(0, getWidth() - snappedLeftInset() - snappedRightInset()),
|
||||
Math.max(0, getHeight() - snappedTopInset() - snappedBottomInset()));
|
||||
@@ -90,18 +94,29 @@ public class TransitionPane extends StackPane {
|
||||
return content;
|
||||
}
|
||||
|
||||
/// Lays out displayed pages and releases a candidate retained from an earlier pass.
|
||||
/// Releases the preparation container after its descendants have been laid out.
|
||||
@Override
|
||||
protected void layoutChildren() {
|
||||
super.layoutChildren();
|
||||
if (preloadPane != null && getScene() != null) {
|
||||
// Descendant layout follows this method; keep a new candidate attached
|
||||
// through that traversal instead of treating this callback as completion.
|
||||
if (preloadLayoutPending) {
|
||||
preloadLayoutPending = false;
|
||||
} else {
|
||||
disposePreloadPane();
|
||||
}
|
||||
@Nullable Scene scene = getScene();
|
||||
if (preloadPane != null && scene != null && preloadLayoutPending) {
|
||||
preloadLayoutPending = false;
|
||||
StackPane candidate = preloadPane;
|
||||
Platform.runLater(() -> {
|
||||
if (preloadPane == candidate) {
|
||||
disposePreloadPane();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Suppresses future prewarming after a page enters its displayed state in a showing window.
|
||||
private static void markDisplayed(Node content) {
|
||||
@Nullable Scene scene = content.getScene();
|
||||
@Nullable Window window = scene == null ? null : scene.getWindow();
|
||||
if (window != null && window.isShowing()
|
||||
&& content.isVisible() && content.getOpacity() > 0) {
|
||||
content.getProperties().put(RENDER_INITIALIZED_KEY, Boolean.TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,11 +151,14 @@ public class TransitionPane extends StackPane {
|
||||
}
|
||||
|
||||
/// Replaces the current page, cancelling any preceding transition.
|
||||
/// Animated changes wait for a scene CSS and layout pass; mouse input is
|
||||
/// Animated changes wait for CSS, layout, and first-display rendering preparation; mouse input is
|
||||
/// suppressed while pending or running. Detaching the container or hiding
|
||||
/// its window cancels the transition. Cancellation applies the end state
|
||||
/// of a started animation with a finite duration and restores the target page
|
||||
/// through [AnimationProducer#finish(Node)], even if playback has not started.
|
||||
/// Previously undisplayed pages initialized at zero opacity render almost transparently
|
||||
/// before playback, provided they are visible and their opacity is not bound. Preparation
|
||||
/// preserves initialized transforms, so offscreen content may still be culled.
|
||||
public void setContent(Node newView, AnimationProducer transition,
|
||||
Duration duration, Interpolator interpolator) {
|
||||
if (activeTransition != null) {
|
||||
@@ -156,6 +174,7 @@ public class TransitionPane extends StackPane {
|
||||
|| window == null || !window.isShowing()) {
|
||||
transition.finish(newView);
|
||||
replaceContentNodes(null, newView);
|
||||
markDisplayed(newView);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -172,10 +191,21 @@ public class TransitionPane extends StackPane {
|
||||
private final Runnable layoutListener = this::layoutCompleted;
|
||||
/// The animation created after preparation, or `null` before playback.
|
||||
private @Nullable Animation animation;
|
||||
/// Whether the near-transparent render still needs a subsequent scene synchronization.
|
||||
private boolean renderingPending;
|
||||
|
||||
/// Arms a one-shot layout notification after navigation handlers return.
|
||||
@Override
|
||||
public void start() {
|
||||
renderingPending = !newView.getProperties().containsKey(RENDER_INITIALIZED_KEY)
|
||||
&& newView.isVisible() && !newView.opacityProperty().isBound()
|
||||
&& newView.getOpacity() == 0;
|
||||
if (renderingPending) {
|
||||
newView.setOpacity(PREPARATION_OPACITY);
|
||||
}
|
||||
if (!canContinue()) {
|
||||
return;
|
||||
}
|
||||
super.start();
|
||||
Platform.runLater(() -> {
|
||||
if (canContinue()) {
|
||||
@@ -193,6 +223,17 @@ public class TransitionPane extends StackPane {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (renderingPending) {
|
||||
renderingPending = false;
|
||||
// This dirty state forces the next scene synchronization to wait for
|
||||
// the preceding render. Only its following runLater starts the clock.
|
||||
newView.setOpacity(0);
|
||||
if (canContinue()) {
|
||||
scene.addPostLayoutPulseListener(layoutListener);
|
||||
Platform.requestNextPulse();
|
||||
}
|
||||
return;
|
||||
}
|
||||
animation = transition.animate(TransitionPane.this, previousNode, newView, duration, interpolator);
|
||||
if (activeTransition != this) {
|
||||
return;
|
||||
@@ -246,6 +287,9 @@ public class TransitionPane extends StackPane {
|
||||
newView.setCacheHint(originalCacheHint);
|
||||
}
|
||||
transition.finish(newView);
|
||||
if (animation != null) {
|
||||
markDisplayed(newView);
|
||||
}
|
||||
getChildren().removeIf(node -> node != newView && node != preloadPane);
|
||||
setMouseTransparent(originalMouseTransparent);
|
||||
}
|
||||
|
||||
@@ -328,6 +328,8 @@ final class MainWindowPane extends StackPane {
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(container.getWidth());
|
||||
// Clipping hides the title; nonzero opacity skips offscreen rendering preparation.
|
||||
nextNode.setOpacity(1);
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
@@ -355,6 +357,8 @@ final class MainWindowPane extends StackPane {
|
||||
public void init(TransitionPane container, Node previousNode, Node nextNode) {
|
||||
super.init(container, previousNode, nextNode);
|
||||
nextNode.setTranslateX(-container.getWidth());
|
||||
// Clipping hides the title; nonzero opacity skips offscreen rendering preparation.
|
||||
nextNode.setOpacity(1);
|
||||
}
|
||||
|
||||
/// {@inheritDoc}
|
||||
|
||||
Reference in new issue
Block a user