更新界面动画效果 (#4780)

This commit is contained in:
Glavo
2025-11-14 16:17:14 +08:00
parent 756dce0d6f
commit 44869da20e
28 changed files with 1804 additions and 712 deletions

View File

@@ -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<? super MouseEvent> 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<Node> 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<DialogTransition> transitionType = new SimpleStyleableObjectProperty<>(
StyleableProperties.DIALOG_TRANSITION,
JFXDialog.this,
"dialogTransition",
DialogTransition.CENTER);
public DialogTransition getTransitionType() {
return transitionType == null ? DialogTransition.CENTER : transitionType.get();
}
public StyleableObjectProperty<DialogTransition> transitionTypeProperty() {
return this.transitionType;
}
public void setTransitionType(DialogTransition transition) {
this.transitionType.set(transition);
}
private static final class StyleableProperties {
private static final CssMetaData<JFXDialog, DialogTransition> DIALOG_TRANSITION =
new CssMetaData<JFXDialog, DialogTransition>("-jfx-dialog-transition",
DialogTransitionConverter.getInstance(),
DialogTransition.CENTER) {
@Override
public boolean isSettable(JFXDialog control) {
return control.transitionType == null || !control.transitionType.isBound();
}
@Override
public StyleableProperty<DialogTransition> getStyleableProperty(JFXDialog control) {
return control.transitionTypeProperty();
}
};
private static final List<CssMetaData<? extends Styleable, ?>> CHILD_STYLEABLES;
static {
final List<CssMetaData<? extends Styleable, ?>> styleables =
new ArrayList<>(StackPane.getClassCssMetaData());
Collections.addAll(styleables,
DIALOG_TRANSITION
);
CHILD_STYLEABLES = Collections.unmodifiableList(styleables);
}
}
@Override
public List<CssMetaData<? extends Styleable, ?>> getCssMetaData() {
return getClassCssMetaData();
}
public static List<CssMetaData<? extends Styleable, ?>> getClassCssMetaData() {
return StyleableProperties.CHILD_STYLEABLES;
}
/***************************************************************************
* *
* Custom Events *
* *
**************************************************************************/
private final ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogClosedProperty = new ObjectPropertyBase<EventHandler<? super JFXDialogEvent>>() {
@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<EventHandler<? super JFXDialogEvent>> onDialogClosedProperty() {
return onDialogClosedProperty;
}
public void setOnDialogClosed(EventHandler<? super JFXDialogEvent> handler) {
onDialogClosedProperty().set(handler);
}
public EventHandler<? super JFXDialogEvent> getOnDialogClosed() {
return onDialogClosedProperty().get();
}
private final ObjectProperty<EventHandler<? super JFXDialogEvent>> onDialogOpenedProperty = new ObjectPropertyBase<EventHandler<? super JFXDialogEvent>>() {
@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<EventHandler<? super JFXDialogEvent>> onDialogOpenedProperty() {
return onDialogOpenedProperty;
}
public void setOnDialogOpened(EventHandler<? super JFXDialogEvent> handler) {
onDialogOpenedProperty().set(handler);
}
public EventHandler<? super JFXDialogEvent> getOnDialogOpened() {
return onDialogOpenedProperty().get();
}
}

View File

@@ -49,6 +49,7 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor; import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.account.AccountListPage; import org.jackhuang.hmcl.ui.account.AccountListPage;
import org.jackhuang.hmcl.ui.animation.AnimationUtils; 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.*;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType; import org.jackhuang.hmcl.ui.construct.MessageDialogPane.MessageType;
import org.jackhuang.hmcl.ui.decorator.DecoratorController; import org.jackhuang.hmcl.ui.decorator.DecoratorController;
@@ -328,16 +329,16 @@ public final class Controllers {
if (AnimationUtils.playWindowAnimation()) { if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline( Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0), new KeyFrame(Duration.millis(0),
new KeyValue(decorator.getDecorator().opacityProperty(), 0, FXUtils.EASE), new KeyValue(decorator.getDecorator().opacityProperty(), 0, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, FXUtils.EASE), new KeyValue(decorator.getDecorator().scaleXProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, FXUtils.EASE), new KeyValue(decorator.getDecorator().scaleYProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, FXUtils.EASE) new KeyValue(decorator.getDecorator().scaleZProperty(), 0.8, Motion.EASE)
), ),
new KeyFrame(Duration.millis(600), new KeyFrame(Duration.millis(600),
new KeyValue(decorator.getDecorator().opacityProperty(), 1, FXUtils.EASE), new KeyValue(decorator.getDecorator().opacityProperty(), 1, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleXProperty(), 1, FXUtils.EASE), new KeyValue(decorator.getDecorator().scaleXProperty(), 1, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleYProperty(), 1, FXUtils.EASE), new KeyValue(decorator.getDecorator().scaleYProperty(), 1, Motion.EASE),
new KeyValue(decorator.getDecorator().scaleZProperty(), 1, FXUtils.EASE) new KeyValue(decorator.getDecorator().scaleZProperty(), 1, Motion.EASE)
) )
); );
timeline.play(); timeline.play();

View File

@@ -437,28 +437,12 @@ public final class FXUtils {
installSlowTooltip(node, new Tooltip(tooltip)); installSlowTooltip(node, new Tooltip(tooltip));
} }
public static void playAnimation(Node node, String animationKey, Timeline timeline) { public static void playAnimation(Node node, String animationKey, Animation animation) {
animationKey = "FXUTILS.ANIMATION." + animationKey; animationKey = "hmcl.animations." + animationKey;
Object oldTimeline = node.getProperties().get(animationKey); if (node.getProperties().get(animationKey) instanceof Animation oldAnimation)
// if (oldTimeline instanceof Timeline) ((Timeline) oldTimeline).stop(); oldAnimation.stop();
if (timeline != null) timeline.play(); animation.play();
node.getProperties().put(animationKey, timeline); node.getProperties().put(animationKey, animation);
}
public static <T> Animation playAnimation(Node node, String animationKey, Duration duration, WritableValue<T> 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 openFolder(Path file) { 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) { public static void onEscPressed(Node node, Runnable action) {
node.addEventHandler(KeyEvent.KEY_PRESSED, e -> { node.addEventHandler(KeyEvent.KEY_PRESSED, e -> {
if (e.getCode() == KeyCode.ESCAPE) { if (e.getCode() == KeyCode.ESCAPE) {

View File

@@ -1,32 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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();
}

View File

@@ -1,31 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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<KeyFrame> animate(AnimationHandler handler);
@Nullable AnimationProducer opposite();
}

View File

@@ -20,33 +20,23 @@ package org.jackhuang.hmcl.ui.animation;
import javafx.animation.Interpolator; import javafx.animation.Interpolator;
import javafx.animation.KeyFrame; import javafx.animation.KeyFrame;
import javafx.animation.KeyValue; import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.util.Duration; import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jetbrains.annotations.Nullable;
import java.util.Arrays; public enum ContainerAnimations implements TransitionPane.AnimationProducer {
import java.util.Collections;
import java.util.List;
public enum ContainerAnimations implements AnimationProducer {
NONE { NONE {
@Override @Override
public void init(AnimationHandler c) { public Timeline animate(
c.getPreviousNode().setTranslateX(0); Pane container, Node previousNode, Node nextNode,
c.getPreviousNode().setTranslateY(0); Duration duration, Interpolator interpolator) {
c.getPreviousNode().setScaleX(1); return new Timeline();
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);
} }
@Override @Override
public List<KeyFrame> animate(AnimationHandler c) { public TransitionPane.AnimationProducer opposite() {
return Collections.emptyList(); return this;
} }
}, },
@@ -55,151 +45,48 @@ public enum ContainerAnimations implements AnimationProducer {
*/ */
FADE { FADE {
@Override @Override
public void init(AnimationHandler c) { public Timeline animate(
c.getPreviousNode().setTranslateX(0); Pane container, Node previousNode, Node nextNode,
c.getPreviousNode().setTranslateY(0); Duration duration, Interpolator interpolator) {
c.getPreviousNode().setScaleX(1); return new Timeline(new KeyFrame(Duration.ZERO,
c.getPreviousNode().setScaleY(1); new KeyValue(previousNode.opacityProperty(), 1, interpolator),
c.getPreviousNode().setOpacity(1); new KeyValue(nextNode.opacityProperty(), 0, interpolator)),
c.getCurrentNode().setTranslateX(0); new KeyFrame(duration,
c.getCurrentNode().setTranslateY(0); new KeyValue(previousNode.opacityProperty(), 0, interpolator),
c.getCurrentNode().setScaleX(1); new KeyValue(nextNode.opacityProperty(), 1, interpolator)));
c.getCurrentNode().setScaleY(1);
c.getCurrentNode().setOpacity(0);
} }
@Override @Override
public List<KeyFrame> animate(AnimationHandler c) { public TransitionPane.AnimationProducer opposite() {
return Arrays.asList(new KeyFrame(Duration.ZERO, return this;
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)));
} }
}, },
/**
* 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<KeyFrame> 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<KeyFrame> 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<KeyFrame> 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<KeyFrame> 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 * A swipe effect
*/ */
SWIPE_LEFT { SWIPE_LEFT {
@Override @Override
public void init(AnimationHandler c) { public void init(TransitionPane container, Node previousNode, Node nextNode) {
c.getPreviousNode().setScaleX(1); super.init(container, previousNode, nextNode);
c.getPreviousNode().setScaleY(1); nextNode.setTranslateX(container.getWidth());
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());
} }
@Override @Override
public List<KeyFrame> animate(AnimationHandler c) { public Timeline animate(
return Arrays.asList(new KeyFrame(Duration.ZERO, Pane container, Node previousNode, Node nextNode,
new KeyValue(c.getCurrentNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH), Duration duration, Interpolator interpolator) {
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)), return new Timeline(new KeyFrame(Duration.ZERO,
new KeyFrame(c.getDuration(), new KeyValue(nextNode.translateXProperty(), container.getWidth(), interpolator),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(previousNode.translateXProperty(), 0, interpolator)),
new KeyValue(c.getPreviousNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))); 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 { SWIPE_RIGHT {
@Override @Override
public void init(AnimationHandler c) { public void init(TransitionPane container, Node previousNode, Node nextNode) {
c.getPreviousNode().setScaleX(1); super.init(container, previousNode, nextNode);
c.getPreviousNode().setScaleY(1); nextNode.setTranslateX(-container.getWidth());
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());
} }
@Override @Override
public List<KeyFrame> animate(AnimationHandler c) { public Timeline animate(
return Arrays.asList(new KeyFrame(Duration.ZERO, Pane container, Node previousNode, Node nextNode,
new KeyValue(c.getCurrentNode().translateXProperty(), -c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH), Duration duration, Interpolator interpolator) {
new KeyValue(c.getPreviousNode().translateXProperty(), 0, Interpolator.EASE_BOTH)), return new Timeline(new KeyFrame(Duration.ZERO,
new KeyFrame(c.getDuration(), new KeyValue(nextNode.translateXProperty(), -container.getWidth(), interpolator),
new KeyValue(c.getCurrentNode().translateXProperty(), 0, Interpolator.EASE_BOTH), new KeyValue(previousNode.translateXProperty(), 0, interpolator)),
new KeyValue(c.getPreviousNode().translateXProperty(), c.getCurrentRoot().getWidth(), Interpolator.EASE_BOTH))); 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 <a href="https://m3.material.io/styles/motion/transitions/transition-patterns">Transitions - Material Design 3</a>
FORWARD {
@Override @Override
public void init(AnimationHandler c) { public Timeline animate(
c.getPreviousNode().setScaleX(1); Pane container, Node previousNode, Node nextNode,
c.getPreviousNode().setScaleY(1); Duration duration, Interpolator interpolator) {
c.getPreviousNode().setOpacity(0); double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50;
c.getPreviousNode().setTranslateX(0); return new Timeline(
c.getCurrentNode().setScaleX(1); new KeyFrame(Duration.ZERO,
c.getCurrentNode().setScaleY(1); new KeyValue(previousNode.translateXProperty(), 0, interpolator),
c.getCurrentNode().setOpacity(1); new KeyValue(previousNode.opacityProperty(), 1, interpolator),
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth()); 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 @Override
public List<KeyFrame> animate(AnimationHandler c) { public TransitionPane.AnimationProducer opposite() {
return Arrays.asList(new KeyFrame(Duration.ZERO, return BACKWARD;
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)));
} }
}, },
SWIPE_RIGHT_FADE_SHORT { /// @see <a href="https://m3.material.io/styles/motion/transitions/transition-patterns">Transitions - Material Design 3</a>
BACKWARD {
@Override @Override
public void init(AnimationHandler c) { public Timeline animate(
c.getPreviousNode().setScaleX(1); Pane container, Node previousNode, Node nextNode,
c.getPreviousNode().setScaleY(1); Duration duration, Interpolator interpolator) {
c.getPreviousNode().setOpacity(0); double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50;
c.getPreviousNode().setTranslateX(0); return new Timeline(
c.getCurrentNode().setScaleX(1); new KeyFrame(Duration.ZERO,
c.getCurrentNode().setScaleY(1); new KeyValue(previousNode.translateXProperty(), 0, interpolator),
c.getCurrentNode().setOpacity(1); new KeyValue(previousNode.opacityProperty(), 1, interpolator),
c.getCurrentNode().setTranslateX(c.getCurrentRoot().getWidth()); 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 @Override
public List<KeyFrame> animate(AnimationHandler c) { public TransitionPane.AnimationProducer opposite() {
return Arrays.asList(new KeyFrame(Duration.ZERO, return FORWARD;
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)));
} }
}; },
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 { protected static void reset(Node node) {
NONE.opposite = NONE; node.setTranslateX(0);
FADE.opposite = FADE; node.setTranslateY(0);
SWIPE_LEFT.opposite = SWIPE_RIGHT; node.setScaleX(1);
SWIPE_RIGHT.opposite = SWIPE_LEFT; node.setScaleY(1);
FADE_IN.opposite = FADE_OUT; node.setOpacity(1);
FADE_OUT.opposite = FADE_IN;
ZOOM_IN.opposite = ZOOM_OUT;
ZOOM_OUT.opposite = ZOOM_IN;
} }
@Override @Override
public abstract void init(AnimationHandler handler); public void init(TransitionPane container, Node previousNode, Node nextNode) {
reset(previousNode);
@Override reset(nextNode);
public abstract List<KeyFrame> animate(AnimationHandler handler);
@Override
public @Nullable ContainerAnimations opposite() {
return opposite;
} }
} }

View File

@@ -0,0 +1,781 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.animation;
import javafx.animation.Interpolator;
import javafx.util.Duration;
import java.util.Objects;
/// @author Glavo
/// @see <a href="https://api.flutter.dev/flutter/animation/Curves-class.html">Flutter Curves</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_linear.mp4">curve_linear.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease.mp4">curve_ease.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in.mp4">curve_ease_in.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_to_linear.mp4">curve_ease_in_to_linear.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_sine.mp4">curve_ease_in_sine.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quad.mp4">curve_ease_in_quad.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_cubic.mp4">curve_ease_in_cubic.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quart.mp4">curve_ease_in_quart.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quint.mp4">curve_ease_in_quint.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_expo.mp4">curve_ease_in_expo.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_circ.mp4">curve_ease_in_circ.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_back.mp4">curve_ease_in_back.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out.mp4">curve_ease_out.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_linear_to_ease_out.mp4">curve_linear_to_ease_out.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_sine.mp4">curve_ease_out_sine.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quad.mp4">curve_ease_out_quad.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_cubic.mp4">curve_ease_out_cubic.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quart.mp4">curve_ease_out_quart.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quint.mp4">curve_ease_out_quint.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_expo.mp4">curve_ease_out_expo.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_circ.mp4">curve_ease_out_circ.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_back.mp4">curve_ease_out_back.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out.mp4">curve_ease_in_out.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_sine.mp4">curve_ease_in_out_sine.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quad.mp4">curve_ease_in_out_quad.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_cubic.mp4">curve_ease_in_out_cubic.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quart.mp4">curve_ease_in_out_quart.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quint.mp4">curve_ease_in_out_quint.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_expo.mp4">curve_ease_in_out_expo.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_circ.mp4">curve_ease_in_out_circ.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_cubic_emphasized.mp4">curve_ease_in_out_cubic_emphasized.mp4</a>
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 Penners easing functions.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_back.mp4">curve_ease_in_out_back.mp4</a>
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 isnt left waiting for the
/// animation to finish, and the negative effects of motion are minimized.
///
/// @see <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_fast_out_slow_in.mp4">curve_fast_out_slow_in.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_slow_middle.mp4">curve_slow_middle.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4">curve_bounce_in.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_out.mp4">curve_bounce_out.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in_out.mp4">curve_bounce_in_out.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in.mp4">curve_elastic_in.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_out.mp4">curve_elastic_out.mp4</a>
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 <a href="https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in_out.mp4">curve_elastic_in_out.mp4</a>
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() {
}
}

View File

@@ -17,77 +17,51 @@
*/ */
package org.jackhuang.hmcl.ui.animation; package org.jackhuang.hmcl.ui.animation;
import javafx.animation.Timeline; import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.util.Duration; import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils; 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 { public class TransitionPane extends StackPane {
private static final Duration DEFAULT_DURATION = Duration.millis(200);
private Duration duration; private Node currentNode;
private Node previousNode, currentNode;
public TransitionPane() { public TransitionPane() {
FXUtils.setOverflowHidden(this); FXUtils.setOverflowHidden(this);
} }
@Override
public Node getPreviousNode() {
return previousNode;
}
@Override
public Node getCurrentNode() { public Node getCurrentNode() {
return currentNode; return currentNode;
} }
@Override public void bindTabHeader(TabHeader tabHeader) {
public StackPane getCurrentRoot() { this.setContent(tabHeader.getSelectionModel().getSelectedItem().getNode(), ContainerAnimations.NONE);
return this; 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 final void setContent(Node newView, AnimationProducer transition) {
public Duration getDuration() { setContent(newView, transition, Motion.SHORT4);
return duration;
} }
public void setContent(Node newView, AnimationProducer transition) { public final void setContent(Node newView, AnimationProducer transition, Duration duration) {
setContent(newView, transition, DEFAULT_DURATION); setContent(newView, transition, duration, Motion.EASE);
} }
public void setContent(Node newView, AnimationProducer transition, Duration duration) { public void setContent(Node newView, AnimationProducer transition,
this.duration = duration; Duration duration, Interpolator interpolator) {
Node previousNode;
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) {
if (getWidth() > 0 && getHeight() > 0) { if (getWidth() > 0 && getHeight() > 0) {
previousNode = currentNode; previousNode = currentNode;
if (previousNode == null) { if (previousNode == null) {
@@ -105,10 +79,49 @@ public class TransitionPane extends StackPane implements AnimationHandler {
currentNode = newView; currentNode = newView;
getChildren().setAll(previousNode, currentNode); 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(); private final EmptyPane EMPTY_PANE = new EmptyPane();
public static class EmptyPane extends StackPane { 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;
}
}
} }

View File

@@ -17,6 +17,7 @@
*/ */
package org.jackhuang.hmcl.ui.construct; package org.jackhuang.hmcl.ui.construct;
import javafx.animation.Interpolator;
import javafx.beans.property.BooleanProperty; import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty; import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleBooleanProperty;
@@ -26,12 +27,14 @@ import javafx.event.EventHandler;
import javafx.event.EventType; import javafx.event.EventType;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils; 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.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.Navigation;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Stack; import java.util.Stack;
@@ -56,6 +59,10 @@ public class Navigator extends TransitionPane {
} }
public void navigate(Node node, AnimationProducer animationProducer) { 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(); FXUtils.checkFxUserThread();
if (!initialized) if (!initialized)
@@ -75,7 +82,7 @@ public class Navigator extends TransitionPane {
node.fireEvent(navigating); node.fireEvent(navigating);
node.getProperties().put("hmcl.navigator.animation", animationProducer); 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); NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATED);
node.fireEvent(navigated); node.fireEvent(navigated);
@@ -113,7 +120,7 @@ public class Navigator extends TransitionPane {
Node poppedNode = stack.pop(); Node poppedNode = stack.pop();
NavigationEvent exited = new NavigationEvent(this, poppedNode, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.EXITED); NavigationEvent exited = new NavigationEvent(this, poppedNode, Navigation.NavigationDirection.PREVIOUS, NavigationEvent.EXITED);
poppedNode.fireEvent(exited); poppedNode.fireEvent(exited);
if (poppedNode instanceof PageAware) ((PageAware) poppedNode).onPageHidden(); if (poppedNode instanceof PageAware pageAware) pageAware.onPageHidden();
backable.set(canGoBack()); backable.set(canGoBack());
Node node = stack.peek(); Node node = stack.peek();
@@ -123,8 +130,8 @@ public class Navigator extends TransitionPane {
node.fireEvent(navigating); node.fireEvent(navigating);
Object obj = from.getProperties().get("hmcl.navigator.animation"); Object obj = from.getProperties().get("hmcl.navigator.animation");
if (obj instanceof AnimationProducer) { if (obj instanceof AnimationProducer animationProducer) {
setContent(node, (AnimationProducer) obj); setContent(node, Objects.requireNonNullElse(animationProducer.opposite(), animationProducer));
} else { } else {
setContent(node, ContainerAnimations.NONE); setContent(node, ContainerAnimations.NONE);
} }
@@ -160,12 +167,13 @@ public class Navigator extends TransitionPane {
return stack.size(); return stack.size();
} }
public void setContent(Node content, AnimationProducer animationProducer) { @Override
super.setContent(content, animationProducer); public void setContent(Node newView, AnimationProducer transition, Duration duration, Interpolator interpolator) {
super.setContent(newView, transition, duration, interpolator);
if (content instanceof Region) { if (newView instanceof Region region) {
((Region) content).setMinSize(0, 0); region.setMinSize(0, 0);
FXUtils.setOverflowHidden((Region) content); FXUtils.setOverflowHidden(region);
} }
} }
@@ -181,7 +189,7 @@ public class Navigator extends TransitionPane {
this.onNavigated.set(onNavigated); this.onNavigated.set(onNavigated);
} }
private ObjectProperty<EventHandler<NavigationEvent>> onNavigated = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigated") { private final ObjectProperty<EventHandler<NavigationEvent>> onNavigated = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigated") {
@Override @Override
protected void invalidated() { protected void invalidated() {
setEventHandler(NavigationEvent.NAVIGATED, get()); setEventHandler(NavigationEvent.NAVIGATED, get());
@@ -200,14 +208,14 @@ public class Navigator extends TransitionPane {
this.onNavigating.set(onNavigating); this.onNavigating.set(onNavigating);
} }
private ObjectProperty<EventHandler<NavigationEvent>> onNavigating = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigating") { private final ObjectProperty<EventHandler<NavigationEvent>> onNavigating = new SimpleObjectProperty<EventHandler<NavigationEvent>>(this, "onNavigating") {
@Override @Override
protected void invalidated() { protected void invalidated() {
setEventHandler(NavigationEvent.NAVIGATING, get()); setEventHandler(NavigationEvent.NAVIGATING, get());
} }
}; };
public static class NavigationEvent extends Event { public static final class NavigationEvent extends Event {
public static final EventType<NavigationEvent> EXITED = new EventType<>("EXITED"); public static final EventType<NavigationEvent> EXITED = new EventType<>("EXITED");
public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED"); public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED");
public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING"); public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING");

View File

@@ -18,7 +18,6 @@
package org.jackhuang.hmcl.ui.construct; package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXRippler; import com.jfoenix.controls.JFXRippler;
import javafx.animation.Interpolator;
import javafx.animation.Transition; import javafx.animation.Transition;
import javafx.beans.DefaultProperty; import javafx.beans.DefaultProperty;
import javafx.beans.InvalidationListener; import javafx.beans.InvalidationListener;
@@ -40,6 +39,7 @@ import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import javafx.util.Duration; import javafx.util.Duration;
import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.animation.AnimationUtils;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import java.util.List; import java.util.List;
@@ -109,7 +109,7 @@ public class RipplerContainer extends StackPane {
setOnMouseEntered(e -> new Transition() { setOnMouseEntered(e -> new Transition() {
{ {
setCycleDuration(DURATION); setCycleDuration(DURATION);
setInterpolator(Interpolator.EASE_IN); setInterpolator(Motion.EASE_IN);
} }
@Override @Override
@@ -121,7 +121,7 @@ public class RipplerContainer extends StackPane {
setOnMouseExited(e -> new Transition() { setOnMouseExited(e -> new Transition() {
{ {
setCycleDuration(DURATION); setCycleDuration(DURATION);
setInterpolator(Interpolator.EASE_OUT); setInterpolator(Motion.EASE_OUT);
} }
@Override @Override

View File

@@ -48,8 +48,8 @@ public class TabHeader extends Control implements TabControl, PageAware {
} }
} }
private ObservableList<Tab<?>> tabs = FXCollections.observableArrayList(); private final ObservableList<Tab<?>> tabs = FXCollections.observableArrayList();
private ObjectProperty<Side> side = new SimpleObjectProperty<>(Side.TOP); private final ObjectProperty<Side> side = new SimpleObjectProperty<>(Side.TOP);
@Override @Override
public ObservableList<Tab<?>> getTabs() { public ObservableList<Tab<?>> getTabs() {
@@ -170,14 +170,14 @@ public class TabHeader extends Control implements TabControl, PageAware {
this.selectedTab = control.getSelectionModel().getSelectedItem(); this.selectedTab = control.getSelectionModel().getSelectedItem();
} }
protected class HeaderContainer extends StackPane { protected final class HeaderContainer extends StackPane {
private Timeline timeline; private Timeline timeline;
private StackPane selectedTabLine; private final StackPane selectedTabLine;
private HeadersRegion headersRegion; private final HeadersRegion headersRegion;
private Scale scale = new Scale(1, 1, 0, 0); private final Scale scale = new Scale(1, 1, 0, 0);
private Rotate rotate = new Rotate(0, 0, 1); private final Rotate rotate = new Rotate(0, 0, 1);
private double selectedTabLineOffset; @SuppressWarnings({"FieldCanBeLocal", "unused"})
private ObservableList<Node> binding; private final ObservableList<Node> binding;
public HeaderContainer() { public HeaderContainer() {
getStyleClass().add("tab-header-area"); getStyleClass().add("tab-header-area");
@@ -232,13 +232,12 @@ public class TabHeader extends Control implements TabControl, PageAware {
protected void invalidated() { protected void invalidated() {
super.invalidated(); super.invalidated();
switch (get()) { action = switch (get()) {
case TOP: action = new Top(); break; case TOP -> new Top();
case BOTTOM: action = new Bottom(); break; case BOTTOM -> new Bottom();
case LEFT: action = new Left(); break; case LEFT -> new Left();
case RIGHT: action = new Right(); break; case RIGHT -> new Right();
default: throw new InternalError(); };
}
} }
}; };
@@ -269,7 +268,7 @@ public class TabHeader extends Control implements TabControl, PageAware {
action.layoutChildren(); action.layoutChildren();
} }
protected void animateSelectionLine() { private void animateSelectionLine() {
action.animateSelectionLine(); action.animateSelectionLine();
} }
@@ -325,7 +324,6 @@ public class TabHeader extends Control implements TabControl, PageAware {
double oldWidth = lineWidth * oldScaleX; double oldWidth = lineWidth * oldScaleX;
double oldTransX = selectedTabLine.getTranslateX(); double oldTransX = selectedTabLine.getTranslateX();
double newScaleX = newWidth * oldScaleX / oldWidth; double newScaleX = newWidth * oldScaleX / oldWidth;
selectedTabLineOffset = newTransX;
// newTransX += offsetStart * (double)this.direction; // newTransX += offsetStart * (double)this.direction;
double transDiff = newTransX - oldTransX; double transDiff = newTransX - oldTransX;
if (transDiff < 0.0D) { if (transDiff < 0.0D) {
@@ -465,7 +463,6 @@ public class TabHeader extends Control implements TabControl, PageAware {
double oldHeight = lineHeight * oldScaleY; double oldHeight = lineHeight * oldScaleY;
double oldTransY = selectedTabLine.getTranslateY(); double oldTransY = selectedTabLine.getTranslateY();
double newScaleY = newHeight * oldScaleY / oldHeight; double newScaleY = newHeight * oldScaleY / oldHeight;
selectedTabLineOffset = newTransY;
// newTransY += offsetStart * (double)this.direction; // newTransY += offsetStart * (double)this.direction;
double transDiff = newTransY - oldTransY; double transDiff = newTransY - oldTransY;
if (transDiff < 0.0D) { if (transDiff < 0.0D) {

View File

@@ -41,6 +41,7 @@ import javafx.stage.StageStyle;
import javafx.util.Duration; import javafx.util.Duration;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.animation.AnimationUtils; 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.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -82,19 +83,19 @@ public class Decorator extends Control {
if (playRestoreMinimizeAnimation && !iconified) { if (playRestoreMinimizeAnimation && !iconified) {
playRestoreMinimizeAnimation = false; playRestoreMinimizeAnimation = false;
Timeline timeline = new Timeline( Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0), new KeyFrame(Duration.ZERO,
new KeyValue(this.opacityProperty(), 0, FXUtils.EASE), new KeyValue(this.opacityProperty(), 0, Motion.EASE),
new KeyValue(this.translateYProperty(), 200, FXUtils.EASE), new KeyValue(this.translateYProperty(), 200, Motion.EASE),
new KeyValue(this.scaleXProperty(), 0.4, FXUtils.EASE), new KeyValue(this.scaleXProperty(), 0.4, Motion.EASE),
new KeyValue(this.scaleYProperty(), 0.4, FXUtils.EASE), new KeyValue(this.scaleYProperty(), 0.4, Motion.EASE),
new KeyValue(this.scaleZProperty(), 0.4, FXUtils.EASE) new KeyValue(this.scaleZProperty(), 0.4, Motion.EASE)
), ),
new KeyFrame(Duration.millis(200), new KeyFrame(Motion.SHORT4,
new KeyValue(this.opacityProperty(), 1, FXUtils.EASE), new KeyValue(this.opacityProperty(), 1, Motion.EASE),
new KeyValue(this.translateYProperty(), 0, FXUtils.EASE), new KeyValue(this.translateYProperty(), 0, Motion.EASE),
new KeyValue(this.scaleXProperty(), 1, FXUtils.EASE), new KeyValue(this.scaleXProperty(), 1, Motion.EASE),
new KeyValue(this.scaleYProperty(), 1, FXUtils.EASE), new KeyValue(this.scaleYProperty(), 1, Motion.EASE),
new KeyValue(this.scaleZProperty(), 1, FXUtils.EASE) new KeyValue(this.scaleZProperty(), 1, Motion.EASE)
) )
); );
timeline.play(); timeline.play();
@@ -277,19 +278,19 @@ public class Decorator extends Control {
if (AnimationUtils.playWindowAnimation() && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) { if (AnimationUtils.playWindowAnimation() && OperatingSystem.CURRENT_OS != OperatingSystem.MACOS) {
playRestoreMinimizeAnimation = true; playRestoreMinimizeAnimation = true;
Timeline timeline = new Timeline( Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0), new KeyFrame(Duration.ZERO,
new KeyValue(this.opacityProperty(), 1, FXUtils.EASE), new KeyValue(this.opacityProperty(), 1, Motion.EASE),
new KeyValue(this.translateYProperty(), 0, FXUtils.EASE), new KeyValue(this.translateYProperty(), 0, Motion.EASE),
new KeyValue(this.scaleXProperty(), 1, FXUtils.EASE), new KeyValue(this.scaleXProperty(), 1, Motion.EASE),
new KeyValue(this.scaleYProperty(), 1, FXUtils.EASE), new KeyValue(this.scaleYProperty(), 1, Motion.EASE),
new KeyValue(this.scaleZProperty(), 1, FXUtils.EASE) new KeyValue(this.scaleZProperty(), 1, Motion.EASE)
), ),
new KeyFrame(Duration.millis(200), new KeyFrame(Motion.SHORT4,
new KeyValue(this.opacityProperty(), 0, FXUtils.EASE), new KeyValue(this.opacityProperty(), 0, Motion.EASE),
new KeyValue(this.translateYProperty(), 200, FXUtils.EASE), new KeyValue(this.translateYProperty(), 200, Motion.EASE),
new KeyValue(this.scaleXProperty(), 0.4, FXUtils.EASE), new KeyValue(this.scaleXProperty(), 0.4, Motion.EASE),
new KeyValue(this.scaleYProperty(), 0.4, FXUtils.EASE), new KeyValue(this.scaleYProperty(), 0.4, Motion.EASE),
new KeyValue(this.scaleZProperty(), 0.4, FXUtils.EASE) new KeyValue(this.scaleZProperty(), 0.4, Motion.EASE)
) )
); );
timeline.setOnFinished(event -> primaryStage.setIconified(true)); timeline.setOnFinished(event -> primaryStage.setIconified(true));

View File

@@ -1,90 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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<KeyFrame> 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<KeyFrame> 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;
}
}

View File

@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXDialog; import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXSnackbar; import com.jfoenix.controls.JFXSnackbar;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame; import javafx.animation.KeyFrame;
import javafx.animation.KeyValue; import javafx.animation.KeyValue;
import javafx.animation.Timeline; 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.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane; import org.jackhuang.hmcl.ui.account.AddAuthlibInjectorServerPane;
import org.jackhuang.hmcl.ui.animation.AnimationUtils; import org.jackhuang.hmcl.ui.animation.*;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.construct.DialogAware; import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.DialogCloseEvent;
import org.jackhuang.hmcl.ui.construct.Navigator; import org.jackhuang.hmcl.ui.construct.Navigator;
@@ -90,16 +90,16 @@ public class DecoratorController {
if (AnimationUtils.playWindowAnimation()) { if (AnimationUtils.playWindowAnimation()) {
Timeline timeline = new Timeline( Timeline timeline = new Timeline(
new KeyFrame(Duration.millis(0), new KeyFrame(Duration.millis(0),
new KeyValue(decorator.opacityProperty(), 1, FXUtils.EASE), new KeyValue(decorator.opacityProperty(), 1, Motion.EASE),
new KeyValue(decorator.scaleXProperty(), 1, FXUtils.EASE), new KeyValue(decorator.scaleXProperty(), 1, Motion.EASE),
new KeyValue(decorator.scaleYProperty(), 1, FXUtils.EASE), new KeyValue(decorator.scaleYProperty(), 1, Motion.EASE),
new KeyValue(decorator.scaleZProperty(), 0.3, FXUtils.EASE) new KeyValue(decorator.scaleZProperty(), 0.3, Motion.EASE)
), ),
new KeyFrame(Duration.millis(200), new KeyFrame(Duration.millis(200),
new KeyValue(decorator.opacityProperty(), 0, FXUtils.EASE), new KeyValue(decorator.opacityProperty(), 0, Motion.EASE),
new KeyValue(decorator.scaleXProperty(), 0.8, FXUtils.EASE), new KeyValue(decorator.scaleXProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.scaleYProperty(), 0.8, FXUtils.EASE), new KeyValue(decorator.scaleYProperty(), 0.8, Motion.EASE),
new KeyValue(decorator.scaleZProperty(), 0.8, FXUtils.EASE) new KeyValue(decorator.scaleZProperty(), 0.8, Motion.EASE)
) )
); );
timeline.setOnFinished(event -> Launcher.stopApplication()); timeline.setOnFinished(event -> Launcher.stopApplication());
@@ -364,10 +364,57 @@ public class DecoratorController {
// ==== Navigation ==== // ==== 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<KeyFrame> 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) { public void navigate(Node node) {
navigator.navigate(node, animation); navigator.navigate(node, ANIMATION);
} }
private void close() { private void close() {
@@ -458,7 +505,9 @@ public class DecoratorController {
Platform.runLater(() -> showDialog(node)); Platform.runLater(() -> showDialog(node));
return; return;
} }
dialog = new JFXDialog(); dialog = new JFXDialog(AnimationUtils.isAnimationEnabled()
? JFXDialog.DialogTransition.CENTER
: JFXDialog.DialogTransition.NONE);
dialogPane = new JFXDialogPane(); dialogPane = new JFXDialogPane();
dialog.setContent(dialogPane); dialog.setContent(dialogPane);

View File

@@ -1,57 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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());
}
}

View File

@@ -18,6 +18,10 @@
package org.jackhuang.hmcl.ui.decorator; package org.jackhuang.hmcl.ui.decorator;
import com.jfoenix.controls.JFXButton; 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.InvalidationListener;
import javafx.beans.WeakInvalidationListener; import javafx.beans.WeakInvalidationListener;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
@@ -39,12 +43,13 @@ import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle; import javafx.scene.shape.Rectangle;
import javafx.stage.Stage; import javafx.stage.Stage;
import javafx.util.Duration;
import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Theme; import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; 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.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.Motion;
import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.Navigation;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
@@ -205,16 +210,13 @@ public class DecoratorSkin extends SkinBase<Decorator> {
if (s == null) return; if (s == null) return;
Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode()); Node node = createNavBar(skinnable, s.getLeftPaneWidth(), s.isBackable(), skinnable.canCloseProperty().get(), skinnable.showCloseAsHomeProperty().get(), s.isRefreshable(), s.getTitle(), s.getTitleNode());
if (s.isAnimate()) { if (s.isAnimate()) {
AnimationProducer animation; TransitionPane.AnimationProducer animation = switch (skinnable.getNavigationDirection()) {
if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.NEXT) { case NEXT -> NavBarAnimations.NEXT;
animation = ContainerAnimations.SWIPE_LEFT_FADE_SHORT; case PREVIOUS -> NavBarAnimations.PREVIOUS;
} else if (skinnable.getNavigationDirection() == Navigation.NavigationDirection.PREVIOUS) { default -> ContainerAnimations.FADE;
animation = ContainerAnimations.SWIPE_RIGHT_FADE_SHORT; };
} else {
animation = ContainerAnimations.FADE;
}
skinnable.setNavigationDirection(Navigation.NavigationDirection.START); skinnable.setNavigationDirection(Navigation.NavigationDirection.START);
navBarPane.setContent(node, animation); navBarPane.setContent(node, animation, Motion.SHORT4);
} else { } else {
navBarPane.getChildren().setAll(node); navBarPane.getChildren().setAll(node);
} }
@@ -500,4 +502,78 @@ public class DecoratorSkin extends SkinBase<Decorator> {
} }
} }
} }
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;
}
};
}
} }

View File

@@ -1,73 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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 <https://www.gnu.org/licenses/>.
*/
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<TabHeader.Tab<?>> tabs = FXCollections.observableArrayList();
@Override
public ObservableList<TabHeader.Tab<?>> getTabs() {
return tabs;
}
private final ObjectProperty<SingleSelectionModel<TabHeader.Tab<?>>> selectionModel = new SimpleObjectProperty<>(this, "selectionModel", new TabControl.TabControlSelectionModel(this));
public SingleSelectionModel<TabHeader.Tab<?>> getSelectionModel() {
return selectionModel.get();
}
public ObjectProperty<SingleSelectionModel<TabHeader.Tab<?>>> selectionModelProperty() {
return selectionModel;
}
public void setSelectionModel(SingleSelectionModel<TabHeader.Tab<?>> selectionModel) {
this.selectionModel.set(selectionModel);
}
}

View File

@@ -17,6 +17,7 @@
*/ */
package org.jackhuang.hmcl.ui.decorator; package org.jackhuang.hmcl.ui.decorator;
import javafx.animation.Interpolator;
import javafx.beans.binding.Bindings; import javafx.beans.binding.Bindings;
import javafx.beans.property.*; import javafx.beans.property.*;
import javafx.scene.Node; import javafx.scene.Node;
@@ -24,7 +25,7 @@ import javafx.scene.control.Control;
import javafx.scene.control.Skin; import javafx.scene.control.Skin;
import javafx.scene.layout.Region; import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane; 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.animation.TransitionPane;
import org.jackhuang.hmcl.ui.wizard.Refreshable; import org.jackhuang.hmcl.ui.wizard.Refreshable;
@@ -36,8 +37,8 @@ public abstract class DecoratorTransitionPage extends Control implements Decorat
private Node currentPage; private Node currentPage;
protected final TransitionPane transitionPane = new TransitionPane(); protected final TransitionPane transitionPane = new TransitionPane();
protected void navigate(Node page, AnimationProducer animation) { protected void navigate(Node page, TransitionPane.AnimationProducer animation, Duration duration, Interpolator interpolator) {
transitionPane.setContent(currentPage = page, animation); transitionPane.setContent(currentPage = page, animation, duration, interpolator);
} }
protected void onNavigating(Node from) { protected void onNavigating(Node from) {

View File

@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.ui.decorator;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.SkinBase; import javafx.scene.control.SkinBase;
import org.jackhuang.hmcl.task.Task; 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.Navigator;
import org.jackhuang.hmcl.ui.construct.PageCloseEvent; import org.jackhuang.hmcl.ui.construct.PageCloseEvent;
import org.jackhuang.hmcl.ui.wizard.*; import org.jackhuang.hmcl.ui.wizard.*;
@@ -74,7 +75,7 @@ public class DecoratorWizardDisplayer extends DecoratorTransitionPage implements
@Override @Override
public void navigateTo(Node page, Navigation.NavigationDirection nav) { public void navigateTo(Node page, Navigation.NavigationDirection nav) {
displayer.navigateTo(page, nav); displayer.navigateTo(page, nav);
navigate(page, nav.getAnimation()); navigate(page, nav.getAnimation(), Motion.SHORT4, Motion.EASE);
String prefix = category == null ? "" : category + " - "; String prefix = category == null ? "" : category + " - ";

View File

@@ -33,6 +33,7 @@ import javafx.scene.layout.BorderPane;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.InstallerItem; import org.jackhuang.hmcl.ui.InstallerItem;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane; 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.WizardController;
import org.jackhuang.hmcl.ui.wizard.WizardPage; import org.jackhuang.hmcl.ui.wizard.WizardPage;
import org.jackhuang.hmcl.util.SettingsMap; 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)) 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(() -> { library.setOnRemove(() -> {
controller.getSettings().remove(libraryId); controller.getSettings().remove(libraryId);

View File

@@ -36,7 +36,6 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder; 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.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox; import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -105,9 +104,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
Profiles.registerVersionsListener(this::loadVersions); Profiles.registerVersionsListener(this::loadVersions);
tab.select(newGameTab); tab.select(newGameTab);
FXUtils.onChangeAndOperate(tab.getSelectionModel().selectedItemProperty(), newValue -> { transitionPane.bindTabHeader(tab);
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
});
AdvancedListBox sideBar = new AdvancedListBox() AdvancedListBox sideBar = new AdvancedListBox()
.startCategory(i18n("download.game").toUpperCase(Locale.ROOT)) .startCategory(i18n("download.game").toUpperCase(Locale.ROOT))

View File

@@ -23,7 +23,6 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles; import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; 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.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
@@ -60,10 +59,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
tab.select(gameTab); tab.select(gameTab);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null)); addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null));
transitionPane.setContent(gameTab.getNode(), ContainerAnimations.NONE); transitionPane.bindTabHeader(tab);
FXUtils.onChange(tab.getSelectionModel().selectedItemProperty(), newValue -> {
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
});
AdvancedListBox sideBar = new AdvancedListBox() AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerTab(tab, gameTab, i18n("settings.type.global.manage"), SVG.STADIA_CONTROLLER) .addNavigationDrawerTab(tab, gameTab, i18n("settings.type.global.manage"), SVG.STADIA_CONTROLLER)

View File

@@ -535,7 +535,7 @@ public class TerracottaControllerPage extends StackPane {
children.addAll(nodesProperty); 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()); listener.changed(UI_STATE, null, UI_STATE.get());
holder.add(listener); holder.add(listener);
@@ -718,7 +718,7 @@ public class TerracottaControllerPage extends StackPane {
pane.getChildren().add(item); pane.getChildren().add(item);
} }
this.transition.setContent(pane, ContainerAnimations.SWIPE_LEFT_FADE_SHORT); this.transition.setContent(pane, ContainerAnimations.SLIDE_UP_FADE_IN);
} }
} }
} }

View File

@@ -31,7 +31,6 @@ import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; 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.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.AdvancedListBox; import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.PageAware; import org.jackhuang.hmcl.ui.construct.PageAware;
@@ -58,11 +57,7 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
statusPage.setNodeSupplier(TerracottaControllerPage::new); statusPage.setNodeSupplier(TerracottaControllerPage::new);
tab = new TabHeader(statusPage); tab = new TabHeader(statusPage);
tab.select(statusPage); tab.select(statusPage);
transitionPane.bindTabHeader(tab);
transitionPane.setContent(statusPage.getNode(), ContainerAnimations.NONE);
FXUtils.onChange(tab.getSelectionModel().selectedItemProperty(), newValue -> {
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
});
BorderPane left = new BorderPane(); BorderPane left = new BorderPane();
FXUtils.setLimitWidth(left, 200); FXUtils.setLimitWidth(left, 200);

View File

@@ -37,7 +37,6 @@ import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.WeakListenerHolder; 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.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
@@ -77,10 +76,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
tab.select(versionSettingsTab); tab.select(versionSettingsTab);
transitionPane.setContent(versionSettingsTab.getNode(), ContainerAnimations.NONE); transitionPane.bindTabHeader(tab);
FXUtils.onChange(tab.getSelectionModel().selectedItemProperty(), newValue -> {
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE);
});
listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST)); listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST));
} }

View File

@@ -28,7 +28,6 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; 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.animation.TransitionPane;
import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
@@ -73,9 +72,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
datapackTab.setNodeSupplier(() -> new DatapackListPage(this)); datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
header.select(worldInfoTab); header.select(worldInfoTab);
transitionPane.setContent(worldInfoTab.getNode(), ContainerAnimations.NONE); transitionPane.bindTabHeader(header);
FXUtils.onChange(header.getSelectionModel().selectedItemProperty(), newValue ->
transitionPane.setContent(newValue.getNode(), ContainerAnimations.FADE));
setCenter(transitionPane); setCenter(transitionPane);
BorderPane left = new BorderPane(); BorderPane left = new BorderPane();

View File

@@ -40,11 +40,9 @@ public interface Navigation {
enum NavigationDirection { enum NavigationDirection {
START(ContainerAnimations.NONE), START(ContainerAnimations.NONE),
PREVIOUS(ContainerAnimations.SWIPE_RIGHT), PREVIOUS(ContainerAnimations.BACKWARD),
NEXT(ContainerAnimations.SWIPE_LEFT), NEXT(ContainerAnimations.FORWARD),
FINISH(ContainerAnimations.SWIPE_LEFT), FINISH(ContainerAnimations.FORWARD);
IN(ContainerAnimations.ZOOM_IN),
OUT(ContainerAnimations.ZOOM_OUT);
private final ContainerAnimations animation; private final ContainerAnimations animation;

View File

@@ -83,6 +83,10 @@ public class WizardController implements Navigation {
} }
public void onNext(Node page) { public void onNext(Node page) {
onNext(page, NavigationDirection.NEXT);
}
public void onNext(Node page, NavigationDirection direction) {
pages.push(page); pages.push(page);
if (stopped) { // navigatingTo may stop this wizard. if (stopped) { // navigatingTo may stop this wizard.
@@ -93,11 +97,15 @@ public class WizardController implements Navigation {
((WizardPage) page).onNavigate(settings); ((WizardPage) page).onNavigate(settings);
LOG.info("Navigating to " + page + ", pages: " + pages); LOG.info("Navigating to " + page + ", pages: " + pages);
displayer.navigateTo(page, NavigationDirection.NEXT); displayer.navigateTo(page, direction);
} }
@Override @Override
public void onPrev(boolean cleanUp) { public void onPrev(boolean cleanUp) {
onPrev(cleanUp, NavigationDirection.PREVIOUS);
}
public void onPrev(boolean cleanUp, NavigationDirection direction) {
if (!canPrev()) { if (!canPrev()) {
if (provider.cancelIfCannotGoBack()) { if (provider.cancelIfCannotGoBack()) {
onCancel(); onCancel();
@@ -116,7 +124,7 @@ public class WizardController implements Navigation {
((WizardPage) prevPage).onNavigate(settings); ((WizardPage) prevPage).onNavigate(settings);
LOG.info("Navigating to " + prevPage + ", pages: " + pages); LOG.info("Navigating to " + prevPage + ", pages: " + pages);
displayer.navigateTo(prevPage, NavigationDirection.PREVIOUS); displayer.navigateTo(prevPage, direction);
} }
@Override @Override