From 44869da20e4e8f47581156987fda741e7d04eaac Mon Sep 17 00:00:00 2001 From: Glavo Date: Fri, 14 Nov 2025 16:17:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E7=95=8C=E9=9D=A2=E5=8A=A8?= =?UTF-8?q?=E7=94=BB=E6=95=88=E6=9E=9C=20(#4780)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/jfoenix/controls/JFXDialog.java | 562 +++++++++++++ .../org/jackhuang/hmcl/ui/Controllers.java | 17 +- .../java/org/jackhuang/hmcl/ui/FXUtils.java | 30 +- .../hmcl/ui/animation/AnimationHandler.java | 32 - .../hmcl/ui/animation/AnimationProducer.java | 31 - .../ui/animation/ContainerAnimations.java | 361 +++----- .../jackhuang/hmcl/ui/animation/Motion.java | 781 ++++++++++++++++++ .../hmcl/ui/animation/TransitionPane.java | 111 +-- .../hmcl/ui/construct/Navigator.java | 34 +- .../hmcl/ui/construct/RipplerContainer.java | 6 +- .../hmcl/ui/construct/TabHeader.java | 35 +- .../hmcl/ui/decorator/Decorator.java | 49 +- .../decorator/DecoratorAnimationProducer.java | 90 -- .../ui/decorator/DecoratorController.java | 75 +- .../ui/decorator/DecoratorNavigatorPage.java | 57 -- .../hmcl/ui/decorator/DecoratorSkin.java | 96 ++- .../hmcl/ui/decorator/DecoratorTabPage.java | 73 -- .../ui/decorator/DecoratorTransitionPage.java | 7 +- .../decorator/DecoratorWizardDisplayer.java | 3 +- .../ui/download/AbstractInstallersPage.java | 12 +- .../hmcl/ui/download/DownloadPage.java | 5 +- .../hmcl/ui/main/LauncherSettingsPage.java | 6 +- .../terracotta/TerracottaControllerPage.java | 4 +- .../hmcl/ui/terracotta/TerracottaPage.java | 7 +- .../hmcl/ui/versions/VersionPage.java | 6 +- .../hmcl/ui/versions/WorldManagePage.java | 6 +- .../jackhuang/hmcl/ui/wizard/Navigation.java | 8 +- .../hmcl/ui/wizard/WizardController.java | 12 +- 28 files changed, 1804 insertions(+), 712 deletions(-) create mode 100644 HMCL/src/main/java/com/jfoenix/controls/JFXDialog.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationHandler.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java create mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/Motion.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorAnimationProducer.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorNavigatorPage.java delete mode 100644 HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTabPage.java diff --git a/HMCL/src/main/java/com/jfoenix/controls/JFXDialog.java b/HMCL/src/main/java/com/jfoenix/controls/JFXDialog.java new file mode 100644 index 000000000..151cefe61 --- /dev/null +++ b/HMCL/src/main/java/com/jfoenix/controls/JFXDialog.java @@ -0,0 +1,562 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package com.jfoenix.controls; + +import com.jfoenix.controls.events.JFXDialogEvent; +import com.jfoenix.converters.DialogTransitionConverter; +import com.jfoenix.effects.JFXDepthManager; +import com.jfoenix.transitions.CachedTransition; +import javafx.animation.*; +import javafx.beans.DefaultProperty; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.ObjectPropertyBase; +import javafx.beans.property.SimpleBooleanProperty; +import javafx.css.*; +import javafx.event.Event; +import javafx.event.EventHandler; +import javafx.geometry.Pos; +import javafx.scene.CacheHint; +import javafx.scene.Node; +import javafx.scene.SnapshotParameters; +import javafx.scene.image.ImageView; +import javafx.scene.image.WritableImage; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.*; +import javafx.scene.paint.Color; +import javafx.util.Duration; +import org.jackhuang.hmcl.ui.animation.Motion; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/// Note: for JFXDialog to work properly, the root node **MUST** +/// be of type [StackPane] +/// +/// @author Shadi Shaheen +/// @version 1.0 +/// @since 2016-03-09 +@DefaultProperty(value = "content") +public class JFXDialog extends StackPane { + + private static final double INITIAL_SCALE = 0.8; + + // public static enum JFXDialogLayout{PLAIN, HEADING, ACTIONS, BACKDROP}; + public enum DialogTransition { + CENTER, NONE + } + + private StackPane contentHolder; + + private double offsetX = 0; + private double offsetY = 0; + + private StackPane dialogContainer; + private Region content; + private Transition showAnimation; + private Transition hideAnimation; + + private final EventHandler closeHandler = e -> close(); + + /// creates empty JFXDialog control with CENTER animation type + public JFXDialog() { + this(null, null, DialogTransition.CENTER); + } + + /// creates empty JFXDialog control with a specified animation type + public JFXDialog(DialogTransition transition) { + this(null, null, transition); + } + + /// creates JFXDialog control with a specified animation type, the animation type + /// can be one of the following: + /// + /// - CENTER + /// - TOP + /// - RIGHT + /// - BOTTOM + /// - LEFT + /// + /// @param dialogContainer is the parent of the dialog, it + /// @param content the content of dialog + /// @param transitionType the animation type + public JFXDialog(StackPane dialogContainer, Region content, DialogTransition transitionType) { + initialize(); + setContent(content); + setDialogContainer(dialogContainer); + this.transitionType.set(transitionType); + // init change listeners + initChangeListeners(); + } + + /// creates JFXDialog control with a specified animation type that + /// is closed when clicking on the overlay, the animation type + /// can be one of the following: + /// + /// - CENTER + /// - TOP + /// - RIGHT + /// - BOTTOM + /// - LEFT + /// + public JFXDialog(StackPane dialogContainer, Region content, DialogTransition transitionType, boolean overlayClose) { + setOverlayClose(overlayClose); + initialize(); + setContent(content); + setDialogContainer(dialogContainer); + this.transitionType.set(transitionType); + // init change listeners + initChangeListeners(); + } + + private void initChangeListeners() { + overlayCloseProperty().addListener((o, oldVal, newVal) -> { + if (newVal) { + this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); + } else { + this.removeEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); + } + }); + } + + private void initialize() { + this.setVisible(false); + this.getStyleClass().add(DEFAULT_STYLE_CLASS); + this.transitionType.addListener((o, oldVal, newVal) -> { + showAnimation = getShowAnimation(transitionType.get()); + hideAnimation = getHideAnimation(transitionType.get()); + }); + + contentHolder = new StackPane(); + contentHolder.setBackground(new Background(new BackgroundFill(Color.WHITE, new CornerRadii(2), null))); + JFXDepthManager.setDepth(contentHolder, 4); + contentHolder.setPickOnBounds(false); + // ensure stackpane is never resized beyond it's preferred size + contentHolder.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE); + this.getChildren().add(contentHolder); + this.getStyleClass().add("jfx-dialog-overlay-pane"); + StackPane.setAlignment(contentHolder, Pos.CENTER); + this.setBackground(new Background(new BackgroundFill(Color.rgb(0, 0, 0, 0.1), null, null))); + // close the dialog if clicked on the overlay pane + if (overlayClose.get()) { + this.addEventHandler(MouseEvent.MOUSE_PRESSED, closeHandler); + } + // prevent propagating the events to overlay pane + contentHolder.addEventHandler(MouseEvent.ANY, Event::consume); + } + + /*************************************************************************** + * * + * Setters / Getters * + * * + **************************************************************************/ + + /// @return the dialog container + public StackPane getDialogContainer() { + return dialogContainer; + } + + /// set the dialog container + /// Note: the dialog container must be StackPane, its the container for the dialog to be shown in. + public void setDialogContainer(StackPane dialogContainer) { + if (dialogContainer != null) { + this.dialogContainer = dialogContainer; + // FIXME: need to be improved to consider only the parent boundary + offsetX = dialogContainer.getBoundsInLocal().getWidth(); + offsetY = dialogContainer.getBoundsInLocal().getHeight(); + showAnimation = getShowAnimation(transitionType.get()); + hideAnimation = getHideAnimation(transitionType.get()); + } + } + + /// @return dialog content node + public Region getContent() { + return content; + } + + /// set the content of the dialog + public void setContent(Region content) { + if (content != null) { + this.content = content; + this.content.setPickOnBounds(false); + contentHolder.getChildren().setAll(content); + } + } + + /// indicates whether the dialog will close when clicking on the overlay or not + private final BooleanProperty overlayClose = new SimpleBooleanProperty(true); + + public final BooleanProperty overlayCloseProperty() { + return this.overlayClose; + } + + public final boolean isOverlayClose() { + return this.overlayCloseProperty().get(); + } + + public final void setOverlayClose(final boolean overlayClose) { + this.overlayCloseProperty().set(overlayClose); + } + + /// if sets to true, the content of dialog container will be cached and replaced with an image + /// when displaying the dialog (better performance). + /// this is recommended if the content behind the dialog will not change during the showing + /// period + private final BooleanProperty cacheContainer = new SimpleBooleanProperty(false); + + public boolean isCacheContainer() { + return cacheContainer.get(); + } + + public BooleanProperty cacheContainerProperty() { + return cacheContainer; + } + + public void setCacheContainer(boolean cacheContainer) { + this.cacheContainer.set(cacheContainer); + } + + /// it will show the dialog in the specified container + public void show(StackPane dialogContainer) { + this.setDialogContainer(dialogContainer); + showDialog(); + } + + private ArrayList tempContent; + + /** + * show the dialog inside its parent container + */ + public void show() { + this.setDialogContainer(dialogContainer); + showDialog(); + } + + private void showDialog() { + if (dialogContainer == null) { + throw new RuntimeException("ERROR: JFXDialog container is not set!"); + } + if (isCacheContainer()) { + tempContent = new ArrayList<>(dialogContainer.getChildren()); + + SnapshotParameters snapShotparams = new SnapshotParameters(); + snapShotparams.setFill(Color.TRANSPARENT); + WritableImage temp = dialogContainer.snapshot(snapShotparams, + new WritableImage((int) dialogContainer.getWidth(), + (int) dialogContainer.getHeight())); + ImageView tempImage = new ImageView(temp); + tempImage.setCache(true); + tempImage.setCacheHint(CacheHint.SPEED); + dialogContainer.getChildren().setAll(tempImage, this); + } else { + //prevent error if opening an already opened dialog + dialogContainer.getChildren().remove(this); + tempContent = null; + dialogContainer.getChildren().add(this); + } + + if (showAnimation != null) { + showAnimation.play(); + } else { + setVisible(true); + setOpacity(1); + Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.OPENED)); + } + } + + /** + * close the dialog + */ + public void close() { + if (hideAnimation != null) { + hideAnimation.play(); + } else { + setOpacity(0); + setVisible(false); + closeDialog(); + } + } + + private void closeDialog() { + resetProperties(); + Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.CLOSED)); + if (tempContent == null) { + dialogContainer.getChildren().remove(this); + } else { + dialogContainer.getChildren().setAll(tempContent); + } + } + + /*************************************************************************** + * * + * Transitions * + * * + **************************************************************************/ + + private Transition getShowAnimation(DialogTransition transitionType) { + Transition animation = null; + if (contentHolder != null) { + animation = switch (transitionType) { + case CENTER -> { + contentHolder.setScaleX(INITIAL_SCALE); + contentHolder.setScaleY(INITIAL_SCALE); + yield new CenterTransition(); + } + case NONE -> { + contentHolder.setScaleX(1); + contentHolder.setScaleY(1); + contentHolder.setTranslateX(0); + contentHolder.setTranslateY(0); + yield null; + } + }; + } + if (animation != null) { + animation.setOnFinished(finish -> + Event.fireEvent(JFXDialog.this, new JFXDialogEvent(JFXDialogEvent.OPENED))); + } + return animation; + } + + private Transition getHideAnimation(DialogTransition transitionType) { + Transition animation = null; + if (contentHolder != null) { + animation = switch (transitionType) { + case CENTER -> new HideTransition(); + case NONE -> null; + }; + } + if (animation != null) { + animation.setOnFinished(finish -> closeDialog()); + } + return animation; + } + + private void resetProperties() { + this.setVisible(false); + contentHolder.setTranslateX(0); + contentHolder.setTranslateY(0); + contentHolder.setScaleX(1); + contentHolder.setScaleY(1); + } + + private final class HideTransition extends CachedTransition { + private static final Interpolator INTERPOLATOR = Motion.EMPHASIZED_ACCELERATE; + + public HideTransition() { + super(contentHolder, new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(contentHolder.scaleXProperty(), 1, INTERPOLATOR), + new KeyValue(contentHolder.scaleYProperty(), 1, INTERPOLATOR), + new KeyValue(JFXDialog.this.opacityProperty(), 1, INTERPOLATOR), + new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR) + ), + new KeyFrame(Motion.LONG2.subtract(Duration.millis(10)), + new KeyValue(JFXDialog.this.visibleProperty(), false, Motion.LINEAR), + new KeyValue(JFXDialog.this.opacityProperty(), 0, INTERPOLATOR) + ), + new KeyFrame(Motion.LONG2, + new KeyValue(contentHolder.scaleXProperty(), INITIAL_SCALE, INTERPOLATOR), + new KeyValue(contentHolder.scaleYProperty(), INITIAL_SCALE, INTERPOLATOR) + )) + ); + // reduce the number to increase the shifting , increase number to reduce shifting + setCycleDuration(Duration.seconds(0.4)); + setDelay(Duration.ZERO); + } + } + + private final class CenterTransition extends CachedTransition { + private static final Interpolator INTERPOLATOR = Motion.EMPHASIZED_DECELERATE; + + CenterTransition() { + super(contentHolder, new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(contentHolder.scaleXProperty(), INITIAL_SCALE, INTERPOLATOR), + new KeyValue(contentHolder.scaleYProperty(), INITIAL_SCALE, INTERPOLATOR), + new KeyValue(JFXDialog.this.visibleProperty(), false, Motion.LINEAR) + ), + new KeyFrame(Duration.millis(10), + new KeyValue(JFXDialog.this.visibleProperty(), true, Motion.LINEAR), + new KeyValue(JFXDialog.this.opacityProperty(), 0, INTERPOLATOR) + ), + new KeyFrame(Motion.EXTRA_LONG4, + new KeyValue(contentHolder.scaleXProperty(), 1, INTERPOLATOR), + new KeyValue(contentHolder.scaleYProperty(), 1, INTERPOLATOR), + new KeyValue(JFXDialog.this.opacityProperty(), 1, INTERPOLATOR) + )) + ); + // reduce the number to increase the shifting , increase number to reduce shifting + setCycleDuration(Duration.seconds(0.4)); + setDelay(Duration.ZERO); + } + } + + /*************************************************************************** + * * + * Stylesheet Handling * + * * + **************************************************************************/ + + /// Initialize the style class to 'jfx-dialog'. + /// + /// This is the selector class from which CSS can be used to style + /// this control. + private static final String DEFAULT_STYLE_CLASS = "jfx-dialog"; + + /// dialog transition type property, it can be one of the following: + /// + /// - CENTER + /// - TOP + /// - RIGHT + /// - BOTTOM + /// - LEFT + /// - NONE + /// + private final StyleableObjectProperty transitionType = new SimpleStyleableObjectProperty<>( + StyleableProperties.DIALOG_TRANSITION, + JFXDialog.this, + "dialogTransition", + DialogTransition.CENTER); + + public DialogTransition getTransitionType() { + return transitionType == null ? DialogTransition.CENTER : transitionType.get(); + } + + public StyleableObjectProperty transitionTypeProperty() { + return this.transitionType; + } + + public void setTransitionType(DialogTransition transition) { + this.transitionType.set(transition); + } + + private static final class StyleableProperties { + private static final CssMetaData DIALOG_TRANSITION = + new CssMetaData("-jfx-dialog-transition", + DialogTransitionConverter.getInstance(), + DialogTransition.CENTER) { + @Override + public boolean isSettable(JFXDialog control) { + return control.transitionType == null || !control.transitionType.isBound(); + } + + @Override + public StyleableProperty getStyleableProperty(JFXDialog control) { + return control.transitionTypeProperty(); + } + }; + + private static final List> CHILD_STYLEABLES; + + static { + final List> styleables = + new ArrayList<>(StackPane.getClassCssMetaData()); + Collections.addAll(styleables, + DIALOG_TRANSITION + ); + CHILD_STYLEABLES = Collections.unmodifiableList(styleables); + } + } + + @Override + public List> getCssMetaData() { + return getClassCssMetaData(); + } + + public static List> getClassCssMetaData() { + return StyleableProperties.CHILD_STYLEABLES; + } + + + /*************************************************************************** + * * + * Custom Events * + * * + **************************************************************************/ + + private final ObjectProperty> onDialogClosedProperty = new ObjectPropertyBase>() { + @Override + protected void invalidated() { + setEventHandler(JFXDialogEvent.CLOSED, get()); + } + + @Override + public Object getBean() { + return JFXDialog.this; + } + + @Override + public String getName() { + return "onClosed"; + } + }; + + /** + * Defines a function to be called when the dialog is closed. + * Note: it will be triggered after the close animation is finished. + */ + public ObjectProperty> onDialogClosedProperty() { + return onDialogClosedProperty; + } + + public void setOnDialogClosed(EventHandler handler) { + onDialogClosedProperty().set(handler); + } + + public EventHandler getOnDialogClosed() { + return onDialogClosedProperty().get(); + } + + + private final ObjectProperty> onDialogOpenedProperty = new ObjectPropertyBase>() { + @Override + protected void invalidated() { + setEventHandler(JFXDialogEvent.OPENED, get()); + } + + @Override + public Object getBean() { + return JFXDialog.this; + } + + @Override + public String getName() { + return "onOpened"; + } + }; + + /** + * Defines a function to be called when the dialog is opened. + * Note: it will be triggered after the show animation is finished. + */ + public ObjectProperty> onDialogOpenedProperty() { + return onDialogOpenedProperty; + } + + public void setOnDialogOpened(EventHandler handler) { + onDialogOpenedProperty().set(handler); + } + + public EventHandler getOnDialogOpened() { + return onDialogOpenedProperty().get(); + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index d7def65c1..ff9c85977 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -49,6 +49,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.ui.account.AccountListPage; import org.jackhuang.hmcl.ui.animation.AnimationUtils; +import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.decorator.DecoratorController; @@ -328,16 +329,16 @@ public final class Controllers { if (AnimationUtils.playWindowAnimation()) { Timeline timeline = new Timeline( new KeyFrame(Duration.millis(0), - new KeyValue(decorator.getDecorator().opacityProperty(), 0, FXUtils.EASE), - new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, FXUtils.EASE), - new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, FXUtils.EASE), - new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, FXUtils.EASE) + new KeyValue(decorator.getDecorator().opacityProperty(), 0, Motion.EASE), + new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, Motion.EASE), + new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, Motion.EASE), + new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, Motion.EASE) ), new KeyFrame(Duration.millis(600), - new KeyValue(decorator.getDecorator().opacityProperty(), 1, FXUtils.EASE), - new KeyValue(decorator.getDecorator().scaleXProperty(), 1, FXUtils.EASE), - new KeyValue(decorator.getDecorator().scaleYProperty(), 1, FXUtils.EASE), - new KeyValue(decorator.getDecorator().scaleZProperty(), 1, FXUtils.EASE) + new KeyValue(decorator.getDecorator().opacityProperty(), 1, Motion.EASE), + new KeyValue(decorator.getDecorator().scaleXProperty(), 1, Motion.EASE), + new KeyValue(decorator.getDecorator().scaleYProperty(), 1, Motion.EASE), + new KeyValue(decorator.getDecorator().scaleZProperty(), 1, Motion.EASE) ) ); timeline.play(); 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 2b5f49982..765426c91 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/FXUtils.java @@ -437,28 +437,12 @@ public final class FXUtils { installSlowTooltip(node, new Tooltip(tooltip)); } - public static void playAnimation(Node node, String animationKey, Timeline timeline) { - animationKey = "FXUTILS.ANIMATION." + animationKey; - Object oldTimeline = node.getProperties().get(animationKey); -// if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop(); - if (timeline != null) timeline.play(); - node.getProperties().put(animationKey, timeline); - } - - public static Animation playAnimation(Node node, String animationKey, Duration duration, WritableValue property, T from, T to, Interpolator interpolator) { - if (from == null) from = property.getValue(); - if (duration == null || Objects.equals(duration, Duration.ZERO) || Objects.equals(from, to)) { - playAnimation(node, animationKey, null); - property.setValue(to); - return null; - } else { - Timeline timeline = new Timeline( - new KeyFrame(Duration.ZERO, new KeyValue(property, from, interpolator)), - new KeyFrame(duration, new KeyValue(property, to, interpolator)) - ); - playAnimation(node, animationKey, timeline); - return timeline; - } + public static void playAnimation(Node node, String animationKey, Animation animation) { + animationKey = "hmcl.animations." + animationKey; + if (node.getProperties().get(animationKey) instanceof Animation oldAnimation) + oldAnimation.stop(); + animation.play(); + node.getProperties().put(animationKey, animation); } public static void openFolder(Path file) { @@ -1349,8 +1333,6 @@ public final class FXUtils { } }; - public static final Interpolator EASE = Interpolator.SPLINE(0.25, 0.1, 0.25, 1); - public static void onEscPressed(Node node, Runnable action) { node.addEventHandler(KeyEvent.KEY_PRESSED, e -> { if (e.getCode() == KeyCode.ESCAPE) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationHandler.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationHandler.java deleted file mode 100644 index b3c46a105..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationHandler.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.ui.animation; - -import javafx.scene.Node; -import javafx.scene.layout.Pane; -import javafx.util.Duration; - -public interface AnimationHandler { - Duration getDuration(); - - Pane getCurrentRoot(); - - Node getPreviousNode(); - - Node getCurrentNode(); -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java deleted file mode 100644 index 10839c6bb..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/AnimationProducer.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.ui.animation; - -import javafx.animation.KeyFrame; -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public interface AnimationProducer { - void init(AnimationHandler handler); - - List animate(AnimationHandler handler); - - @Nullable AnimationProducer opposite(); -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java index 281ac618d..98ddbf7d5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/ContainerAnimations.java @@ -20,33 +20,23 @@ package org.jackhuang.hmcl.ui.animation; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; +import javafx.animation.Timeline; +import javafx.scene.Node; +import javafx.scene.layout.Pane; import javafx.util.Duration; -import org.jackhuang.hmcl.ui.FXUtils; -import org.jetbrains.annotations.Nullable; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -public enum ContainerAnimations implements AnimationProducer { +public enum ContainerAnimations implements TransitionPane.AnimationProducer { NONE { @Override - public void init(AnimationHandler c) { - c.getPreviousNode().setTranslateX(0); - c.getPreviousNode().setTranslateY(0); - c.getPreviousNode().setScaleX(1); - c.getPreviousNode().setScaleY(1); - c.getPreviousNode().setOpacity(1); - c.getCurrentNode().setTranslateX(0); - c.getCurrentNode().setTranslateY(0); - c.getCurrentNode().setScaleX(1); - c.getCurrentNode().setScaleY(1); - c.getCurrentNode().setOpacity(1); + public Timeline animate( + Pane container, Node previousNode, Node nextNode, + Duration duration, Interpolator interpolator) { + return new Timeline(); } @Override - public List animate(AnimationHandler c) { - return Collections.emptyList(); + public TransitionPane.AnimationProducer opposite() { + return this; } }, @@ -55,151 +45,48 @@ public enum ContainerAnimations implements AnimationProducer { */ FADE { @Override - public void init(AnimationHandler c) { - c.getPreviousNode().setTranslateX(0); - c.getPreviousNode().setTranslateY(0); - c.getPreviousNode().setScaleX(1); - c.getPreviousNode().setScaleY(1); - c.getPreviousNode().setOpacity(1); - c.getCurrentNode().setTranslateX(0); - c.getCurrentNode().setTranslateY(0); - c.getCurrentNode().setScaleX(1); - c.getCurrentNode().setScaleY(1); - c.getCurrentNode().setOpacity(0); + public Timeline animate( + Pane container, Node previousNode, Node nextNode, + Duration duration, Interpolator interpolator) { + return new Timeline(new KeyFrame(Duration.ZERO, + new KeyValue(previousNode.opacityProperty(), 1, interpolator), + new KeyValue(nextNode.opacityProperty(), 0, interpolator)), + new KeyFrame(duration, + new KeyValue(previousNode.opacityProperty(), 0, interpolator), + new KeyValue(nextNode.opacityProperty(), 1, interpolator))); } @Override - public List animate(AnimationHandler c) { - return Arrays.asList(new KeyFrame(Duration.ZERO, - new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH), - new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH)), - new KeyFrame(c.getDuration(), - new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH))); + public TransitionPane.AnimationProducer opposite() { + return this; } }, - /** - * A fade between the old and new view - */ - FADE_IN { - @Override - public void init(AnimationHandler c) { - c.getCurrentNode().setTranslateX(0); - c.getCurrentNode().setTranslateY(0); - c.getCurrentNode().setScaleX(1); - c.getCurrentNode().setScaleY(1); - c.getCurrentNode().setOpacity(0); - } - - @Override - public List animate(AnimationHandler c) { - return Arrays.asList(new KeyFrame(Duration.ZERO, - new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE)), - new KeyFrame(c.getDuration(), - new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE))); - } - }, - - /** - * A fade between the old and new view - */ - FADE_OUT { - @Override - public void init(AnimationHandler c) { - c.getCurrentNode().setTranslateX(0); - c.getCurrentNode().setTranslateY(0); - c.getCurrentNode().setScaleX(1); - c.getCurrentNode().setScaleY(1); - c.getCurrentNode().setOpacity(1); - } - - @Override - public List animate(AnimationHandler c) { - return Arrays.asList(new KeyFrame(Duration.ZERO, - new KeyValue(c.getCurrentNode().opacityProperty(), 1, FXUtils.SINE)), - new KeyFrame(c.getDuration(), - new KeyValue(c.getCurrentNode().opacityProperty(), 0, FXUtils.SINE))); - } - }, - /** - * A zoom effect - */ - ZOOM_IN { - @Override - public void init(AnimationHandler c) { - c.getPreviousNode().setTranslateX(0); - c.getPreviousNode().setTranslateY(0); - c.getPreviousNode().setScaleX(1); - c.getPreviousNode().setScaleY(1); - c.getPreviousNode().setOpacity(1); - c.getCurrentNode().setTranslateX(0); - c.getCurrentNode().setTranslateY(0); - } - - @Override - public List animate(AnimationHandler c) { - return Arrays.asList(new KeyFrame(Duration.ZERO, - new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)), - new KeyFrame(c.getDuration(), - new KeyValue(c.getPreviousNode().scaleXProperty(), 4, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().scaleYProperty(), 4, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))); - } - }, - /** - * A zoom effect - */ - ZOOM_OUT { - @Override - public void init(AnimationHandler c) { - c.getPreviousNode().setTranslateX(0); - c.getPreviousNode().setTranslateY(0); - c.getPreviousNode().setScaleX(1); - c.getPreviousNode().setScaleY(1); - c.getPreviousNode().setOpacity(1); - c.getCurrentNode().setTranslateX(0); - c.getCurrentNode().setTranslateY(0); - } - - @Override - public List animate(AnimationHandler c) { - return Arrays.asList(new KeyFrame(Duration.ZERO, - new KeyValue(c.getPreviousNode().scaleXProperty(), 1, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().scaleYProperty(), 1, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)), - new KeyFrame(c.getDuration(), - new KeyValue(c.getPreviousNode().scaleXProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().scaleYProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))); - } - }, /** * A swipe effect */ SWIPE_LEFT { @Override - public void init(AnimationHandler c) { - c.getPreviousNode().setScaleX(1); - c.getPreviousNode().setScaleY(1); - c.getPreviousNode().setOpacity(0); - c.getPreviousNode().setTranslateX(0); - c.getCurrentNode().setScaleX(1); - c.getCurrentNode().setScaleY(1); - c.getCurrentNode().setOpacity(1); - c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth()); + public void init(TransitionPane container, Node previousNode, Node nextNode) { + super.init(container, previousNode, nextNode); + nextNode.setTranslateX(container.getWidth()); } @Override - public List animate(AnimationHandler c) { - return Arrays.asList(new KeyFrame(Duration.ZERO, - new KeyValue(c.getCurrentNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)), - new KeyFrame(c.getDuration(), - new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))); + public Timeline animate( + Pane container, Node previousNode, Node nextNode, + Duration duration, Interpolator interpolator) { + return new Timeline(new KeyFrame(Duration.ZERO, + new KeyValue(nextNode.translateXProperty(), container.getWidth(), interpolator), + new KeyValue(previousNode.translateXProperty(), 0, interpolator)), + new KeyFrame(duration, + new KeyValue(nextNode.translateXProperty(), 0, interpolator), + new KeyValue(previousNode.translateXProperty(), -container.getWidth(), interpolator))); + } + + @Override + public TransitionPane.AnimationProducer opposite() { + return SWIPE_RIGHT; } }, @@ -208,105 +95,123 @@ public enum ContainerAnimations implements AnimationProducer { */ SWIPE_RIGHT { @Override - public void init(AnimationHandler c) { - c.getPreviousNode().setScaleX(1); - c.getPreviousNode().setScaleY(1); - c.getPreviousNode().setOpacity(0); - c.getPreviousNode().setTranslateX(0); - c.getCurrentNode().setScaleX(1); - c.getCurrentNode().setScaleY(1); - c.getCurrentNode().setOpacity(1); - c.getCurrentNode().setTranslateX(-c.getCurrentRoot().getWidth()); + public void init(TransitionPane container, Node previousNode, Node nextNode) { + super.init(container, previousNode, nextNode); + nextNode.setTranslateX(-container.getWidth()); } @Override - public List animate(AnimationHandler c) { - return Arrays.asList(new KeyFrame(Duration.ZERO, - new KeyValue(c.getCurrentNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)), - new KeyFrame(c.getDuration(), - new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))); + public Timeline animate( + Pane container, Node previousNode, Node nextNode, + Duration duration, Interpolator interpolator) { + return new Timeline(new KeyFrame(Duration.ZERO, + new KeyValue(nextNode.translateXProperty(), -container.getWidth(), interpolator), + new KeyValue(previousNode.translateXProperty(), 0, interpolator)), + new KeyFrame(duration, + new KeyValue(nextNode.translateXProperty(), 0, interpolator), + new KeyValue(previousNode.translateXProperty(), container.getWidth(), interpolator))); + } + + @Override + public TransitionPane.AnimationProducer opposite() { + return SWIPE_LEFT; } }, - SWIPE_LEFT_FADE_SHORT { + /// @see Transitions - Material Design 3 + FORWARD { @Override - public void init(AnimationHandler c) { - c.getPreviousNode().setScaleX(1); - c.getPreviousNode().setScaleY(1); - c.getPreviousNode().setOpacity(0); - c.getPreviousNode().setTranslateX(0); - c.getCurrentNode().setScaleX(1); - c.getCurrentNode().setScaleY(1); - c.getCurrentNode().setOpacity(1); - c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth()); + public Timeline animate( + Pane container, Node previousNode, Node nextNode, + Duration duration, Interpolator interpolator) { + double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50; + return new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(previousNode.translateXProperty(), 0, interpolator), + new KeyValue(previousNode.opacityProperty(), 1, interpolator), + new KeyValue(nextNode.opacityProperty(), 0, interpolator)), + new KeyFrame(duration.multiply(0.5), + new KeyValue(previousNode.translateXProperty(), -offset, interpolator), + new KeyValue(previousNode.opacityProperty(), 0, interpolator), + + new KeyValue(nextNode.opacityProperty(), 0, interpolator), + new KeyValue(nextNode.translateXProperty(), offset, interpolator)), + new KeyFrame(duration, + new KeyValue(nextNode.opacityProperty(), 1, interpolator), + new KeyValue(nextNode.translateXProperty(), 0, interpolator)) + ); } @Override - public List animate(AnimationHandler c) { - return Arrays.asList(new KeyFrame(Duration.ZERO, - new KeyValue(c.getCurrentNode().translateXProperty(), 50, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)), - new KeyFrame(c.getDuration(), - new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().translateXProperty(), -50, Interpolator.EASE_BOTH), - new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))); + public TransitionPane.AnimationProducer opposite() { + return BACKWARD; } }, - SWIPE_RIGHT_FADE_SHORT { + /// @see Transitions - Material Design 3 + BACKWARD { @Override - public void init(AnimationHandler c) { - c.getPreviousNode().setScaleX(1); - c.getPreviousNode().setScaleY(1); - c.getPreviousNode().setOpacity(0); - c.getPreviousNode().setTranslateX(0); - c.getCurrentNode().setScaleX(1); - c.getCurrentNode().setScaleY(1); - c.getCurrentNode().setOpacity(1); - c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth()); + public Timeline animate( + Pane container, Node previousNode, Node nextNode, + Duration duration, Interpolator interpolator) { + double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50; + return new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(previousNode.translateXProperty(), 0, interpolator), + new KeyValue(previousNode.opacityProperty(), 1, interpolator), + new KeyValue(nextNode.opacityProperty(), 0, interpolator)), + new KeyFrame(duration.multiply(0.5), + new KeyValue(previousNode.translateXProperty(), offset, interpolator), + new KeyValue(previousNode.opacityProperty(), 0, interpolator), + + new KeyValue(nextNode.opacityProperty(), 0, interpolator), + new KeyValue(nextNode.translateXProperty(), -offset, interpolator)), + new KeyFrame(duration, + new KeyValue(nextNode.opacityProperty(), 1, interpolator), + new KeyValue(nextNode.translateXProperty(), 0, interpolator)) + ); } @Override - public List animate(AnimationHandler c) { - return Arrays.asList(new KeyFrame(Duration.ZERO, - new KeyValue(c.getCurrentNode().translateXProperty(), -50, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getCurrentNode().opacityProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().opacityProperty(), 1, Interpolator.EASE_BOTH)), - new KeyFrame(c.getDuration(), - new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().translateXProperty(), 50, Interpolator.EASE_BOTH), - new KeyValue(c.getCurrentNode().opacityProperty(), 1, Interpolator.EASE_BOTH), - new KeyValue(c.getPreviousNode().opacityProperty(), 0, Interpolator.EASE_BOTH))); + public TransitionPane.AnimationProducer opposite() { + return FORWARD; } - }; + }, - private ContainerAnimations opposite; + /// Imitates the animation when switching tabs in the Windows 11 Settings interface + SLIDE_UP_FADE_IN { + @Override + public Timeline animate( + Pane container, Node previousNode, Node nextNode, + Duration duration, Interpolator interpolator) { + double offset = container.getHeight() > 0 ? container.getHeight() * 0.2 : 50; + return new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(previousNode.translateYProperty(), 0, interpolator), + new KeyValue(previousNode.opacityProperty(), 1, interpolator), + new KeyValue(nextNode.opacityProperty(), 0, interpolator), + new KeyValue(nextNode.translateYProperty(), offset, interpolator)), + new KeyFrame(duration.multiply(0.5), + new KeyValue(previousNode.opacityProperty(), 0, interpolator)), + new KeyFrame(duration, + new KeyValue(nextNode.opacityProperty(), 1, interpolator), + new KeyValue(nextNode.translateYProperty(), 0, interpolator)) + ); + } + }, + ; - static { - NONE.opposite = NONE; - FADE.opposite = FADE; - SWIPE_LEFT.opposite = SWIPE_RIGHT; - SWIPE_RIGHT.opposite = SWIPE_LEFT; - FADE_IN.opposite = FADE_OUT; - FADE_OUT.opposite = FADE_IN; - ZOOM_IN.opposite = ZOOM_OUT; - ZOOM_OUT.opposite = ZOOM_IN; + protected static void reset(Node node) { + node.setTranslateX(0); + node.setTranslateY(0); + node.setScaleX(1); + node.setScaleY(1); + node.setOpacity(1); } @Override - public abstract void init(AnimationHandler handler); - - @Override - public abstract List animate(AnimationHandler handler); - - @Override - public @Nullable ContainerAnimations opposite() { - return opposite; + public void init(TransitionPane container, Node previousNode, Node nextNode) { + reset(previousNode); + reset(nextNode); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/Motion.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/Motion.java new file mode 100644 index 000000000..732343bfc --- /dev/null +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/Motion.java @@ -0,0 +1,781 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2025 huangyuhui and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.jackhuang.hmcl.ui.animation; + +import javafx.animation.Interpolator; +import javafx.util.Duration; + +import java.util.Objects; + +/// @author Glavo +/// @see Flutter Curves +public final class Motion { + + //region Curves + + /// A linear animation curve. + /// + /// This is the identity map over the unit interval: its [Interpolator#curve(double)] + /// method returns its input unmodified. This is useful as a default curve for + /// cases where a [Interpolator] is required but no actual curve is desired. + /// + /// @see curve_linear.mp4 + public static final Interpolator LINEAR = Interpolator.LINEAR; + + /// The emphasizedAccelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + public static final Interpolator EMPHASIZED_ACCELERATE = new Cubic(0.3, 0.0, 0.8, 0.15); + + /// The emphasizedDecelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + public static final Interpolator EMPHASIZED_DECELERATE = new Cubic(0.05, 0.7, 0.1, 1.0); + + /// The standard easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + public static final Interpolator STANDARD = new Cubic(0.2, 0.0, 0.0, 1.0); + + /// The standardAccelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + public static final Interpolator STANDARD_ACCELERATE = new Cubic(0.3, 0.0, 1.0, 1.0); + + /// The standardDecelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + public static Interpolator STANDARD_DECELERATE = new Cubic(0.0, 0.0, 0.0, 1.0); + + /// The legacyDecelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + public static Interpolator LEGACY_DECELERATE = new Cubic(0.0, 0.0, 0.2, 1.0); + + /// The legacyAccelerate easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + public static Interpolator LEGACY_ACCELERATE = new Cubic(0.4, 0.0, 1.0, 1.0); + + /// The legacy easing curve in the Material specification. + /// + /// See also: + /// + /// * [M3 guidelines: Easing tokens](https://m3.material.io/styles/motion/easing-and-duration/tokens-specs#433b1153-2ea3-4fe2-9748-803a47bc97ee) + /// * [M3 guidelines: Applying easing and duration](https://m3.material.io/styles/motion/easing-and-duration/applying-easing-and-duration) + public static Interpolator LEGACY = new Cubic(0.4, 0.0, 0.2, 1.0); + + /// A cubic animation curve that speeds up quickly and ends slowly. + /// + /// This is the same as the CSS easing function `ease`. + /// + /// @see curve_ease.mp4 + public static final Interpolator EASE = new Cubic(0.25, 0.1, 0.25, 1.0); + + /// A cubic animation curve that starts slowly and ends quickly. + /// + /// This is the same as the CSS easing function `ease-in`. + /// + /// @see curve_ease_in.mp4 + public static final Interpolator EASE_IN = new Cubic(0.42, 0.0, 1.0, 1.0); + + /// A cubic animation curve that starts slowly and ends linearly. + /// + /// The symmetric animation to [#LINEAR_TO_EASE_OUT]. + /// + /// @see curve_ease_in_to_linear.mp4 + public static final Interpolator EASE_IN_TO_LINEAR = new Cubic(0.67, 0.03, 0.65, 0.09); + + /// A cubic animation curve that starts slowly and ends quickly. This is + /// similar to [#EASE_IN], but with sinusoidal easing for a slightly less + /// abrupt beginning and end. Nonetheless, the result is quite gentle and is + /// hard to distinguish from [#linear] at a glance. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_sine.mp4 + public static final Interpolator EASE_IN_SINE = new Cubic(0.47, 0.0, 0.745, 0.715); + + /// A cubic animation curve that starts slowly and ends quickly. Based on a + /// quadratic equation where `f(t) = t²`, this is effectively the inverse of + /// [#decelerate]. + /// + /// Compared to [#EASE_IN_SINE], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_quad.mp4 + public static final Interpolator EASE_IN_QUAD = new Cubic(0.55, 0.085, 0.68, 0.53); + + /// A cubic animation curve that starts slowly and ends quickly. This curve is + /// based on a cubic equation where `f(t) = t³`. The result is a safe sweet + /// spot when choosing a curve for widgets animating off the viewport. + /// + /// Compared to [#EASE_IN_QUAD], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_cubic.mp4 + public static final Interpolator EASE_IN_CUBIC = new Cubic(0.55, 0.055, 0.675, 0.19); + + /// A cubic animation curve that starts slowly and ends quickly. This curve is + /// based on a quartic equation where `f(t) = t⁴`. + /// + /// Animations using this curve or steeper curves will benefit from a longer + /// duration to avoid motion feeling unnatural. + /// + /// Compared to [#EASE_IN_CUBIC], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_quart.mp4 + public static final Interpolator EASE_IN_QUART = new Cubic(0.895, 0.03, 0.685, 0.22); + + /// A cubic animation curve that starts slowly and ends quickly. This curve is + /// based on a quintic equation where `f(t) = t⁵`. + /// + /// Compared to [#EASE_IN_QUART], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_quint.mp4 + public static final Interpolator EASE_IN_QUINT = new Cubic(0.755, 0.05, 0.855, 0.06); + + /// A cubic animation curve that starts slowly and ends quickly. This curve is + /// based on an exponential equation where `f(t) = 2¹⁰⁽ᵗ⁻¹⁾`. + /// + /// Using this curve can give your animations extra flare, but a longer + /// duration may need to be used to compensate for the steepness of the curve. + /// + /// Compared to [#EASE_IN_QUINT], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_expo.mp4 + public static final Interpolator EASE_IN_EXPO = new Cubic(0.95, 0.05, 0.795, 0.035); + + /// A cubic animation curve that starts slowly and ends quickly. This curve is + /// effectively the bottom-right quarter of a circle. + /// + /// Like [#EASE_IN_EXPO], this curve is fairly dramatic and will reduce + /// the clarity of an animation if not given a longer duration. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_circ.mp4 + public static final Interpolator EASE_IN_CIRC = new Cubic(0.6, 0.04, 0.98, 0.335); + + /// A cubic animation curve that starts slowly and ends quickly. This curve + /// is similar to [#elasticIn] in that it overshoots its bounds before + /// reaching its end. Instead of repeated swinging motions before ascending, + /// though, this curve overshoots once, then continues to ascend. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_back.mp4 + public static final Interpolator EASE_IN_BACK = new Cubic(0.6, -0.28, 0.735, 0.045); + + /// A cubic animation curve that starts quickly and ends slowly. + /// + /// This is the same as the CSS easing function `ease-out`. + /// + /// @see curve_ease_out.mp4 + public static final Interpolator EASE_OUT = new Cubic(0.0, 0.0, 0.58, 1.0); + + /// A cubic animation curve that starts linearly and ends slowly. + /// + /// A symmetric animation to [#EASE_IN_TO_LINEAR]. + /// + /// @see curve_linear_to_ease_out.mp4 + public static final Interpolator LINEAR_TO_EASE_OUT = new Cubic(0.35, 0.91, 0.33, 0.97); + + /// A cubic animation curve that starts quickly and ends slowly. This is + /// similar to [#EASE_OUT], but with sinusoidal easing for a slightly + /// less abrupt beginning and end. Nonetheless, the result is quite gentle and + /// is hard to distinguish from [#linear] at a glance. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_out_sine.mp4 + public static final Interpolator EASE_OUT_SINE = new Cubic(0.39, 0.575, 0.565, 1.0); + + /// A cubic animation curve that starts quickly and ends slowly. This is + /// effectively the same as [#decelerate], only simulated using a cubic + /// bezier function. + /// + /// Compared to [#EASE_OUT_SINE], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_out_quad.mp4 + public static final Interpolator EASE_OUT_QUAD = new Cubic(0.25, 0.46, 0.45, 0.94); + + /// A cubic animation curve that starts quickly and ends slowly. This curve is + /// a flipped version of [#EASE_IN_CUBIC]. + /// + /// The result is a safe sweet spot when choosing a curve for animating a + /// widget's position entering or already inside the viewport. + /// + /// Compared to [#EASE_OUT_QUAD], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_out_cubic.mp4 + public static final Interpolator EASE_OUT_CUBIC = new Cubic(0.215, 0.61, 0.355, 1.0); + + /// A cubic animation curve that starts quickly and ends slowly. This curve is + /// a flipped version of [#EASE_IN_QUART]. + /// + /// Animations using this curve or steeper curves will benefit from a longer + /// duration to avoid motion feeling unnatural. + /// + /// Compared to [#EASE_OUT_CUBIC], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_out_quart.mp4 + public static final Interpolator EASE_OUT_QUART = new Cubic(0.165, 0.84, 0.44, 1.0); + + /// A cubic animation curve that starts quickly and ends slowly. This curve is + /// a flipped version of [#EASE_IN_QUINT]. + /// + /// Compared to [#EASE_OUT_QUART], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_out_quint.mp4 + public static final Interpolator EASE_OUT_QUINT = new Cubic(0.23, 1.0, 0.32, 1.0); + + /// A cubic animation curve that starts quickly and ends slowly. This curve is + /// a flipped version of [#EASE_IN_EXPO]. Using this curve can give your + /// animations extra flare, but a longer duration may need to be used to + /// compensate for the steepness of the curve. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_out_expo.mp4 + public static final Interpolator EASE_OUT_EXPO = new Cubic(0.19, 1.0, 0.22, 1.0); + + /// A cubic animation curve that starts quickly and ends slowly. This curve is + /// effectively the top-left quarter of a circle. + /// + /// Like [#EASE_OUT_EXPO], this curve is fairly dramatic and will reduce + /// the clarity of an animation if not given a longer duration. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_out_circ.mp4 + public static final Interpolator EASE_OUT_CIRC = new Cubic(0.075, 0.82, 0.165, 1.0); + + /// A cubic animation curve that starts quickly and ends slowly. This curve is + /// similar to [#elasticOut] in that it overshoots its bounds before + /// reaching its end. Instead of repeated swinging motions after ascending, + /// though, this curve only overshoots once. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_out_back.mp4 + public static final Interpolator EASE_OUT_BACK = new Cubic(0.175, 0.885, 0.32, 1.275); + + /// A cubic animation curve that starts slowly, speeds up, and then ends + /// slowly. + /// + /// This is the same as the CSS easing function `ease-in-out`. + /// + /// @see curve_ease_in_out.mp4 + public static final Interpolator EASE_IN_OUT = new Cubic(0.42, 0.0, 0.58, 1.0); + + /// A cubic animation curve that starts slowly, speeds up, and then ends + /// slowly. This is similar to [#EASE_IN_OUT], but with sinusoidal easing + /// for a slightly less abrupt beginning and end. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_out_sine.mp4 + public static final Interpolator EASE_IN_OUT_SINE = new Cubic(0.445, 0.05, 0.55, 0.95); + + /// A cubic animation curve that starts slowly, speeds up, and then ends + /// slowly. This curve can be imagined as [#EASE_IN_QUAD] as the first + /// half, and [#EASE_OUT_QUAD] as the second. + /// + /// Compared to [#EASE_IN_OUT_SINE], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_out_quad.mp4 + public static final Interpolator EASE_IN_OUT_QUAD = new Cubic(0.455, 0.03, 0.515, 0.955); + + /// A cubic animation curve that starts slowly, speeds up, and then ends + /// slowly. This curve can be imagined as [#EASE_IN_CUBIC] as the first + /// half, and [#EASE_OUT_CUBIC] as the second. + /// + /// The result is a safe sweet spot when choosing a curve for a widget whose + /// initial and final positions are both within the viewport. + /// + /// Compared to [#EASE_IN_OUT_QUAD], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_out_cubic.mp4 + public static final Interpolator EASE_IN_OUT_CUBIC = new Cubic(0.645, 0.045, 0.355, 1.0); + + /// A cubic animation curve that starts slowly, speeds up, and then ends + /// slowly. This curve can be imagined as [#EASE_IN_QUART] as the first + /// half, and [#EASE_OUT_QUART] as the second. + /// + /// Animations using this curve or steeper curves will benefit from a longer + /// duration to avoid motion feeling unnatural. + /// + /// Compared to [#EASE_IN_OUT_CUBIC], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_out_quart.mp4 + public static final Interpolator EASE_IN_OUT_QUART = new Cubic(0.77, 0.0, 0.175, 1.0); + + /// A cubic animation curve that starts slowly, speeds up, and then ends + /// slowly. This curve can be imagined as [#EASE_IN_QUINT] as the first + /// half, and [#EASE_OUT_QUINT] as the second. + /// + /// Compared to [#EASE_IN_OUT_QUART], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_out_quint.mp4 + public static final Interpolator EASE_IN_OUT_QUINT = new Cubic(0.86, 0.0, 0.07, 1.0); + + /// A cubic animation curve that starts slowly, speeds up, and then ends + /// slowly. + /// + /// Since this curve is arrived at with an exponential function, the midpoint + /// is exceptionally steep. Extra consideration should be taken when designing + /// an animation using this. + /// + /// Compared to [#EASE_IN_OUT_QUINT], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_out_expo.mp4 + public static final Interpolator EASE_IN_OUT_EXPO = new Cubic(1.0, 0.0, 0.0, 1.0); + + /// A cubic animation curve that starts slowly, speeds up, and then ends + /// slowly. This curve can be imagined as [#EASE_IN_CIRC] as the first + /// half, and [#EASE_OUT_CIRC] as the second. + /// + /// Like [#EASE_IN_OUT_EXPO], this curve is fairly dramatic and will reduce + /// the clarity of an animation if not given a longer duration. + /// + /// Compared to [#EASE_IN_OUT_EXPO], this curve is slightly steeper. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_out_circ.mp4 + public static final Interpolator EASE_IN_OUT_CIRC = new Cubic(0.785, 0.135, 0.15, 0.86); + + /// A cubic animation curve that starts slowly, speeds up shortly thereafter, + /// and then ends slowly. This curve can be imagined as a steeper version of + /// [#EASE_IN_OUT_CUBIC]. + /// + /// The result is a more emphasized eased curve when choosing a curve for a + /// widget whose initial and final positions are both within the viewport. + /// + /// Compared to [#EASE_IN_OUT_CUBIC], this curve is slightly steeper. + /// + /// @see curve_ease_in_out_cubic_emphasized.mp4 + public static final Interpolator EASE_IN_OUT_CUBIC_EMPHASIZED = new ThreePointCubic( + new Offset(0.05, 0), + new Offset(0.133333, 0.06), + new Offset(0.166666, 0.4), + new Offset(0.208333, 0.82), + new Offset(0.25, 1) + ); + + /// A cubic animation curve that starts slowly, speeds up, and then ends + /// slowly. This curve can be imagined as [#EASE_IN_BACK] as the first + /// half, and [#EASE_OUT_BACK] as the second. + /// + /// Since two curves are used as a basis for this curve, the resulting + /// animation will overshoot its bounds twice before reaching its end - first + /// by exceeding its lower bound, then exceeding its upper bound and finally + /// descending to its final position. + /// + /// Derived from Robert Penner’s easing functions. + /// + /// @see curve_ease_in_out_back.mp4 + public static final Interpolator EASE_IN_OUT_BACK = new Cubic(0.68, -0.55, 0.265, 1.55); + + /// A curve that starts quickly and eases into its final position. + /// + /// Over the course of the animation, the object spends more time near its + /// final destination. As a result, the user isn’t left waiting for the + /// animation to finish, and the negative effects of motion are minimized. + /// + /// @see curve_fast_out_slow_in.mp4 + public static final Interpolator FAST_OUT_SLOW_IN = new Cubic(0.4, 0.0, 0.2, 1.0); + + /// A cubic animation curve that starts quickly, slows down, and then ends + /// quickly. + /// + /// @see curve_slow_middle.mp4 + public static final Interpolator SLOW_MIDDLE = new Cubic(0.15, 0.85, 0.85, 0.15); + + private static double bounce(double t) { + if (t < 1.0 / 2.75) { + return 7.5625 * t * t; + } else if (t < 2 / 2.75) { + t -= 1.5 / 2.75; + return 7.5625 * t * t + 0.75; + } else if (t < 2.5 / 2.75) { + t -= 2.25 / 2.75; + return 7.5625 * t * t + 0.9375; + } + t -= 2.625 / 2.75; + return 7.5625 * t * t + 0.984375; + } + + /// An oscillating curve that grows in magnitude. + /// + /// @see curve_bounce_in.mp4 + + public static final Interpolator BOUNCE_IN = new Interpolator() { + @Override + protected double curve(double t) { + return 1.0 - bounce(1.0 - t); + } + }; + + /// An oscillating curve that first grows and then shrink in magnitude. + /// + /// @see curve_bounce_out.mp4 + public static final Interpolator BOUNCE_OUT = new Interpolator() { + @Override + protected double curve(double t) { + return bounce(t); + } + }; + + /// An oscillating curve that first grows and then shrink in magnitude. + /// + /// @see curve_bounce_in_out.mp4 + public static final Interpolator BOUNCE_IN_OUT = new Interpolator() { + @Override + protected double curve(double t) { + if (t < 0.5) { + return (1.0 - bounce(1.0 - t * 2.0)) * 0.5; + } else { + return bounce(t * 2.0 - 1.0) * 0.5 + 0.5; + } + } + }; + + private static final double PERIOD = 0.4; + + /// An oscillating curve that grows in magnitude while overshooting its bounds. + /// + /// @see curve_elastic_in.mp4 + public static final Interpolator ELASTIC_IN = new Interpolator() { + @Override + protected double curve(double t) { + final double s = PERIOD / 4.0; + t = t - 1.0; + return -Math.pow(2.0, 10.0 * t) * Math.sin((t - s) * (Math.PI * 2.0) / PERIOD); + } + }; + + /// An oscillating curve that shrinks in magnitude while overshooting its bounds. + /// + /// @see curve_elastic_out.mp4 + public static Interpolator ELASTIC_OUT = new Interpolator() { + @Override + protected double curve(double t) { + final double s = PERIOD / 4.0; + return Math.pow(2.0, -10 * t) * Math.sin((t - s) * (Math.PI * 2.0) / PERIOD) + 1.0; + } + }; + + /// An oscillating curve that grows and then shrinks in magnitude while overshooting its bounds. + /// + /// @see curve_elastic_in_out.mp4 + public static Interpolator ELASTIC_IN_OUT = new Interpolator() { + @Override + @SuppressWarnings("DuplicateExpressions") + protected double curve(double t) { + final double s = PERIOD / 4.0; + t = 2.0 * t - 1.0; + if (t < 0.0) { + return -0.5 * Math.pow(2.0, 10.0 * t) * Math.sin((t - s) * (Math.PI * 2.0) / PERIOD); + } else { + return Math.pow(2.0, -10.0 * t) * Math.sin((t - s) * (Math.PI * 2.0) / PERIOD) * 0.5 + 1.0; + } + } + }; + + /// A cubic polynomial mapping of the unit interval. + private static final class Cubic extends Interpolator { + private static final double CUBIC_ERROR_BOUND = 0.001; + + /// The x coordinate of the first control point. + /// + /// The line through the point (0, 0) and the first control point is tangent + /// to the curve at the point (0, 0). + private final double a; + + /// The y coordinate of the first control point. + /// + /// The line through the point (0, 0) and the first control point is tangent + /// to the curve at the point (0, 0). + private final double b; + + /// The x coordinate of the second control point. + /// + /// The line through the point (1, 1) and the second control point is tangent + /// to the curve at the point (1, 1). + private final double c; + + /// The y coordinate of the second control point. + /// + /// The line through the point (1, 1) and the second control point is tangent + /// to the curve at the point (1, 1). + private final double d; + + private Cubic(double a, double b, double c, double d) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + } + + double _evaluateCubic(double a, double b, double m) { + return 3 * a * (1 - m) * (1 - m) * m + 3 * b * (1 - m) * m * m + m * m * m; + } + + @Override + protected double curve(double t) { + double start = 0.0; + double end = 1.0; + while (true) { + final double midpoint = (start + end) / 2; + final double estimate = _evaluateCubic(a, c, midpoint); + if (Math.abs(t - estimate) < CUBIC_ERROR_BOUND) { + return _evaluateCubic(b, d, midpoint); + } + if (estimate < t) { + start = midpoint; + } else { + end = midpoint; + } + } + } + + @Override + public boolean equals(Object o) { + return o instanceof Cubic cubic + && this.a == cubic.a + && this.b == cubic.b + && this.c == cubic.c + && this.d == cubic.d; + } + + @Override + public int hashCode() { + return Objects.hash(a, b, c, d); + } + + @Override + public String toString() { + return "Cubic[a=%s, b=%s, c=%s, d=%s]".formatted(a, b, c, d); + } + } + + private record Offset(double dx, double dy) { + } + + private static final class ThreePointCubic extends Interpolator { + + /// The coordinates of the first control point of the first curve. + /// + /// The line through the point (0, 0) and this control point is tangent to the + /// curve at the point (0, 0). + private final Offset a1; + + /// The coordinates of the second control point of the first curve. + /// + /// The line through the [#midpoint] and this control point is tangent to the + /// curve approaching the [#midpoint]. + private final Offset b1; + + /// The coordinates of the middle shared point. + /// + /// The curve will go through this point. If the control points surrounding + /// this middle point ([#b1], and [#a2]) are not colinear with this point, then + /// the curve's derivative will have a discontinuity (a cusp) at this point. + private final Offset midpoint; + + /// The coordinates of the first control point of the second curve. + /// + /// The line through the [#midpoint] and this control point is tangent to the + /// curve approaching the [#midpoint]. + private final Offset a2; + + /// The coordinates of the second control point of the second curve. + /// + /// The line through the point (1, 1) and this control point is tangent to the + /// curve at (1, 1). + private final Offset b2; + + /// Creates two cubic curves that share a common control point. + /// + /// Rather than creating a new instance, consider using one of the common + /// three-point cubic curves in [Interpolator]. + /// + /// The arguments correspond to the control points for the two curves, + /// including the [#midpoint], but do not include the two implied end points at + /// (0,0) and (1,1), which are fixed. + private ThreePointCubic(Offset a1, Offset b1, Offset midpoint, Offset a2, Offset b2) { + this.a1 = a1; + this.b1 = b1; + this.midpoint = midpoint; + this.a2 = a2; + this.b2 = b2; + } + + @Override + protected double curve(double t) { + final boolean firstCurve = t < midpoint.dx; + final double scaleX = firstCurve ? midpoint.dx : 1.0 - midpoint.dx; + final double scaleY = firstCurve ? midpoint.dy : 1.0 - midpoint.dy; + final double scaledT = (t - (firstCurve ? 0.0 : midpoint.dx)) / scaleX; + if (firstCurve) { + return new Cubic( + a1.dx / scaleX, + a1.dy / scaleY, + b1.dx / scaleX, + b1.dy / scaleY + ).curve(scaledT) * + scaleY; + } else { + return new Cubic( + (a2.dx - midpoint.dx) / scaleX, + (a2.dy - midpoint.dy) / scaleY, + (b2.dx - midpoint.dx) / scaleX, + (b2.dy - midpoint.dy) / scaleY + ).curve(scaledT) * + scaleY + + midpoint.dy; + } + } + + @Override + public boolean equals(Object o) { + return o instanceof ThreePointCubic that + && a1.equals(that.a1) + && b1.equals(that.b1) + && midpoint.equals(that.midpoint) + && a2.equals(that.a2) + && b2.equals(that.b2); + } + + @Override + public int hashCode() { + return Objects.hash(a1, b1, midpoint, a2, b2); + } + + @Override + public String toString() { + return "ThreePointCubic[a1=%s, b1=%s, midpoint=%s, a2=%s, b2=%s]".formatted(a1, b1, midpoint, a2, b2); + } + } + + //endregion Curves + + // region Durations + + /// The short1 duration (50ms) in the Material specification. + public static final Duration SHORT1 = Duration.millis(50); + + /// The short2 duration (100ms) in the Material specification. + public static final Duration SHORT2 = Duration.millis(100); + + /// The short3 duration (150ms) in the Material specification. + public static final Duration SHORT3 = Duration.millis(150); + + /// The short4 duration (200ms) in the Material specification. + public static final Duration SHORT4 = Duration.millis(200); + + /// The medium1 duration (250ms) in the Material specification. + public static final Duration MEDIUM1 = Duration.millis(250); + + /// The medium2 duration (300ms) in the Material specification. + public static final Duration MEDIUM2 = Duration.millis(300); + + /// The medium3 duration (350ms) in the Material specification. + public static final Duration MEDIUM3 = Duration.millis(350); + + /// The medium4 duration (400ms) in the Material specification. + public static final Duration MEDIUM4 = Duration.millis(400); + + /// The long1 duration (450ms) in the Material specification. + public static final Duration LONG1 = Duration.millis(450); + + /// The long2 duration (500ms) in the Material specification. + public static final Duration LONG2 = Duration.millis(500); + + /// The long3 duration (550ms) in the Material specification. + public static final Duration LONG3 = Duration.millis(550); + + /// The long4 duration (600ms) in the Material specification. + public static final Duration LONG4 = Duration.millis(600); + + /// The extralong1 duration (700ms) in the Material specification. + public static final Duration EXTRA_LONG1 = Duration.millis(700); + + /// The extralong2 duration (800ms) in the Material specification. + public static final Duration EXTRA_LONG2 = Duration.millis(800); + + /// The extralong3 duration (900ms) in the Material specification. + public static final Duration EXTRA_LONG3 = Duration.millis(900); + + /// The extralong4 duration (1000ms) in the Material specification. + public static final Duration EXTRA_LONG4 = Duration.millis(1000); + + // endregion Durations + + private Motion() { + } +} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java index f3ebe322f..795f0b9ba 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/animation/TransitionPane.java @@ -17,77 +17,51 @@ */ package org.jackhuang.hmcl.ui.animation; -import javafx.animation.Timeline; +import javafx.animation.Animation; +import javafx.animation.Interpolator; import javafx.application.Platform; import javafx.scene.Node; +import javafx.scene.layout.Pane; import javafx.scene.layout.StackPane; import javafx.util.Duration; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.ui.construct.TabHeader; +import org.jetbrains.annotations.Nullable; -public class TransitionPane extends StackPane implements AnimationHandler { - private static final Duration DEFAULT_DURATION = Duration.millis(200); +public class TransitionPane extends StackPane { - private Duration duration; - private Node previousNode, currentNode; + private Node currentNode; public TransitionPane() { FXUtils.setOverflowHidden(this); } - @Override - public Node getPreviousNode() { - return previousNode; - } - - @Override public Node getCurrentNode() { return currentNode; } - @Override - public StackPane getCurrentRoot() { - return this; + public void bindTabHeader(TabHeader tabHeader) { + this.setContent(tabHeader.getSelectionModel().getSelectedItem().getNode(), ContainerAnimations.NONE); + FXUtils.onChange(tabHeader.getSelectionModel().selectedItemProperty(), newValue -> { + this.setContent(newValue.getNode(), + ContainerAnimations.SLIDE_UP_FADE_IN, + Motion.MEDIUM4, + Motion.EASE_IN_OUT_CUBIC_EMPHASIZED + ); + }); } - @Override - public Duration getDuration() { - return duration; + public final void setContent(Node newView, AnimationProducer transition) { + setContent(newView, transition, Motion.SHORT4); } - public void setContent(Node newView, AnimationProducer transition) { - setContent(newView, transition, DEFAULT_DURATION); + public final void setContent(Node newView, AnimationProducer transition, Duration duration) { + setContent(newView, transition, duration, Motion.EASE); } - public void setContent(Node newView, AnimationProducer transition, Duration duration) { - this.duration = duration; - - updateContent(newView); - - if (previousNode == EMPTY_PANE) { - getChildren().setAll(newView); - return; - } - - if (AnimationUtils.isAnimationEnabled() && transition != ContainerAnimations.NONE) { - setMouseTransparent(true); - transition.init(this); - - // runLater or "init" will not work - Platform.runLater(() -> { - Timeline newAnimation = new Timeline(); - newAnimation.getKeyFrames().setAll(transition.animate(this)); - newAnimation.setOnFinished(e -> { - setMouseTransparent(false); - getChildren().remove(previousNode); - }); - FXUtils.playAnimation(this, "transition_pane", newAnimation); - }); - } else { - getChildren().remove(previousNode); - } - } - - private void updateContent(Node newView) { + public void setContent(Node newView, AnimationProducer transition, + Duration duration, Interpolator interpolator) { + Node previousNode; if (getWidth() > 0 && getHeight() > 0) { previousNode = currentNode; if (previousNode == null) { @@ -105,10 +79,49 @@ public class TransitionPane extends StackPane implements AnimationHandler { currentNode = newView; getChildren().setAll(previousNode, currentNode); + + if (previousNode == EMPTY_PANE) { + getChildren().setAll(newView); + return; + } + + if (AnimationUtils.isAnimationEnabled() && transition != ContainerAnimations.NONE) { + setMouseTransparent(true); + transition.init(this, previousNode, getCurrentNode()); + + Node finalPreviousNode = previousNode; + // runLater or "init" will not work + Platform.runLater(() -> { + Animation newAnimation = transition.animate( + this, + finalPreviousNode, + getCurrentNode(), + duration, interpolator); + newAnimation.setOnFinished(e -> { + setMouseTransparent(false); + getChildren().remove(finalPreviousNode); + }); + FXUtils.playAnimation(this, "transition_pane", newAnimation); + }); + } else { + getChildren().remove(previousNode); + } } private final EmptyPane EMPTY_PANE = new EmptyPane(); public static class EmptyPane extends StackPane { } + + public interface AnimationProducer { + default void init(TransitionPane container, Node previousNode, Node nextNode) { + } + + Animation animate(Pane container, Node previousNode, Node nextNode, + Duration duration, Interpolator interpolator); + + default @Nullable TransitionPane.AnimationProducer opposite() { + return null; + } + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java index 4ec498992..8d639cb56 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/Navigator.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.ui.construct; +import javafx.animation.Interpolator; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -26,12 +27,14 @@ import javafx.event.EventHandler; import javafx.event.EventType; import javafx.scene.Node; import javafx.scene.layout.Region; +import javafx.util.Duration; import org.jackhuang.hmcl.ui.FXUtils; -import org.jackhuang.hmcl.ui.animation.AnimationProducer; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.wizard.Navigation; +import java.util.Objects; import java.util.Optional; import java.util.Stack; @@ -56,6 +59,10 @@ public class Navigator extends TransitionPane { } public void navigate(Node node, AnimationProducer animationProducer) { + navigate(node, animationProducer, Motion.SHORT4, Motion.EASE); + } + + public void navigate(Node node, AnimationProducer animationProducer, Duration duration, Interpolator interpolator) { FXUtils.checkFxUserThread(); if (!initialized) @@ -75,7 +82,7 @@ public class Navigator extends TransitionPane { node.fireEvent(navigating); node.getProperties().put("hmcl.navigator.animation", animationProducer); - setContent(node, animationProducer); + setContent(node, animationProducer, duration, interpolator); NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATED); node.fireEvent(navigated); @@ -113,7 +120,7 @@ public class Navigator extends TransitionPane { Node poppedNode = stack.pop(); NavigationEvent exited = new NavigationEvent(this, poppedNode, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.EXITED); poppedNode.fireEvent(exited); - if (poppedNode instanceof PageAware) ((PageAware) poppedNode).onPageHidden(); + if (poppedNode instanceof PageAware pageAware) pageAware.onPageHidden(); backable.set(canGoBack()); Node node = stack.peek(); @@ -123,8 +130,8 @@ public class Navigator extends TransitionPane { node.fireEvent(navigating); Object obj = from.getProperties().get("hmcl.navigator.animation"); - if (obj instanceof AnimationProducer) { - setContent(node, (AnimationProducer) obj); + if (obj instanceof AnimationProducer animationProducer) { + setContent(node, Objects.requireNonNullElse(animationProducer.opposite(), animationProducer)); } else { setContent(node, ContainerAnimations.NONE); } @@ -160,12 +167,13 @@ public class Navigator extends TransitionPane { return stack.size(); } - public void setContent(Node content, AnimationProducer animationProducer) { - super.setContent(content, animationProducer); + @Override + public void setContent(Node newView, AnimationProducer transition, Duration duration, Interpolator interpolator) { + super.setContent(newView, transition, duration, interpolator); - if (content instanceof Region) { - ((Region) content).setMinSize(0, 0); - FXUtils.setOverflowHidden((Region) content); + if (newView instanceof Region region) { + region.setMinSize(0, 0); + FXUtils.setOverflowHidden(region); } } @@ -181,7 +189,7 @@ public class Navigator extends TransitionPane { this.onNavigated.set(onNavigated); } - private ObjectProperty> onNavigated = new SimpleObjectProperty>(this, "onNavigated") { + private final ObjectProperty> onNavigated = new SimpleObjectProperty>(this, "onNavigated") { @Override protected void invalidated() { setEventHandler(NavigationEvent.NAVIGATED, get()); @@ -200,14 +208,14 @@ public class Navigator extends TransitionPane { this.onNavigating.set(onNavigating); } - private ObjectProperty> onNavigating = new SimpleObjectProperty>(this, "onNavigating") { + private final ObjectProperty> onNavigating = new SimpleObjectProperty>(this, "onNavigating") { @Override protected void invalidated() { setEventHandler(NavigationEvent.NAVIGATING, get()); } }; - public static class NavigationEvent extends Event { + public static final class NavigationEvent extends Event { public static final EventType EXITED = new EventType<>("EXITED"); public static final EventType NAVIGATED = new EventType<>("NAVIGATED"); public static final EventType NAVIGATING = new EventType<>("NAVIGATING"); 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 940fea3ac..fb93b0801 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 @@ -18,7 +18,6 @@ package org.jackhuang.hmcl.ui.construct; import com.jfoenix.controls.JFXRippler; -import javafx.animation.Interpolator; import javafx.animation.Transition; import javafx.beans.DefaultProperty; import javafx.beans.InvalidationListener; @@ -40,6 +39,7 @@ import javafx.scene.paint.Paint; import javafx.scene.shape.Rectangle; import javafx.util.Duration; import org.jackhuang.hmcl.ui.animation.AnimationUtils; +import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.util.Lang; import java.util.List; @@ -109,7 +109,7 @@ public class RipplerContainer extends StackPane { setOnMouseEntered(e -> new Transition() { { setCycleDuration(DURATION); - setInterpolator(Interpolator.EASE_IN); + setInterpolator(Motion.EASE_IN); } @Override @@ -121,7 +121,7 @@ public class RipplerContainer extends StackPane { setOnMouseExited(e -> new Transition() { { setCycleDuration(DURATION); - setInterpolator(Interpolator.EASE_OUT); + setInterpolator(Motion.EASE_OUT); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java index 365d787fc..c83708848 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/TabHeader.java @@ -48,8 +48,8 @@ public class TabHeader extends Control implements TabControl, PageAware { } } - private ObservableList> tabs = FXCollections.observableArrayList(); - private ObjectProperty side = new SimpleObjectProperty<>(Side.TOP); + private final ObservableList> tabs = FXCollections.observableArrayList(); + private final ObjectProperty side = new SimpleObjectProperty<>(Side.TOP); @Override public ObservableList> getTabs() { @@ -170,14 +170,14 @@ public class TabHeader extends Control implements TabControl, PageAware { this.selectedTab = control.getSelectionModel().getSelectedItem(); } - protected class HeaderContainer extends StackPane { + protected final class HeaderContainer extends StackPane { private Timeline timeline; - private StackPane selectedTabLine; - private HeadersRegion headersRegion; - private Scale scale = new Scale(1, 1, 0, 0); - private Rotate rotate = new Rotate(0, 0, 1); - private double selectedTabLineOffset; - private ObservableList binding; + private final StackPane selectedTabLine; + private final HeadersRegion headersRegion; + private final Scale scale = new Scale(1, 1, 0, 0); + private final Rotate rotate = new Rotate(0, 0, 1); + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final ObservableList binding; public HeaderContainer() { getStyleClass().add("tab-header-area"); @@ -232,13 +232,12 @@ public class TabHeader extends Control implements TabControl, PageAware { protected void invalidated() { super.invalidated(); - switch (get()) { - case TOP: action = new Top(); break; - case BOTTOM: action = new Bottom(); break; - case LEFT: action = new Left(); break; - case RIGHT: action = new Right(); break; - default: throw new InternalError(); - } + action = switch (get()) { + case TOP -> new Top(); + case BOTTOM -> new Bottom(); + case LEFT -> new Left(); + case RIGHT -> new Right(); + }; } }; @@ -269,7 +268,7 @@ public class TabHeader extends Control implements TabControl, PageAware { action.layoutChildren(); } - protected void animateSelectionLine() { + private void animateSelectionLine() { action.animateSelectionLine(); } @@ -325,7 +324,6 @@ public class TabHeader extends Control implements TabControl, PageAware { double oldWidth = lineWidth * oldScaleX; double oldTransX = selectedTabLine.getTranslateX(); double newScaleX = newWidth * oldScaleX / oldWidth; - selectedTabLineOffset = newTransX; // newTransX += offsetStart * (double)this.direction; double transDiff = newTransX - oldTransX; if (transDiff < 0.0D) { @@ -465,7 +463,6 @@ public class TabHeader extends Control implements TabControl, PageAware { double oldHeight = lineHeight * oldScaleY; double oldTransY = selectedTabLine.getTranslateY(); double newScaleY = newHeight * oldScaleY / oldHeight; - selectedTabLineOffset = newTransY; // newTransY += offsetStart * (double)this.direction; double transDiff = newTransY - oldTransY; if (transDiff < 0.0D) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java index f0d5b1d64..5831aeaf5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/Decorator.java @@ -41,6 +41,7 @@ import javafx.stage.StageStyle; import javafx.util.Duration; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.animation.AnimationUtils; +import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -82,19 +83,19 @@ public class Decorator extends Control { if (playRestoreMinimizeAnimation && !iconified) { playRestoreMinimizeAnimation = false; Timeline timeline = new Timeline( - new KeyFrame(Duration.millis(0), - new KeyValue(this.opacityProperty(), 0, FXUtils.EASE), - new KeyValue(this.translateYProperty(), 200, FXUtils.EASE), - new KeyValue(this.scaleXProperty(), 0.4, FXUtils.EASE), - new KeyValue(this.scaleYProperty(), 0.4, FXUtils.EASE), - new KeyValue(this.scaleZProperty(), 0.4, FXUtils.EASE) + new KeyFrame(Duration.ZERO, + new KeyValue(this.opacityProperty(), 0, Motion.EASE), + new KeyValue(this.translateYProperty(), 200, Motion.EASE), + new KeyValue(this.scaleXProperty(), 0.4, Motion.EASE), + new KeyValue(this.scaleYProperty(), 0.4, Motion.EASE), + new KeyValue(this.scaleZProperty(), 0.4, Motion.EASE) ), - new KeyFrame(Duration.millis(200), - new KeyValue(this.opacityProperty(), 1, FXUtils.EASE), - new KeyValue(this.translateYProperty(), 0, FXUtils.EASE), - new KeyValue(this.scaleXProperty(), 1, FXUtils.EASE), - new KeyValue(this.scaleYProperty(), 1, FXUtils.EASE), - new KeyValue(this.scaleZProperty(), 1, FXUtils.EASE) + new KeyFrame(Motion.SHORT4, + new KeyValue(this.opacityProperty(), 1, Motion.EASE), + new KeyValue(this.translateYProperty(), 0, Motion.EASE), + new KeyValue(this.scaleXProperty(), 1, Motion.EASE), + new KeyValue(this.scaleYProperty(), 1, Motion.EASE), + new KeyValue(this.scaleZProperty(), 1, Motion.EASE) ) ); timeline.play(); @@ -277,19 +278,19 @@ public class Decorator extends Control { if (AnimationUtils.playWindowAnimation() && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) { playRestoreMinimizeAnimation = true; Timeline timeline = new Timeline( - new KeyFrame(Duration.millis(0), - new KeyValue(this.opacityProperty(), 1, FXUtils.EASE), - new KeyValue(this.translateYProperty(), 0, FXUtils.EASE), - new KeyValue(this.scaleXProperty(), 1, FXUtils.EASE), - new KeyValue(this.scaleYProperty(), 1, FXUtils.EASE), - new KeyValue(this.scaleZProperty(), 1, FXUtils.EASE) + new KeyFrame(Duration.ZERO, + new KeyValue(this.opacityProperty(), 1, Motion.EASE), + new KeyValue(this.translateYProperty(), 0, Motion.EASE), + new KeyValue(this.scaleXProperty(), 1, Motion.EASE), + new KeyValue(this.scaleYProperty(), 1, Motion.EASE), + new KeyValue(this.scaleZProperty(), 1, Motion.EASE) ), - new KeyFrame(Duration.millis(200), - new KeyValue(this.opacityProperty(), 0, FXUtils.EASE), - new KeyValue(this.translateYProperty(), 200, FXUtils.EASE), - new KeyValue(this.scaleXProperty(), 0.4, FXUtils.EASE), - new KeyValue(this.scaleYProperty(), 0.4, FXUtils.EASE), - new KeyValue(this.scaleZProperty(), 0.4, FXUtils.EASE) + new KeyFrame(Motion.SHORT4, + new KeyValue(this.opacityProperty(), 0, Motion.EASE), + new KeyValue(this.translateYProperty(), 200, Motion.EASE), + new KeyValue(this.scaleXProperty(), 0.4, Motion.EASE), + new KeyValue(this.scaleYProperty(), 0.4, Motion.EASE), + new KeyValue(this.scaleZProperty(), 0.4, Motion.EASE) ) ); timeline.setOnFinished(event -> primaryStage.setIconified(true)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorAnimationProducer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorAnimationProducer.java deleted file mode 100644 index 6a16a7fcd..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorAnimationProducer.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.ui.decorator; - -import javafx.animation.Interpolator; -import javafx.animation.KeyFrame; -import javafx.animation.KeyValue; -import javafx.scene.Node; -import javafx.util.Duration; -import org.jackhuang.hmcl.ui.animation.AnimationHandler; -import org.jackhuang.hmcl.ui.animation.AnimationProducer; -import org.jackhuang.hmcl.ui.animation.TransitionPane; -import org.jetbrains.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -public final class DecoratorAnimationProducer implements AnimationProducer { - @Override - public void init(AnimationHandler handler) { - } - - @Override - public List animate(AnimationHandler handler) { - Node prev = handler.getPreviousNode(); - Node next = handler.getCurrentNode(); - if (prev instanceof TransitionPane.EmptyPane) { - return Collections.emptyList(); - } - - Duration halfDuration = handler.getDuration().divide(2); - - List keyFrames = new ArrayList<>(); - - keyFrames.add(new KeyFrame(Duration.ZERO, - new KeyValue(prev.opacityProperty(), 1, Interpolator.EASE_BOTH))); - keyFrames.add(new KeyFrame(halfDuration, - new KeyValue(prev.opacityProperty(), 0, Interpolator.EASE_BOTH))); - if (prev instanceof DecoratorAnimatedPage) { - Node left = ((DecoratorAnimatedPage) prev).getLeft(); - Node center = ((DecoratorAnimatedPage) prev).getCenter(); - - keyFrames.add(new KeyFrame(Duration.ZERO, - new KeyValue(left.translateXProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(center.translateXProperty(), 0, Interpolator.EASE_BOTH))); - keyFrames.add(new KeyFrame(halfDuration, - new KeyValue(left.translateXProperty(), -30, Interpolator.EASE_BOTH), - new KeyValue(center.translateXProperty(), 30, Interpolator.EASE_BOTH))); - } - - keyFrames.add(new KeyFrame(halfDuration, - new KeyValue(next.opacityProperty(), 0, Interpolator.EASE_BOTH))); - keyFrames.add(new KeyFrame(handler.getDuration(), - new KeyValue(next.opacityProperty(), 1, Interpolator.EASE_BOTH))); - if (next instanceof DecoratorAnimatedPage) { - Node left = ((DecoratorAnimatedPage) next).getLeft(); - Node center = ((DecoratorAnimatedPage) next).getCenter(); - - keyFrames.add(new KeyFrame(halfDuration, - new KeyValue(left.translateXProperty(), -30, Interpolator.EASE_BOTH), - new KeyValue(center.translateXProperty(), 30, Interpolator.EASE_BOTH))); - keyFrames.add(new KeyFrame(handler.getDuration(), - new KeyValue(left.translateXProperty(), 0, Interpolator.EASE_BOTH), - new KeyValue(center.translateXProperty(), 0, Interpolator.EASE_BOTH))); - } - - return keyFrames; - } - - @Override - public @Nullable AnimationProducer opposite() { - return null; - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java index 27ff5bccf..1d34d0da4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorController.java @@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.decorator; import com.jfoenix.controls.JFXDialog; import com.jfoenix.controls.JFXSnackbar; +import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; @@ -49,8 +50,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane; -import org.jackhuang.hmcl.ui.animation.AnimationUtils; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.*; import org.jackhuang.hmcl.ui.construct.DialogAware; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.Navigator; @@ -90,16 +90,16 @@ public class DecoratorController { if (AnimationUtils.playWindowAnimation()) { Timeline timeline = new Timeline( new KeyFrame(Duration.millis(0), - new KeyValue(decorator.opacityProperty(), 1, FXUtils.EASE), - new KeyValue(decorator.scaleXProperty(), 1, FXUtils.EASE), - new KeyValue(decorator.scaleYProperty(), 1, FXUtils.EASE), - new KeyValue(decorator.scaleZProperty(), 0.3, FXUtils.EASE) + new KeyValue(decorator.opacityProperty(), 1, Motion.EASE), + new KeyValue(decorator.scaleXProperty(), 1, Motion.EASE), + new KeyValue(decorator.scaleYProperty(), 1, Motion.EASE), + new KeyValue(decorator.scaleZProperty(), 0.3, Motion.EASE) ), new KeyFrame(Duration.millis(200), - new KeyValue(decorator.opacityProperty(), 0, FXUtils.EASE), - new KeyValue(decorator.scaleXProperty(), 0.8, FXUtils.EASE), - new KeyValue(decorator.scaleYProperty(), 0.8, FXUtils.EASE), - new KeyValue(decorator.scaleZProperty(), 0.8, FXUtils.EASE) + new KeyValue(decorator.opacityProperty(), 0, Motion.EASE), + new KeyValue(decorator.scaleXProperty(), 0.8, Motion.EASE), + new KeyValue(decorator.scaleYProperty(), 0.8, Motion.EASE), + new KeyValue(decorator.scaleZProperty(), 0.8, Motion.EASE) ) ); timeline.setOnFinished(event -> Launcher.stopApplication()); @@ -364,10 +364,57 @@ public class DecoratorController { // ==== Navigation ==== - private static final DecoratorAnimationProducer animation = new DecoratorAnimationProducer(); + private static final TransitionPane.AnimationProducer ANIMATION = (Pane container, + Node previousNode, Node nextNode, + Duration duration, + Interpolator interpolator) -> { + Timeline timeline = new Timeline(); + if (previousNode instanceof TransitionPane.EmptyPane) { + return timeline; + } + + Duration halfDuration = duration.divide(2); + + List keyFrames = new ArrayList<>(); + + keyFrames.add(new KeyFrame(Duration.ZERO, + new KeyValue(previousNode.opacityProperty(), 1, interpolator))); + keyFrames.add(new KeyFrame(halfDuration, + new KeyValue(previousNode.opacityProperty(), 0, interpolator))); + if (previousNode instanceof DecoratorAnimatedPage prevPage) { + Node left = prevPage.getLeft(); + Node center = prevPage.getCenter(); + + keyFrames.add(new KeyFrame(Duration.ZERO, + new KeyValue(left.translateXProperty(), 0, interpolator), + new KeyValue(center.translateXProperty(), 0, interpolator))); + keyFrames.add(new KeyFrame(halfDuration, + new KeyValue(left.translateXProperty(), -30, interpolator), + new KeyValue(center.translateXProperty(), 30, interpolator))); + } + + keyFrames.add(new KeyFrame(halfDuration, + new KeyValue(nextNode.opacityProperty(), 0, interpolator))); + keyFrames.add(new KeyFrame(duration, + new KeyValue(nextNode.opacityProperty(), 1, interpolator))); + if (nextNode instanceof DecoratorAnimatedPage nextPage) { + Node left = nextPage.getLeft(); + Node center = nextPage.getCenter(); + + keyFrames.add(new KeyFrame(halfDuration, + new KeyValue(left.translateXProperty(), -30, interpolator), + new KeyValue(center.translateXProperty(), 30, interpolator))); + keyFrames.add(new KeyFrame(duration, + new KeyValue(left.translateXProperty(), 0, interpolator), + new KeyValue(center.translateXProperty(), 0, interpolator))); + } + + timeline.getKeyFrames().setAll(keyFrames); + return timeline; + }; public void navigate(Node node) { - navigator.navigate(node, animation); + navigator.navigate(node, ANIMATION); } private void close() { @@ -458,7 +505,9 @@ public class DecoratorController { Platform.runLater(() -> showDialog(node)); return; } - dialog = new JFXDialog(); + dialog = new JFXDialog(AnimationUtils.isAnimationEnabled() + ? JFXDialog.DialogTransition.CENTER + : JFXDialog.DialogTransition.NONE); dialogPane = new JFXDialogPane(); dialog.setContent(dialogPane); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorNavigatorPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorNavigatorPage.java deleted file mode 100644 index 7f0c86115..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorNavigatorPage.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.ui.decorator; - -import javafx.scene.Node; -import org.jackhuang.hmcl.ui.animation.AnimationProducer; -import org.jackhuang.hmcl.ui.construct.Navigator; - -public abstract class DecoratorNavigatorPage extends DecoratorTransitionPage { - protected final Navigator navigator = new Navigator(); - - { - this.navigator.setOnNavigating(this::onNavigating); - this.navigator.setOnNavigated(this::onNavigated); - backableProperty().bind(navigator.backableProperty()); - } - - @Override - protected void navigate(Node page, AnimationProducer animationProducer) { - navigator.navigate(page, animationProducer); - } - - @Override - public boolean back() { - if (navigator.canGoBack()) { - navigator.close(); - return false; - } else { - return true; - } - } - - private void onNavigating(Navigator.NavigationEvent event) { - if (event.getSource() != this.navigator) return; - onNavigating(event.getNode()); - } - - private void onNavigated(Navigator.NavigationEvent event) { - if (event.getSource() != this.navigator) return; - onNavigated(event.getNode()); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java index a4a5be60f..af3cd2238 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorSkin.java @@ -18,6 +18,10 @@ package org.jackhuang.hmcl.ui.decorator; import com.jfoenix.controls.JFXButton; +import javafx.animation.Interpolator; +import javafx.animation.KeyFrame; +import javafx.animation.KeyValue; +import javafx.animation.Timeline; import javafx.beans.InvalidationListener; import javafx.beans.WeakInvalidationListener; import javafx.beans.binding.Bindings; @@ -39,12 +43,13 @@ import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.stage.Stage; +import javafx.util.Duration; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.AnimationProducer; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; +import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -205,16 +210,13 @@ public class DecoratorSkin extends SkinBase { if (s == null) return; Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode()); if (s.isAnimate()) { - AnimationProducer animation; - if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.NEXT) { - animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT; - } else if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.PREVIOUS) { - animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT; - } else { - animation = ContainerAnimations.FADE; - } + TransitionPane.AnimationProducer animation = switch (skinnable.getNavigationDirection()) { + case NEXT -> NavBarAnimations.NEXT; + case PREVIOUS -> NavBarAnimations.PREVIOUS; + default -> ContainerAnimations.FADE; + }; skinnable.setNavigationDirection(Navigation.NavigationDirection.START); - navBarPane.setContent(node, animation); + navBarPane.setContent(node, animation, Motion.SHORT4); } else { navBarPane.getChildren().setAll(node); } @@ -500,4 +502,78 @@ public class DecoratorSkin extends SkinBase { } } } + + enum NavBarAnimations implements TransitionPane.AnimationProducer { + NEXT { + @Override + public void init(TransitionPane container, Node previousNode, Node nextNode) { + previousNode.setScaleX(1); + previousNode.setScaleY(1); + previousNode.setOpacity(0); + previousNode.setTranslateX(0); + nextNode.setScaleX(1); + nextNode.setScaleY(1); + nextNode.setOpacity(1); + nextNode.setTranslateX(container.getWidth()); + } + + @Override + public Timeline animate( + Pane container, Node previousNode, Node nextNode, + Duration duration, Interpolator interpolator) { + return new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(nextNode.translateXProperty(), 50, interpolator), + new KeyValue(previousNode.translateXProperty(), 0, interpolator), + new KeyValue(nextNode.opacityProperty(), 0, interpolator), + new KeyValue(previousNode.opacityProperty(), 1, interpolator)), + new KeyFrame(duration, + new KeyValue(nextNode.translateXProperty(), 0, interpolator), + new KeyValue(previousNode.translateXProperty(), -50, interpolator), + new KeyValue(nextNode.opacityProperty(), 1, interpolator), + new KeyValue(previousNode.opacityProperty(), 0, interpolator)) + ); + } + + @Override + public TransitionPane.AnimationProducer opposite() { + return NEXT; + } + }, + + PREVIOUS { + @Override + public void init(TransitionPane container, Node previousNode, Node nextNode) { + previousNode.setScaleX(1); + previousNode.setScaleY(1); + previousNode.setOpacity(1); + previousNode.setTranslateX(0); + nextNode.setScaleX(1); + nextNode.setScaleY(1); + nextNode.setOpacity(0); + nextNode.setTranslateX(container.getWidth()); + } + + @Override + public Timeline animate(Pane container, Node previousNode, Node nextNode, Duration duration, Interpolator interpolator) { + return new Timeline( + new KeyFrame(Duration.ZERO, + new KeyValue(nextNode.translateXProperty(), -50, interpolator), + new KeyValue(previousNode.translateXProperty(), 0, interpolator), + new KeyValue(nextNode.opacityProperty(), 0, interpolator), + new KeyValue(previousNode.opacityProperty(), 1, interpolator)), + new KeyFrame(duration, + new KeyValue(nextNode.translateXProperty(), 0, interpolator), + new KeyValue(previousNode.translateXProperty(), 50, interpolator), + new KeyValue(nextNode.opacityProperty(), 1, interpolator), + new KeyValue(previousNode.opacityProperty(), 0, interpolator)) + ); + } + + @Override + public TransitionPane.AnimationProducer opposite() { + return PREVIOUS; + } + }; + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTabPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTabPage.java deleted file mode 100644 index e7cf6b033..000000000 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTabPage.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.jackhuang.hmcl.ui.decorator; - -import javafx.beans.property.ObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -import javafx.scene.control.SingleSelectionModel; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; -import org.jackhuang.hmcl.ui.construct.Navigator; -import org.jackhuang.hmcl.ui.construct.TabControl; -import org.jackhuang.hmcl.ui.construct.TabHeader; -import org.jackhuang.hmcl.ui.wizard.Navigation; - -public abstract class DecoratorTabPage extends DecoratorTransitionPage implements TabControl { - - public DecoratorTabPage() { - getSelectionModel().selectedItemProperty().addListener((a, b, newValue) -> { - newValue.initializeIfNeeded(); - if (newValue.getNode() != null) { - onNavigating(getCurrentPage()); - if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATING)); - navigate(newValue.getNode(), ContainerAnimations.FADE); - onNavigated(getCurrentPage()); - if (getCurrentPage() != null) getCurrentPage().fireEvent(new Navigator.NavigationEvent(null, getCurrentPage(), Navigation.NavigationDirection.NEXT, Navigator.NavigationEvent.NAVIGATED)); - } - }); - } - - public DecoratorTabPage(TabHeader.Tab... tabs) { - this(); - if (tabs != null) { - getTabs().addAll(tabs); - } - } - - private ObservableList> tabs = FXCollections.observableArrayList(); - - @Override - public ObservableList> getTabs() { - return tabs; - } - - private final ObjectProperty>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControl.TabControlSelectionModel(this)); - - public SingleSelectionModel> getSelectionModel() { - return selectionModel.get(); - } - - public ObjectProperty>> selectionModelProperty() { - return selectionModel; - } - - public void setSelectionModel(SingleSelectionModel> selectionModel) { - this.selectionModel.set(selectionModel); - } -} diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java index 58b3ef42d..ed1686a12 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorTransitionPage.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.ui.decorator; +import javafx.animation.Interpolator; import javafx.beans.binding.Bindings; import javafx.beans.property.*; import javafx.scene.Node; @@ -24,7 +25,7 @@ import javafx.scene.control.Control; import javafx.scene.control.Skin; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; -import org.jackhuang.hmcl.ui.animation.AnimationProducer; +import javafx.util.Duration; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.wizard.Refreshable; @@ -36,8 +37,8 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat private Node currentPage; protected final TransitionPane transitionPane = new TransitionPane(); - protected void navigate(Node page, AnimationProducer animation) { - transitionPane.setContent(currentPage = page, animation); + protected void navigate(Node page, TransitionPane.AnimationProducer animation, Duration duration, Interpolator interpolator) { + transitionPane.setContent(currentPage = page, animation, duration, interpolator); } protected void onNavigating(Node from) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java index ff5edc34f..742e6afea 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/decorator/DecoratorWizardDisplayer.java @@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.decorator; import javafx.scene.Node; import javafx.scene.control.SkinBase; import org.jackhuang.hmcl.task.Task; +import org.jackhuang.hmcl.ui.animation.Motion; import org.jackhuang.hmcl.ui.construct.Navigator; import org.jackhuang.hmcl.ui.construct.PageCloseEvent; import org.jackhuang.hmcl.ui.wizard.*; @@ -74,7 +75,7 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements @Override public void navigateTo(Node page, Navigation.NavigationDirection nav) { displayer.navigateTo(page, nav); - navigate(page, nav.getAnimation()); + navigate(page, nav.getAnimation(), Motion.SHORT4, Motion.EASE); String prefix = category == null ? "" : category + " - "; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AbstractInstallersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AbstractInstallersPage.java index 237e1f2b2..d18787673 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AbstractInstallersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AbstractInstallersPage.java @@ -33,6 +33,7 @@ import javafx.scene.layout.BorderPane; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.InstallerItem; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; +import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardPage; import org.jackhuang.hmcl.util.SettingsMap; @@ -60,7 +61,16 @@ public abstract class AbstractInstallersPage extends Control implements WizardPa } if (!(library.resolvedStateProperty().get() instanceof InstallerItem.IncompatibleState)) - controller.onNext(new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer." + libraryId)), gameVersion, downloadProvider, libraryId, () -> controller.onPrev(false))); + controller.onNext( + new VersionsPage( + controller, + i18n("install.installer.choose", i18n("install.installer." + libraryId)), + gameVersion, + downloadProvider, + libraryId, + () -> controller.onPrev(false, Navigation.NavigationDirection.NEXT) + ), Navigation.NavigationDirection.NEXT + ); }); library.setOnRemove(() -> { controller.getSettings().remove(libraryId); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index e70a19130..7291495b4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -36,7 +36,6 @@ import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.WeakListenerHolder; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.AdvancedListBox; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; @@ -105,9 +104,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage Profiles.registerVersionsListener(this::loadVersions); tab.select(newGameTab); - FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> { - transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE); - }); + transitionPane.bindTabHeader(tab); AdvancedListBox sideBar = new AdvancedListBox() .startCategory(i18n("download.game").toUpperCase(Locale.ROOT)) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java index d2f6a5f70..ffea66e1a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/LauncherSettingsPage.java @@ -23,7 +23,6 @@ import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; @@ -60,10 +59,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor tab.select(gameTab); addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null)); - transitionPane.setContent(gameTab.getNode(), ContainerAnimations.NONE); - FXUtils.onChange(tab.getSelectionModel().selectedItemProperty(), newValue -> { - transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE); - }); + transitionPane.bindTabHeader(tab); AdvancedListBox sideBar = new AdvancedListBox() .addNavigationDrawerTab(tab, gameTab, i18n("settings.type.global.manage"), SVG.STADIA_CONTROLLER) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java index d96988a00..50e0944d4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java @@ -535,7 +535,7 @@ public class TerracottaControllerPage extends StackPane { children.addAll(nodesProperty); } - transition.setContent(components, ContainerAnimations.SWIPE_LEFT_FADE_SHORT); + transition.setContent(components, ContainerAnimations.SLIDE_UP_FADE_IN); }; listener.changed(UI_STATE, null, UI_STATE.get()); holder.add(listener); @@ -718,7 +718,7 @@ public class TerracottaControllerPage extends StackPane { pane.getChildren().add(item); } - this.transition.setContent(pane, ContainerAnimations.SWIPE_LEFT_FADE_SHORT); + this.transition.setContent(pane, ContainerAnimations.SLIDE_UP_FADE_IN); } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaPage.java index 853a6a50f..478e2733d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaPage.java @@ -31,7 +31,6 @@ import org.jackhuang.hmcl.terracotta.TerracottaMetadata; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.AdvancedListBox; import org.jackhuang.hmcl.ui.construct.PageAware; @@ -58,11 +57,7 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa statusPage.setNodeSupplier(TerracottaControllerPage::new); tab = new TabHeader(statusPage); tab.select(statusPage); - - transitionPane.setContent(statusPage.getNode(), ContainerAnimations.NONE); - FXUtils.onChange(tab.getSelectionModel().selectedItemProperty(), newValue -> { - transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE); - }); + transitionPane.bindTabHeader(tab); BorderPane left = new BorderPane(); FXUtils.setLimitWidth(left, 200); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java index 177e7c82d..b37e0c24b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java @@ -37,7 +37,6 @@ import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.WeakListenerHolder; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; @@ -77,10 +76,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); tab.select(versionSettingsTab); - transitionPane.setContent(versionSettingsTab.getNode(), ContainerAnimations.NONE); - FXUtils.onChange(tab.getSelectionModel().selectedItemProperty(), newValue -> { - transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE); - }); + transitionPane.bindTabHeader(tab); listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST)); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java index f99faa459..40c77fc96 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java @@ -28,7 +28,6 @@ import javafx.scene.layout.VBox; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; -import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; @@ -73,9 +72,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco datapackTab.setNodeSupplier(() -> new DatapackListPage(this)); header.select(worldInfoTab); - transitionPane.setContent(worldInfoTab.getNode(), ContainerAnimations.NONE); - FXUtils.onChange(header.getSelectionModel().selectedItemProperty(), newValue -> - transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE)); + transitionPane.bindTabHeader(header); + setCenter(transitionPane); BorderPane left = new BorderPane(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Navigation.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Navigation.java index a99f5aac0..ca787cf1d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Navigation.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/Navigation.java @@ -40,11 +40,9 @@ public interface Navigation { enum NavigationDirection { START(ContainerAnimations.NONE), - PREVIOUS(ContainerAnimations.SWIPE_RIGHT), - NEXT(ContainerAnimations.SWIPE_LEFT), - FINISH(ContainerAnimations.SWIPE_LEFT), - IN(ContainerAnimations.ZOOM_IN), - OUT(ContainerAnimations.ZOOM_OUT); + PREVIOUS(ContainerAnimations.BACKWARD), + NEXT(ContainerAnimations.FORWARD), + FINISH(ContainerAnimations.FORWARD); private final ContainerAnimations animation; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.java index b7b8a56c1..c1a0b0bf2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/wizard/WizardController.java @@ -83,6 +83,10 @@ public class WizardController implements Navigation { } public void onNext(Node page) { + onNext(page, NavigationDirection.NEXT); + } + + public void onNext(Node page, NavigationDirection direction) { pages.push(page); if (stopped) { // navigatingTo may stop this wizard. @@ -93,11 +97,15 @@ public class WizardController implements Navigation { ((WizardPage) page).onNavigate(settings); LOG.info("Navigating to " + page + ", pages: " + pages); - displayer.navigateTo(page, NavigationDirection.NEXT); + displayer.navigateTo(page, direction); } @Override public void onPrev(boolean cleanUp) { + onPrev(cleanUp, NavigationDirection.PREVIOUS); + } + + public void onPrev(boolean cleanUp, NavigationDirection direction) { if (!canPrev()) { if (provider.cancelIfCannotGoBack()) { onCancel(); @@ -116,7 +124,7 @@ public class WizardController implements Navigation { ((WizardPage) prevPage).onNavigate(settings); LOG.info("Navigating to " + prevPage + ", pages: " + pages); - displayer.navigateTo(prevPage, NavigationDirection.PREVIOUS); + displayer.navigateTo(prevPage, direction); } @Override