diff --git a/.idea/runConfigurations/ResourcesLauncher.xml b/.idea/runConfigurations/ResourcesLauncher.xml
new file mode 100644
index 00000000..4237e961
--- /dev/null
+++ b/.idea/runConfigurations/ResourcesLauncher.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/AnimationFactory.java b/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/AnimationFactory.java
index 114a29b2..108f78d5 100644
--- a/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/AnimationFactory.java
+++ b/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/AnimationFactory.java
@@ -18,173 +18,218 @@
package io.github.palexdev.mfxeffects.animations;
+import io.github.palexdev.mfxeffects.animations.Animations.KeyFrames;
+import io.github.palexdev.mfxeffects.animations.Animations.TimelineBuilder;
import javafx.animation.Interpolator;
-import javafx.animation.KeyFrame;
-import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.scene.Node;
+import javafx.scene.Parent;
import javafx.util.Duration;
/**
* Convenience factory for various animations applied to {@code Nodes}.
*
+ * @see #extraOffset
* @see Timeline
*/
public enum AnimationFactory {
FADE_IN {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.opacityProperty(), 0, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.opacityProperty(), 1.0, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.opacityProperty(), 0.0))
+ .add(KeyFrames.of(millis, node.opacityProperty(), 1.0, i))
+ .getAnimation();
}
},
FADE_OUT {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.opacityProperty(), 1.0, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.opacityProperty(), 0, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.opacityProperty(), 1.0))
+ .add(KeyFrames.of(millis, node.opacityProperty(), 0.0, i))
+ .getAnimation();
}
},
SLIDE_IN_BOTTOM {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.translateYProperty(), -node.getBoundsInParent().getHeight() * 2, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.translateYProperty(), 0, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ double distance = computeDistanceBottom(node);
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.translateYProperty(), distance))
+ .add(KeyFrames.of(millis, node.translateYProperty(), 0, i))
+ .getAnimation();
}
},
SLIDE_OUT_BOTTOM {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.translateYProperty(), 0, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.translateYProperty(), node.getBoundsInParent().getHeight() * 2, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ double distance = computeDistanceBottom(node);
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.translateYProperty(), 0))
+ .add(KeyFrames.of(millis, node.translateYProperty(), distance, i))
+ .getAnimation();
}
},
SLIDE_IN_LEFT {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.translateXProperty(), -node.getBoundsInParent().getWidth() * 2, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.translateXProperty(), 0, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ double distance = computeDistanceLeft(node);
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.translateXProperty(), -distance))
+ .add(KeyFrames.of(millis, node.translateXProperty(), 0, i))
+ .getAnimation();
}
},
SLIDE_OUT_LEFT {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.translateXProperty(), 0, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.translateXProperty(), -node.getBoundsInParent().getWidth() * 2, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ double distance = computeDistanceLeft(node);
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.translateXProperty(), 0))
+ .add(KeyFrames.of(millis, node.translateXProperty(), -distance, i))
+ .getAnimation();
}
},
SLIDE_IN_RIGHT {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.translateXProperty(), node.getBoundsInParent().getWidth() * 2, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.translateXProperty(), 0, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ double distance = computeDistanceRight(node);
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.translateXProperty(), distance))
+ .add(KeyFrames.of(millis, node.translateXProperty(), 0, i))
+ .getAnimation();
}
},
SLIDE_OUT_RIGHT {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.translateXProperty(), 0, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.translateXProperty(), node.getBoundsInParent().getWidth() * 2, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ double distance = computeDistanceRight(node);
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.translateXProperty(), 0))
+ .add(KeyFrames.of(millis, node.translateXProperty(), distance, i))
+ .getAnimation();
}
},
SLIDE_IN_TOP {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.translateYProperty(), node.getBoundsInParent().getHeight() * 2, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.translateYProperty(), 0, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ double distance = computeDistanceTop(node);
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.translateYProperty(), distance))
+ .add(KeyFrames.of(millis, node.translateYProperty(), 0, i))
+ .getAnimation();
}
},
SLIDE_OUT_TOP {
@Override
- public Timeline build(Node node, double durationMillis, Interpolator i) {
- AnimationFactory.resetNode(node);
- KeyValue keyValue1 = new KeyValue(node.translateYProperty(), 0, i);
- KeyFrame keyFrame1 = new KeyFrame(Duration.ZERO, keyValue1);
-
- KeyValue keyValue2 = new KeyValue(node.translateYProperty(), -node.getBoundsInParent().getHeight() * 2, i);
- KeyFrame keyFrame2 = new KeyFrame(Duration.millis(durationMillis), keyValue2);
-
- return new Timeline(keyFrame1, keyFrame2);
+ public Timeline build(Node node, double millis, Interpolator i) {
+ double distance = computeDistanceTop(node);
+ return TimelineBuilder.build()
+ .add(KeyFrames.of(0, node.translateYProperty(), 0))
+ .add(KeyFrames.of(millis, node.translateYProperty(), distance, i))
+ .getAnimation();
}
};
public static final Interpolator INTERPOLATOR_V1 = Interpolator.SPLINE(0.25, 0.1, 0.25, 1);
public static final Interpolator INTERPOLATOR_V2 = Interpolator.SPLINE(0.0825D, 0.3025D, 0.0875D, 0.9975D);
- private static void resetNode(Node node) {
- if (node != null) {
- node.setTranslateX(0);
- node.setTranslateY(0);
- }
+ /**
+ * This special variable is used in slide animations when the "travel distance" is computed.
+ * This extra offset is added to the computed value to ensure the node is outside the parent, for a smooth animation.
+ */
+ @SuppressWarnings("NonFinalFieldInEnum")
+ public static double extraOffset = 5.0;
+
+ /**
+ * Computes the distance between the node and the left side of its parent by using its
+ * {@link Node#boundsInParentProperty()}. This distance ensures the node is going to be outside/inside the parent
+ * towards the animation's ending.
+ *
+ * @see #extraOffset
+ */
+ public double computeDistanceLeft(Node node) {
+ double w = node.getBoundsInParent().getWidth();
+ return node.getBoundsInParent().getMinX() + w + extraOffset;
+ }
+
+ /**
+ * Computes the distance between the node and the right side of its parent. For this computation the
+ * {@link Node#parentProperty()} must not return a {@code null} value (the node must be child of some other node).
+ *
+ * If the parent is {@code null} a 'fallback' value is returned, that is the width of the node plus the extra offset.
+ *
+ * Otherwise, the value is computed like this:
+ * {@code parent.getLayoutBounds().getWidth() - node.getBoundsInParent().getMaxX() + node.getBoundsInParent().getWidth() + extraOffset}
+ *
+ * @see #extraOffset
+ */
+ public double computeDistanceRight(Node node) {
+ double w = node.getBoundsInParent().getWidth();
+ Parent parent = node.getParent();
+ if (parent == null) return w + extraOffset;
+ return parent.getLayoutBounds().getWidth() - node.getBoundsInParent().getMaxX() + w + extraOffset;
+ }
+
+ /**
+ * Computes the distance between the node and the top side of its parent by using its
+ * {@link Node#boundsInParentProperty()}. This distance ensures the node is going to be outside/inside the parent
+ * towards the animation's ending.
+ *
+ * @see #extraOffset
+ */
+ public double computeDistanceTop(Node node) {
+ double h = node.getBoundsInParent().getHeight();
+ return node.getBoundsInParent().getMinY() + h + extraOffset;
+ }
+
+ /**
+ * Computes the distance between the node and the bottom side of its parent. For this computation the
+ * {@link Node#parentProperty()} must not return a {@code null} value (the node must be child of some other node).
+ *
+ * If the parent is {@code null} a 'fallback' value is returned, that is the height of the node plus the extra offset.
+ *
+ * Otherwise, the value is computed like this:
+ * {@code parent.getLayoutBounds().getHeight() - node.getBoundsInParent().getMaxY() + node.getBoundsInParent().getHeight() + extraOffset}
+ *
+ * @see #extraOffset
+ */
+ public double computeDistanceBottom(Node node) {
+ double h = node.getBoundsInParent().getHeight();
+ Parent parent = node.getParent();
+ if (parent == null) return h + extraOffset;
+ return parent.getLayoutBounds().getHeight() - node.getBoundsInParent().getMaxY() + h + extraOffset;
}
/**
* Calls {@link #build(Node, double, Interpolator)} with {@link #INTERPOLATOR_V1} as the default interpolator.
*/
- public Timeline build(Node node, double durationMillis) {
- return build(node, durationMillis, INTERPOLATOR_V1);
+ public Timeline build(Node node, double millis) {
+ return build(node, millis, INTERPOLATOR_V1);
+ }
+
+ /**
+ * Calls {@link #build(Node, double)} with the given duration converted to milliseconds.
+ */
+ public Timeline build(Node node, Duration duration) {
+ return build(node, duration.toMillis());
+ }
+
+ /**
+ * Calls {@link #build(Node, double, Interpolator)} with the given duration converted to milliseconds and the given
+ * interpolator.
+ */
+ public Timeline build(Node node, Duration duration, Interpolator i) {
+ return build(node, duration.toMillis(), i);
}
/**
* Each enum constant will produce a {@link Timeline} with the given parameters.
*
- * @param node the {@link Node} on which perform the animation
- * @param durationMillis the duration of the animation in milliseconds
- * @param i the {@link Interpolator} used by the animations
+ * @param node the {@link Node} on which perform the animation
+ * @param millis the duration of the animation in milliseconds
+ * @param i the {@link Interpolator} used by the animations
*/
- public abstract Timeline build(Node node, double durationMillis, Interpolator i);
+ public abstract Timeline build(Node node, double millis, Interpolator i);
}
diff --git a/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/Animations.java b/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/Animations.java
index dce88b0c..3500c0e3 100644
--- a/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/Animations.java
+++ b/modules/effects/src/main/java/io/github/palexdev/mfxeffects/animations/Animations.java
@@ -21,6 +21,8 @@
import io.github.palexdev.mfxeffects.beans.AnimationsData;
import io.github.palexdev.mfxeffects.enums.Interpolators;
import javafx.animation.*;
+import javafx.beans.InvalidationListener;
+import javafx.beans.Observable;
import javafx.beans.binding.BooleanExpression;
import javafx.beans.value.WritableValue;
import javafx.event.ActionEvent;
@@ -130,6 +132,54 @@ public static AbstractBuilder transitionText(Text text, double millis, String ne
return transitionText(text, Duration.millis(millis), nexText);
}
+ /**
+ * Allows to perform a given action as a {@link Runnable} as soon as the given {@link Animation} reaches the given
+ * {@link Animation.Status}. This is simply done by attaching an {@link InvalidationListener} to the animation's
+ * {@link Animation#statusProperty()}.
+ *
+ * Trivia
+ *
+ * {@link Animation}s have a feature that allows user to specify an action to perform as soon as it ends, I'm talking
+ * about the {@link Animation#onFinishedProperty()}. There is an issue though. If the animation is stopped,
+ * {@link Animation#stop()}, the action won't trigger. 'Stop' and 'Finish' are two different states, but JavaFX devs
+ * didn't make such distinction explicit, in fact when an animation ends, its status property will be set to
+ * {@link Animation.Status#STOPPED}.
+ *
+ * It may be useful in some occasions to perform an action once the animation stops, rather than just on finish.
+ * And so I created this generic method to fill this gap, working on any state you need.
+ *
+ * @param oneShot specifies whether the listener should be removed after the first execution
+ */
+ public static void onStatus(Animation animation, Animation.Status status, Runnable action, boolean oneShot) {
+ if (animation == null) return;
+ InvalidationListener l = new InvalidationListener() {
+ @Override
+ public void invalidated(Observable observable) {
+ if (animation.getStatus() == status) {
+ action.run();
+ if (oneShot) animation.statusProperty().removeListener(this);
+ }
+ }
+ };
+ animation.statusProperty().addListener(l);
+ }
+
+ /**
+ * Convenience method for {@link #onStatus(Animation, Animation.Status, Runnable, boolean)}, performs the given
+ * action on {@link Animation.Status#PAUSED}.
+ */
+ public static void onPaused(Animation animation, Runnable action, boolean oneShot) {
+ onStatus(animation, Animation.Status.PAUSED, action, oneShot);
+ }
+
+ /**
+ * Convenience method for {@link #onStatus(Animation, Animation.Status, Runnable, boolean)}, performs the given
+ * action on {@link Animation.Status#STOPPED}.
+ */
+ public static void onStopped(Animation animation, Runnable action, boolean oneShot) {
+ onStatus(animation, Animation.Status.STOPPED, action, oneShot);
+ }
+
/**
* @return true if the given animation status is RUNNING, otherwise false
*/
@@ -236,6 +286,15 @@ public AbstractBuilder add(KeyFrame... keyFrames) {
return this;
}
+ /**
+ * If the given condition returns true, then a new {@link Timeline} is built with the given keyframe and added to
+ * the 'main' animation by calling {@link #addAnimation(Animation)}.
+ */
+ public AbstractBuilder addConditional(Supplier condition, KeyFrame keyFrame) {
+ if (condition.get()) addAnimation(new Timeline(keyFrame));
+ return this;
+ }
+
/**
* Builds a {@link Timeline} with the given keyframes, sets the given onFinished action to it and then adds it to the
* "main" animation by calling {@link #addAnimation(Animation)}.
diff --git a/modules/resources/src/main/java/io/github/palexdev/mfxresources/builders/IconBuilder.java b/modules/resources/src/main/java/io/github/palexdev/mfxresources/builders/IconBuilder.java
index d6fe9677..9de73ed4 100644
--- a/modules/resources/src/main/java/io/github/palexdev/mfxresources/builders/IconBuilder.java
+++ b/modules/resources/src/main/java/io/github/palexdev/mfxresources/builders/IconBuilder.java
@@ -18,6 +18,7 @@
package io.github.palexdev.mfxresources.builders;
+import io.github.palexdev.mfxresources.fonts.IconDescriptor;
import io.github.palexdev.mfxresources.fonts.IconsProviders;
import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
import javafx.css.PseudoClass;
@@ -78,6 +79,11 @@ public IconBuilder setDescription(String code) {
return this;
}
+ public IconBuilder setDescription(IconDescriptor description) {
+ icon.setDescription(description);
+ return this;
+ }
+
public IconBuilder setSize(double size) {
icon.setSize(size);
return this;
@@ -102,6 +108,8 @@ public IconWrapperBuilder wrap() {
return new IconWrapperBuilder(icon.wrap());
}
+ public IconWrapperBuilder wrapperBuilder() {return icon.wrapperBuilder();}
+
//================================================================================
// Node Delegate Methods
//================================================================================
diff --git a/modules/resources/src/main/java/io/github/palexdev/mfxresources/builders/IconWrapperBuilder.java b/modules/resources/src/main/java/io/github/palexdev/mfxresources/builders/IconWrapperBuilder.java
index 056299ee..9e5a16c7 100644
--- a/modules/resources/src/main/java/io/github/palexdev/mfxresources/builders/IconWrapperBuilder.java
+++ b/modules/resources/src/main/java/io/github/palexdev/mfxresources/builders/IconWrapperBuilder.java
@@ -19,9 +19,11 @@
package io.github.palexdev.mfxresources.builders;
import io.github.palexdev.mfxeffects.beans.Position;
+import io.github.palexdev.mfxresources.fonts.IconDescriptor;
import io.github.palexdev.mfxresources.fonts.IconsProviders;
import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
import io.github.palexdev.mfxresources.fonts.MFXIconWrapper;
+import io.github.palexdev.mfxresources.fonts.MFXIconWrapper.AnimationPresets;
import javafx.css.PseudoClass;
import javafx.event.Event;
import javafx.event.EventHandler;
@@ -115,6 +117,18 @@ public IconWrapperBuilder setIcon(Font font, Function convert
return this;
}
+ public MFXIconWrapper setAnimated(boolean animated) {
+ return wrapper.setAnimated(animated);
+ }
+
+ public MFXIconWrapper setIcon(IconDescriptor descriptor) {
+ return wrapper.setIcon(descriptor);
+ }
+
+ public MFXIconWrapper setAnimationProvider(AnimationPresets preset) {
+ return wrapper.setAnimationProvider(preset);
+ }
+
//================================================================================
// Node Delegate Methods
//================================================================================
diff --git a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/IconsProviders.java b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/IconsProviders.java
index df5d79af..857347cd 100644
--- a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/IconsProviders.java
+++ b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/IconsProviders.java
@@ -22,8 +22,6 @@
import io.github.palexdev.mfxresources.fonts.fontawesome.FontAwesomeBrands;
import io.github.palexdev.mfxresources.fonts.fontawesome.FontAwesomeRegular;
import io.github.palexdev.mfxresources.fonts.fontawesome.FontAwesomeSolid;
-import io.github.palexdev.mfxresources.utils.EnumUtils;
-import javafx.scene.paint.Color;
import java.io.InputStream;
import java.util.function.Function;
@@ -60,85 +58,6 @@ public InputStream load() {
return MFXResources.loadFont(font);
}
- /**
- * Creates a new {@link MFXFontIcon} with a random icon description extracted from the values of "this" enumerator
- * constant.
- *
- * @param size the size of the icon
- * @param color the color of the icon
- */
- public MFXFontIcon randomIcon(double size, Color color) {
- MFXFontIcon icon = new MFXFontIcon();
- String desc;
- switch (this) {
- case FONTAWESOME_BRANDS: {
- icon.setIconsProvider(FONTAWESOME_BRANDS);
- desc = EnumUtils.randomEnum(FontAwesomeBrands.class).getDescription();
- break;
- }
- case FONTAWESOME_REGULAR: {
- icon.setIconsProvider(FONTAWESOME_REGULAR);
- desc = EnumUtils.randomEnum(FontAwesomeRegular.class).getDescription();
- break;
- }
- case FONTAWESOME_SOLID: {
- icon.setIconsProvider(FONTAWESOME_SOLID);
- desc = EnumUtils.randomEnum(FontAwesomeSolid.class).getDescription();
- break;
- }
- default:
- return icon;
- }
- icon.setDescription(desc);
- icon.setColor(color);
- icon.setSize(size);
- return icon;
- }
-
- /**
- * Creates a new {@link MFXFontIcon} with a random icon description extracted from the values of "this" enumerator
- * constant.
- */
- public MFXFontIcon randomIcon() {
- MFXFontIcon icon = new MFXFontIcon();
- String desc;
- switch (this) {
- case FONTAWESOME_BRANDS: {
- icon.setIconsProvider(FONTAWESOME_BRANDS);
- desc = EnumUtils.randomEnum(FontAwesomeBrands.class).getDescription();
- break;
- }
- case FONTAWESOME_REGULAR: {
- icon.setIconsProvider(FONTAWESOME_REGULAR);
- desc = EnumUtils.randomEnum(FontAwesomeRegular.class).getDescription();
- break;
- }
- case FONTAWESOME_SOLID: {
- icon.setIconsProvider(FONTAWESOME_SOLID);
- desc = EnumUtils.randomEnum(FontAwesomeSolid.class).getDescription();
- break;
- }
- default:
- return icon;
- }
- icon.setDescription(desc);
- return icon;
- }
-
- /**
- * Same as {@link #randomIcon(double, Color)}, allows usage from a static context.
- */
- public static MFXFontIcon randomIcon(IconsProviders provider, double size, Color color) {
- return provider.randomIcon(size, color);
- }
-
- /**
- * Same as {@link #randomIcon()}, allows usage from a static context.
- */
- public static MFXFontIcon randomIcon(IconsProviders provider) {
- return provider.randomIcon();
- }
-
/**
* @return the default icon provider used by {@link MFXFontIcon}s, currently {@link #FONTAWESOME_SOLID}
*/
diff --git a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXFontIcon.java b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXFontIcon.java
index 1e1f6f2e..c6bff761 100644
--- a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXFontIcon.java
+++ b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXFontIcon.java
@@ -48,7 +48,7 @@
*
* Now integrates with {@link MFXIconWrapper} in many ways with fluent API.
*/
-public class MFXFontIcon extends Text {
+public class MFXFontIcon extends Text implements Cloneable {
//================================================================================
// Properties
//================================================================================
@@ -66,6 +66,18 @@ public MFXFontIcon(IconDescriptor icon) {
this(icon.getDescription());
}
+ public MFXFontIcon(IconDescriptor icon, Color color) {
+ this(icon.getDescription(), color);
+ }
+
+ public MFXFontIcon(IconDescriptor icon, double size) {
+ this(icon.getDescription(), size);
+ }
+
+ public MFXFontIcon(IconDescriptor icon, double size, Color color) {
+ this(icon.getDescription(), size, color);
+ }
+
public MFXFontIcon(String description) {
this(description, 16.0);
}
@@ -242,8 +254,9 @@ public StyleableObjectProperty colorProperty() {
return color;
}
- public void setColor(Color color) {
+ public MFXFontIcon setColor(Color color) {
this.color.set(color);
+ return this;
}
public String getDescription() {
@@ -264,6 +277,11 @@ public MFXFontIcon setDescription(String code) {
return this;
}
+ public MFXFontIcon setDescription(IconDescriptor description) {
+ this.description.set(description.getDescription());
+ return this;
+ }
+
public double getSize() {
return size.get();
}
@@ -330,6 +348,20 @@ private static class StyleableProperties {
return getClassCssMetaData();
}
+ /**
+ * Creates a new {@code MFXFontIcon} instance with the same properties from this.
+ */
+ @SuppressWarnings({"MethodDoesntCallSuperMethod", "CloneDoesntDeclareCloneNotSupportedException"})
+ @Override
+ protected MFXFontIcon clone() {
+ MFXFontIcon clone = new MFXFontIcon();
+ clone.setDescriptionConverter(getDescriptionConverter());
+ clone.setDescription(getDescription());
+ clone.setSize(getSize());
+ clone.setColor(getColor());
+ return clone;
+ }
+
@Override
public String toString() {
return "MFXFontIcon{" +
diff --git a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java
index 9cb54186..cc6a35f4 100644
--- a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java
+++ b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/MFXIconWrapper.java
@@ -18,9 +18,18 @@
package io.github.palexdev.mfxresources.fonts;
+import io.github.palexdev.mfxeffects.animations.AnimationFactory;
+import io.github.palexdev.mfxeffects.animations.Animations;
+import io.github.palexdev.mfxeffects.animations.Animations.KeyFrames;
+import io.github.palexdev.mfxeffects.animations.Animations.SequentialBuilder;
+import io.github.palexdev.mfxeffects.animations.Animations.TimelineBuilder;
+import io.github.palexdev.mfxeffects.animations.motion.M3Motion;
import io.github.palexdev.mfxeffects.beans.Position;
import io.github.palexdev.mfxeffects.ripple.MFXRippleGenerator;
import io.github.palexdev.mfxresources.base.properties.IconProperty;
+import javafx.animation.Animation;
+import javafx.animation.Interpolator;
+import javafx.animation.Timeline;
import javafx.collections.ObservableList;
import javafx.css.*;
import javafx.event.EventHandler;
@@ -29,11 +38,14 @@
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
+import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
+import javafx.util.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.function.BiFunction;
import java.util.function.Function;
/**
@@ -45,25 +57,93 @@
* The new API makes these two features easier to use for developers. Many times I had to pass the wrapper instance to a
* controller just to enable them. Now, they both can be activated in CSS and in FXML, you don't even need their instance
* in the control as the icon can be specified by editing manually the FXML, the only limit being that you can't change
- * the icons provider.
+ * the icons' provider.
*
* Note that by default the size of the wrapper is always the same for both width and height, it can be specified via the
* {@link #sizeProperty()} or you could let this figure it out automatically at layout time. In the latter case, the
* size is computed as the maximum between the icon's width and height.
+ *
+ * Animation API
+ *
+ * {@code MFXIconWrapper} is now capable of switching the wrapped icon through an animation and here I'm going to explain
+ * here some crucial details here. The API has been developed to be as flexible as possible while still following some
+ * critical rules.
+ *
+ * Following classical ways to implement such thing, there is now a new property, {@link #animatedProperty()}, that allows
+ * to enable/disable the system. Then a user must specify a {@link BiFunction} that given the old/current icon and the
+ * new icon builds the animation, to be precise the return type is {@link IconAnimation}, I'll tell why in a moment.
+ *
+ * The main issue with this wrapper is that it has been developed to keep at max two nodes, the ripple generator and the
+ * icon. However, some animations may need to operate on both the old and the new. I had different choices at this point,
+ * like for example give the user the modifiable children list and let the management be manual. But I didn't want to
+ * break this rule so the here's the solution I came up with.
+ *
+ * When the icon changes, the new one is added to the children list alongside the old one, but it's set to be invisible.
+ * At this point, if an animation is already playing it is stopped by invoking {@link IconAnimation#stop(MFXIconWrapper)}.
+ * This wrapping class, contains three values, the animation playing, the old icon and the new icon. If multiple animations
+ * are played in rapid succession, by stopping the last one with that method, we ensure {@code MFXIconWrapper} is in a
+ * consistent state. However it's important to precise that it only ensures the children list consists of the
+ * ripple generator (if enabled) and the new icon. If any property of the new icon was changed, that is user's responsibility
+ * to reset when the animation stops (see {@link Animations#onStopped(Animation, Runnable, boolean)}).
+ *
+ * For example, if the animation is changing the opacity value of a node, and the animation is stopped for whatever
+ * reason, then the user that coded the animation should take this fact into account and add the proper reset code.
+ *
+ * After ensuring that no other animation is currently running the set {@link BiFunction} will produce a new {@link IconAnimation}
+ * object, and play the new animation.
+ *
+ * Final note, the {@link IconAnimation} object is also responsible for removing the old icon from the wrapper once
+ * the animation ends/stops (again see {@link Animations#onStatus(Animation, Animation.Status, Runnable, boolean)} to
+ * understand the difference).
+ *
+ * Last but not least, I understand that in some occasions it may be difficult to set an animation via code, since often
+ * the wrapper is part of the view/skin. So, just like the ripple generator and the round feature, I added a property
+ * {@link #animationPresetProperty()} that allows to set one of the predefined animations in {@link AnimationPresets}
+ * from CSS by setting the '-mfx-animation-preset' property.
+ *
+ * A very important note about this: the property is just a bridge to CSS. DO NOT use it via code, there's no
+ * point in it. If you use {@link #setAnimationProvider(BiFunction)} and {@link #setAnimationPreset(AnimationPresets)}
+ * together the one will overwrite the other!
*/
-// TODO add icon switch with animation
public class MFXIconWrapper extends StackPane {
//================================================================================
// Properties
//================================================================================
private final String STYLE_CLASS = "mfx-icon-wrapper";
- private final IconProperty icon = new IconProperty();
+ private final IconProperty icon = new IconProperty() {
+ @Override
+ public void set(MFXFontIcon newValue) {
+ MFXFontIcon oldValue = get();
+ if (isAnimated() && newValue != null) {
+ super.set(newValue);
+ newValue.setVisible(false);
+ addChild(newValue);
+ if (animation != null) animation.stop(MFXIconWrapper.this);
+ if (animationProvider != null) {
+ animation = animationProvider.apply(oldValue, newValue);
+ animation.play();
+ return;
+ }
+ }
+ removeChild(oldValue);
+ super.set(newValue);
+ if (newValue != null) addChild(newValue);
+ }
+
+ @Override
+ protected void invalidated() {
+ updateChildren();
+ }
+ };
private MFXRippleGenerator rg;
private EventHandler pressHandler;
private EventHandler releaseHandler;
private EventHandler exitHandler;
+ private IconAnimation animation;
+ private BiFunction animationProvider;
+
//================================================================================
// Constructors
//================================================================================
@@ -89,11 +169,7 @@ private void initialize() {
setMinSize(USE_PREF_SIZE, USE_PREF_SIZE);
setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> requestFocus());
- icon.addListener((observable, oldValue, newValue) -> {
- super.getChildren().remove(oldValue);
- manageChildren();
- });
- size.addListener((observable, oldValue, newValue) -> setPrefSize(newValue.doubleValue(), newValue.doubleValue()));
+ setAnimationPreset(AnimationPresets.FADE);
}
/**
@@ -140,11 +216,16 @@ public MFXIconWrapper enableRippleGenerator(boolean enable, Function
+ * However, instead of directly managing the children list, it's best for performance to use the
+ * {@link Node#viewOrderProperty()}. The ripple generator is always given order 0, while the icon has order 1,
+ * this way the latter always appears in front of the first.
*/
- private void manageChildren() {
- ObservableList children = super.getChildren();
- children.clear();
+ protected void updateChildren() {
MFXFontIcon icon = getIcon();
- if (rg != null) children.add(rg);
- if (icon != null) children.add(icon);
+ if (rg != null) rg.setViewOrder(0);
+ if (icon != null) icon.setViewOrder(1);
+ }
+
+ /**
+ * Convenience method for calling {@code super.getChildren().add(child)}.
+ */
+ protected void addChild(Node child) {
+ super.getChildren().add(child);
+ }
+
+ /**
+ * Convenience method for calling {@code super.getChildren().remove(child)}.
+ */
+ protected void removeChild(Node child) {
+ super.getChildren().remove(child);
}
//================================================================================
@@ -235,6 +332,7 @@ public ObservableList getChildren() {
@Override
protected void layoutChildren() {
super.layoutChildren();
+ if (rg != null) rg.resizeRelocate(0, 0, getWidth(), getHeight());
MFXFontIcon icon = getIcon();
if (icon != null && icon.getDescription() != null && !icon.getDescription().isBlank() && getSize() == -1) {
@@ -251,12 +349,48 @@ protected void layoutChildren() {
//================================================================================
// Styleable Properties
//================================================================================
+ private final StyleableBooleanProperty animated = new SimpleStyleableBooleanProperty(
+ StyleableProperties.ANIMATED,
+ this,
+ "animated",
+ false
+ ) {
+ @Override
+ public StyleOrigin getStyleOrigin() {
+ return StyleOrigin.USER_AGENT;
+ }
+ };
+
+ private final StyleableObjectProperty animationPreset = new SimpleStyleableObjectProperty<>(
+ StyleableProperties.ANIMATION_PRESET,
+ this,
+ "animationPreset",
+ null
+ ) {
+ @Override
+ protected void invalidated() {
+ AnimationPresets p = get();
+ if (p == null) return;
+ setAnimationProvider(p);
+ }
+
+ @Override
+ public StyleOrigin getStyleOrigin() {
+ return StyleOrigin.USER_AGENT;
+ }
+ };
+
private final StyleableDoubleProperty size = new SimpleStyleableDoubleProperty(
StyleableProperties.SIZE,
this,
"size",
-1.0
) {
+ @Override
+ protected void invalidated() {
+ setPrefSize(get(), get());
+ }
+
@Override
public StyleOrigin getStyleOrigin() {
return StyleOrigin.USER_AGENT;
@@ -274,6 +408,7 @@ protected void invalidated() {
boolean state = get();
if (!state && rg != null) enableRippleGenerator(false);
if (state && rg == null) enableRippleGenerator(true);
+ updateChildren();
}
@Override
@@ -300,6 +435,39 @@ public StyleOrigin getStyleOrigin() {
}
};
+ public boolean isAnimated() {
+ return animated.get();
+ }
+
+ /**
+ * Specifies whether icon switching should be animated.
+ *
+ * Can be set in CSS via the property: '-mfx-animated'.
+ *
+ * @see #setAnimationProvider(BiFunction)
+ * @see #setAnimationProvider(AnimationPresets)
+ */
+ public StyleableBooleanProperty animatedProperty() {
+ return animated;
+ }
+
+ public MFXIconWrapper setAnimated(boolean animated) {
+ this.animated.set(animated);
+ return this;
+ }
+
+ public AnimationPresets getAnimationPreset() {
+ return animationPreset.get();
+ }
+
+ public StyleableObjectProperty animationPresetProperty() {
+ return animationPreset;
+ }
+
+ public void setAnimationPreset(AnimationPresets animationPreset) {
+ this.animationPreset.set(animationPreset);
+ }
+
public double getSize() {
return size.get();
}
@@ -363,6 +531,21 @@ private static class StyleableProperties {
private static final StyleablePropertyFactory FACTORY = new StyleablePropertyFactory<>(StackPane.getClassCssMetaData());
private static final List> cssMetaDataList;
+ private static final CssMetaData ANIMATED =
+ FACTORY.createBooleanCssMetaData(
+ "-mfx-animated",
+ MFXIconWrapper::animatedProperty,
+ false
+ );
+
+ private static final CssMetaData ANIMATION_PRESET =
+ FACTORY.createEnumCssMetaData(
+ AnimationPresets.class,
+ "-mfx-animation-preset",
+ MFXIconWrapper::animationPresetProperty,
+ null
+ );
+
private static final CssMetaData SIZE =
FACTORY.createSizeCssMetaData(
"-mfx-size",
@@ -386,7 +569,7 @@ private static class StyleableProperties {
static {
List> data = new ArrayList<>(StackPane.getClassCssMetaData());
- Collections.addAll(data, SIZE, ENABLE_RIPPLE, ROUND);
+ Collections.addAll(data, ANIMATED, ANIMATION_PRESET, SIZE, ENABLE_RIPPLE, ROUND);
cssMetaDataList = List.copyOf(data);
}
}
@@ -458,4 +641,400 @@ public MFXIconWrapper setIcon(Font font, Function converter,
setIcon(new MFXFontIcon().setIconsProvider(font, converter).setDescription(desc));
return this;
}
+
+ /**
+ * Convenience method to set the {@link #iconProperty()} to a new {@link MFXFontIcon} instance given an {@link IconDescriptor}.
+ */
+ public MFXIconWrapper setIcon(IconDescriptor descriptor) {
+ setIcon(new MFXFontIcon(descriptor));
+ return this;
+ }
+
+ /**
+ * @see #setAnimationProvider(BiFunction)
+ */
+ public BiFunction getAnimationProvider() {
+ return animationProvider;
+ }
+
+ /**
+ * Sets the function responsible for building the animation used when switching icons, this must be used in combination
+ * with {@link #animatedProperty()}, if that is not enabled than no animation will play.
+ *
+ * Also, to be precise, the function must return an {@link IconAnimation} object, see its docs for the why.
+ */
+ public MFXIconWrapper setAnimationProvider(BiFunction animationProvider) {
+ this.animationProvider = animationProvider;
+ return this;
+ }
+
+ /**
+ * This can be used to convert one of the predefined animations in {@link AnimationPresets} to the {@link BiFunction}
+ * needed by {@link #setAnimationProvider(BiFunction)}.
+ */
+ public MFXIconWrapper setAnimationProvider(AnimationPresets preset) {
+ this.animationProvider = (o, n) -> preset.animate(this, o, n);
+ return this;
+ }
+
+ //================================================================================
+ // Internal Classes
+ //================================================================================
+
+ /**
+ * This wrapper class is used by {@link MFXIconWrapper} to manage its state when switching icons through animations.
+ * It has three properties: the animation used for the switch, the old/current icon, and the new icon.
+ *
+ * There are two main functionalities here:
+ *
1) When the animation ends, makes sure to remove the old icon from the children list
+ *
2) When the animation is stopped, ensures that {@link MFXIconWrapper} is in a consistent state by calling
+ * {@link MFXIconWrapper#updateChildren()}
+ *
+ * @see #stop(MFXIconWrapper)
+ */
+ public static class IconAnimation {
+ private final Animation animation;
+ private final MFXFontIcon oldIcon;
+ private final MFXFontIcon newIcon;
+
+ public IconAnimation(MFXIconWrapper wrapper, Animation animation, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ this.animation = animation;
+ this.oldIcon = oldIcon;
+ this.newIcon = newIcon;
+ Animations.onStopped(animation, () -> wrapper.removeChild(oldIcon), true);
+ }
+
+ /**
+ * Delegate for {@link Animation#play()}.
+ */
+ public IconAnimation play() {
+ if (animation != null) animation.play();
+ return this;
+ }
+
+ /**
+ * This method is automatically invoked by {@link MFXIconWrapper#iconProperty()} when it changes, if another
+ * animation is playing.
+ *
+ * If the animation is stopped before its end this method makes sure that the {@link MFXIconWrapper} is in a
+ * consistent state. In particular two things need to be done:
+ *
1) One thing is to ensure the children list contains the right nodes, it's enough to call
+ * {@link MFXIconWrapper#updateChildren()}
+ *
2) The other is not responsibility of this method but rather the animation. When their state transitions
+ * to {@link Animation.Status#STOPPED}, they should restore changes that otherwise may leave the {@link MFXIconWrapper}
+ * and its children in an inconsistent state. More on the difference between 'stopped' and 'finished' here:
+ * {@link Animations#onStatus(Animation, Animation.Status, Runnable, boolean)}.
+ *
An example for point 2 may be an animation that scales some content, say from 1.0 to 0.0. If for whatever
+ * reason (like the start of a new animation) the animation is stopped, it's needed to reset the changes, so set
+ * the scale back to 1.0.
+ *
+ * The automatic call of this method is to ensure no strange effects occur if multiple animations are started in
+ * rapid succession. For this reason, it's also recommended to keep animations as short as possible. For instance
+ * most of the predefined animations in {@link AnimationPresets} last 200ms.
+ */
+ public void stop(MFXIconWrapper wrapper) {
+ if (!Animations.isStopped(animation)) {
+ animation.stop();
+ wrapper.removeChild(oldIcon);
+ wrapper.updateChildren();
+ }
+ }
+
+ public Animation getAnimation() {
+ return animation;
+ }
+
+ public MFXFontIcon getOldIcon() {
+ return oldIcon;
+ }
+
+ public MFXFontIcon getNewIcon() {
+ return newIcon;
+ }
+ }
+
+ /**
+ * Enumeration that allows building a series of predefined animations to be used with
+ * {@link MFXIconWrapper#setAnimationProvider(AnimationPresets)}.
+ */
+ public enum AnimationPresets {
+ /**
+ * This animation fades out the old/current icon and fades in the new icon.
+ */
+ FADE {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ newIcon.setOpacity(0.0);
+ newIcon.setVisible(true);
+ Duration d = M3Motion.SHORT4;
+ Interpolator i = M3Motion.STANDARD;
+ SequentialBuilder sb = new SequentialBuilder();
+ if (oldIcon != null) sb.add(AnimationFactory.FADE_OUT.build(oldIcon, d, i));
+ sb.add(AnimationFactory.FADE_IN.build(newIcon, d, i));
+ return new IconAnimation(wrapper, sb.getAnimation(), oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This animation slides up the old/current icon and then down the new icon.
+ */
+ SLIDE_UP {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ clip(wrapper);
+ Duration d = M3Motion.MEDIUM1;
+ SequentialBuilder sb = new SequentialBuilder();
+ if (oldIcon != null)
+ sb.add(AnimationFactory.SLIDE_OUT_TOP.build(oldIcon, d, M3Motion.EMPHASIZED_ACCELERATE));
+ Timeline t = AnimationFactory.SLIDE_IN_TOP.build(newIcon, d, M3Motion.EMPHASIZED_DECELERATE);
+ t.getKeyFrames().add(KeyFrames.of(0, e -> newIcon.setVisible(true)));
+ sb.add(t);
+ Animations.onStopped(sb.getAnimation(), () -> {
+ if (!wrapper.isRound()) wrapper.setClip(null);
+ }, true);
+ return new IconAnimation(wrapper, sb.getAnimation(), oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This animation slides down the old/current icon and then up the new icon.
+ */
+ SLIDE_BOTTOM {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ clip(wrapper);
+ Duration d = M3Motion.MEDIUM1;
+ SequentialBuilder sb = new SequentialBuilder();
+ if (oldIcon != null)
+ sb.add(AnimationFactory.SLIDE_OUT_BOTTOM.build(oldIcon, d, M3Motion.EMPHASIZED_ACCELERATE));
+ Timeline t = AnimationFactory.SLIDE_IN_BOTTOM.build(newIcon, d, M3Motion.EMPHASIZED_DECELERATE);
+ t.getKeyFrames().add(KeyFrames.of(0, e -> newIcon.setVisible(true)));
+ sb.add(t);
+ Animations.onStopped(sb.getAnimation(), () -> {
+ if (!wrapper.isRound()) wrapper.setClip(null);
+ }, true);
+ return new IconAnimation(wrapper, sb.getAnimation(), oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This animation slides down both the old/current and new icons.
+ */
+ SLIDE_BOTTOM_UP {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ clip(wrapper);
+ Duration d = M3Motion.MEDIUM1;
+ SequentialBuilder sb = new SequentialBuilder();
+ if (oldIcon != null)
+ sb.add(AnimationFactory.SLIDE_OUT_BOTTOM.build(oldIcon, d, M3Motion.EMPHASIZED_ACCELERATE));
+ Timeline t = AnimationFactory.SLIDE_IN_TOP.build(newIcon, d, M3Motion.EMPHASIZED_DECELERATE);
+ t.getKeyFrames().add(KeyFrames.of(0, e -> newIcon.setVisible(true)));
+ sb.add(t);
+ Animations.onStopped(sb.getAnimation(), () -> {
+ if (!wrapper.isRound()) wrapper.setClip(null);
+ }, true);
+ return new IconAnimation(wrapper, sb.getAnimation(), oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This animation slides down both the old/current and new icons.
+ */
+ SLIDE_UP_BOTTOM {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ clip(wrapper);
+ Duration d = M3Motion.MEDIUM1;
+ SequentialBuilder sb = new SequentialBuilder();
+ if (oldIcon != null)
+ sb.add(AnimationFactory.SLIDE_OUT_TOP.build(oldIcon, d, M3Motion.EMPHASIZED_ACCELERATE));
+ Timeline t = AnimationFactory.SLIDE_IN_BOTTOM.build(newIcon, d, M3Motion.EMPHASIZED_DECELERATE);
+ t.getKeyFrames().add(KeyFrames.of(0, e -> newIcon.setVisible(true)));
+ sb.add(t);
+ Animations.onStopped(sb.getAnimation(), () -> {
+ if (!wrapper.isRound()) wrapper.setClip(null);
+ }, true);
+ return new IconAnimation(wrapper, sb.getAnimation(), oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This animation slides right the old/current icon and then left the new icon.
+ */
+ SLIDE_RIGHT {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ clip(wrapper);
+ Duration d = M3Motion.MEDIUM1;
+ SequentialBuilder sb = new SequentialBuilder();
+ if (oldIcon != null)
+ sb.add(AnimationFactory.SLIDE_OUT_RIGHT.build(oldIcon, d, M3Motion.EMPHASIZED_ACCELERATE));
+ Timeline t = AnimationFactory.SLIDE_IN_RIGHT.build(newIcon, d, M3Motion.EMPHASIZED_DECELERATE);
+ t.getKeyFrames().add(KeyFrames.of(0, e -> newIcon.setVisible(true)));
+ sb.add(t);
+ Animations.onStopped(sb.getAnimation(), () -> {
+ if (!wrapper.isRound()) wrapper.setClip(null);
+ }, true);
+ return new IconAnimation(wrapper, sb.getAnimation(), oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This animation slides left the old/current icon and then right the new icon.
+ */
+ SLIDE_LEFT {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ clip(wrapper);
+ Duration d = M3Motion.MEDIUM1;
+ SequentialBuilder sb = new SequentialBuilder();
+ if (oldIcon != null)
+ sb.add(AnimationFactory.SLIDE_OUT_LEFT.build(oldIcon, d, M3Motion.EMPHASIZED_ACCELERATE));
+ Timeline t = AnimationFactory.SLIDE_IN_LEFT.build(newIcon, d, M3Motion.EMPHASIZED_DECELERATE);
+ t.getKeyFrames().add(KeyFrames.of(0, e -> newIcon.setVisible(true)));
+ sb.add(t);
+ Animations.onStopped(sb.getAnimation(), () -> {
+ if (!wrapper.isRound()) wrapper.setClip(null);
+ }, true);
+ return new IconAnimation(wrapper, sb.getAnimation(), oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This animation slides right both the old/current and new icons
+ */
+ SLIDE_RIGHT_LEFT {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ clip(wrapper);
+ Duration d = M3Motion.MEDIUM1;
+ SequentialBuilder sb = new SequentialBuilder();
+ if (oldIcon != null)
+ sb.add(AnimationFactory.SLIDE_OUT_RIGHT.build(oldIcon, d, M3Motion.EMPHASIZED_ACCELERATE));
+ Timeline t = AnimationFactory.SLIDE_IN_RIGHT.build(newIcon, d, M3Motion.EMPHASIZED_DECELERATE);
+ t.getKeyFrames().add(KeyFrames.of(0, e -> newIcon.setVisible(true)));
+ sb.add(t);
+ Animations.onStopped(sb.getAnimation(), () -> {
+ if (!wrapper.isRound()) wrapper.setClip(null);
+ }, true);
+ return new IconAnimation(wrapper, sb.getAnimation(), oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This animation slides left both the old/current and new icons
+ */
+ SLIDE_LEFT_RIGHT {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ clip(wrapper);
+ Duration d = M3Motion.MEDIUM1;
+ SequentialBuilder sb = new SequentialBuilder();
+ if (oldIcon != null)
+ sb.add(AnimationFactory.SLIDE_OUT_LEFT.build(oldIcon, d, M3Motion.EMPHASIZED_ACCELERATE));
+ Timeline t = AnimationFactory.SLIDE_IN_LEFT.build(newIcon, d, M3Motion.EMPHASIZED_DECELERATE);
+ t.getKeyFrames().add(KeyFrames.of(0, e -> newIcon.setVisible(true)));
+ sb.add(t);
+ Animations.onStopped(sb.getAnimation(), () -> {
+ if (!wrapper.isRound()) wrapper.setClip(null);
+ }, true);
+ return new IconAnimation(wrapper, sb.getAnimation(), oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This animation is similar to the one seen in standard FABs. It animates the scale properties of the
+ * {@link MFXIconWrapper} as well as the opacity of the two icons. It is composed of two phases: the first animates
+ * out the old/current icon, the second animates in the new one.
+ *
+ * A thing worth mentioning is that the animation scales the {@link MFXIconWrapper} directly because for some reason
+ * scaling the two icons results in a wobbly layout.
+ */
+ SCALE {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ newIcon.setOpacity(0.0);
+ newIcon.setVisible(true);
+ Duration d = M3Motion.SHORT4;
+ Interpolator downCurve = M3Motion.EMPHASIZED_ACCELERATE;
+ Interpolator upCurve = M3Motion.EMPHASIZED_DECELERATE;
+ Animation animation = SequentialBuilder.build()
+ .add(TimelineBuilder.build()
+ .add(KeyFrames.of(d, oldIcon.opacityProperty(), 0.0, downCurve))
+ .add(KeyFrames.of(d, wrapper.scaleXProperty(), 0.0, downCurve))
+ .add(KeyFrames.of(d, wrapper.scaleYProperty(), 0.0, downCurve))
+ .getAnimation()
+ )
+ .add(TimelineBuilder.build()
+ .add(KeyFrames.of(d, newIcon.opacityProperty(), 1.0, upCurve))
+ .add(KeyFrames.of(d, wrapper.scaleXProperty(), 1.0, upCurve))
+ .add(KeyFrames.of(d, wrapper.scaleYProperty(), 1.0, upCurve))
+ .getAnimation()
+ )
+ .getAnimation();
+ Animations.onStopped(animation, () -> {
+ newIcon.setOpacity(1.0);
+ wrapper.setScaleX(1.0);
+ wrapper.setScaleY(1.0);
+ }, true);
+ return new IconAnimation(wrapper, animation, oldIcon, newIcon);
+ }
+ },
+
+ /**
+ * This is animation in quite particular and looks good in very few occasions. It uses two Rectangle clips to
+ * hide the old/current icon and at the same time show the new icon.
+ *
+ * The old icon's clip is set to have the width and height of the old icon, while the new one's clip is set to have
+ * a width of 0 and the height of the new icon. The animation sets the first's width progressively to 0, while
+ * the other progressively to the new icon's width.
+ *
+ * Note that in order to get the new icon's width in the wrapper, it's necessary to force a CSS pass by calling
+ * {@link MFXIconWrapper#applyCss()}.
+ */
+ CLIP {
+ @Override
+ public IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon) {
+ TimelineBuilder aBuilder = new TimelineBuilder();
+ Duration d = M3Motion.LONG1;
+ Interpolator i = M3Motion.EMPHASIZED;
+ double wh = wrapper.getHeight();
+
+ if (oldIcon != null) {
+ Rectangle oClip = new Rectangle(oldIcon.prefWidth(-1), oldIcon.prefHeight(-1));
+ oClip.setLayoutY(-oClip.getHeight());
+ oldIcon.setClip(oClip);
+ aBuilder.add(KeyFrames.of(d, oClip.widthProperty(), 0.0, i));
+ }
+
+ wrapper.applyCss();
+ double nSize = newIcon.prefWidth(-1);
+ Rectangle nClip = new Rectangle(0, wh);
+ nClip.setLayoutY(-wh);
+ newIcon.setClip(nClip);
+ newIcon.setVisible(true);
+ aBuilder.add(KeyFrames.of(d, nClip.widthProperty(), nSize, i));
+ Animations.onStopped(aBuilder.getAnimation(), () -> {
+ if (oldIcon != null) oldIcon.setClip(null);
+ newIcon.setClip(null);
+ }, true);
+ return new IconAnimation(wrapper, aBuilder.getAnimation(), oldIcon, newIcon);
+ }
+ };
+
+ public abstract IconAnimation animate(MFXIconWrapper wrapper, MFXFontIcon oldIcon, MFXFontIcon newIcon);
+
+ /**
+ * Used by the slide animations to clip the {@link MFXIconWrapper} so that icons that go outside its bounds are
+ * hidden. The animations automatically remove it once they stop/end.
+ */
+ protected void clip(MFXIconWrapper wrapper) {
+ if (wrapper.isRound()) return;
+ Rectangle r = new Rectangle();
+ r.widthProperty().bind(wrapper.widthProperty());
+ r.heightProperty().bind(wrapper.heightProperty());
+ wrapper.setClip(r);
+ }
+ }
}
diff --git a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeBrands.java b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeBrands.java
index 9a2f5951..42d4c970 100644
--- a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeBrands.java
+++ b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeBrands.java
@@ -19,6 +19,8 @@
package io.github.palexdev.mfxresources.fonts.fontawesome;
import io.github.palexdev.mfxresources.fonts.IconDescriptor;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import io.github.palexdev.mfxresources.utils.EnumUtils;
import java.util.Arrays;
import java.util.Map;
@@ -520,6 +522,21 @@ public Map getCache() {
return cache();
}
+ /**
+ * @return a new {@link MFXFontIcon} with a random {@link IconDescriptor} from this enumeration
+ */
+ public static MFXFontIcon random() {
+ FontAwesomeBrands desc = EnumUtils.randomEnum(FontAwesomeBrands.class);
+ return new MFXFontIcon(desc);
+ }
+
+ /**
+ * @return a new {@link MFXFontIcon} with a random {@link IconDescriptor} from this enumeration and the given size
+ */
+ public static MFXFontIcon random(double size) {
+ return random().setSize(size);
+ }
+
/**
* Converts the given icon description/name to its corresponding unicode character.
*
diff --git a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeRegular.java b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeRegular.java
index 67503950..3a36a87f 100644
--- a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeRegular.java
+++ b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeRegular.java
@@ -19,6 +19,9 @@
package io.github.palexdev.mfxresources.fonts.fontawesome;
import io.github.palexdev.mfxresources.fonts.IconDescriptor;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import io.github.palexdev.mfxresources.utils.EnumUtils;
+import javafx.scene.paint.Color;
import java.util.Arrays;
import java.util.Map;
@@ -218,6 +221,28 @@ public Map getCache() {
return cache();
}
+ /**
+ * @return a new {@link MFXFontIcon} with a random {@link IconDescriptor} from this enumeration
+ */
+ public static MFXFontIcon random() {
+ FontAwesomeRegular desc = EnumUtils.randomEnum(FontAwesomeRegular.class);
+ return new MFXFontIcon(desc);
+ }
+
+ /**
+ * @return a new {@link MFXFontIcon} with a random {@link IconDescriptor} from this enumeration and the given size
+ */
+ public static MFXFontIcon random(double size) {
+ return random().setSize(size);
+ }
+
+ /**
+ * @return a new {@link MFXFontIcon} with a random {@link IconDescriptor} from this enumeration, the given color and size
+ */
+ public static MFXFontIcon random(Color color, double size) {
+ return random().setColor(color).setSize(size);
+ }
+
/**
* Converts the given icon description/name to its corresponding unicode character.
*
diff --git a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeSolid.java b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeSolid.java
index acece743..7b3835e6 100644
--- a/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeSolid.java
+++ b/modules/resources/src/main/java/io/github/palexdev/mfxresources/fonts/fontawesome/FontAwesomeSolid.java
@@ -19,6 +19,9 @@
package io.github.palexdev.mfxresources.fonts.fontawesome;
import io.github.palexdev.mfxresources.fonts.IconDescriptor;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import io.github.palexdev.mfxresources.utils.EnumUtils;
+import javafx.scene.paint.Color;
import java.util.Arrays;
import java.util.Map;
@@ -1445,6 +1448,28 @@ public Map getCache() {
return cache();
}
+ /**
+ * @return a new {@link MFXFontIcon} with a random {@link IconDescriptor} from this enumeration
+ */
+ public static MFXFontIcon random() {
+ FontAwesomeSolid desc = EnumUtils.randomEnum(FontAwesomeSolid.class);
+ return new MFXFontIcon(desc);
+ }
+
+ /**
+ * @return a new {@link MFXFontIcon} with a random {@link IconDescriptor} from this enumeration and the given size
+ */
+ public static MFXFontIcon random(double size) {
+ return random().setSize(size);
+ }
+
+ /**
+ * @return a new {@link MFXFontIcon} with a random {@link IconDescriptor} from this enumeration, the given color and size
+ */
+ public static MFXFontIcon random(Color color, double size) {
+ return random().setColor(color).setSize(size);
+ }
+
/**
* Converts the given icon description/name to its corresponding unicode character.
*
diff --git a/modules/resources/src/test/java/app/IconAnimation.java b/modules/resources/src/test/java/app/IconAnimation.java
new file mode 100644
index 00000000..d7622199
--- /dev/null
+++ b/modules/resources/src/test/java/app/IconAnimation.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see .
+ */
+
+package app;
+
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import io.github.palexdev.mfxresources.fonts.MFXIconWrapper;
+import io.github.palexdev.mfxresources.fonts.fontawesome.FontAwesomeSolid;
+import javafx.application.Application;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.layout.Border;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.scene.paint.Color;
+import javafx.stage.Stage;
+import org.scenicview.ScenicView;
+
+import java.util.Base64;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class IconAnimation extends Application {
+
+ @Override
+ public void start(Stage primaryStage) {
+ VBox pane = new VBox(20);
+ pane.setAlignment(Pos.TOP_CENTER);
+ pane.setPadding(new Insets(20));
+
+ MFXFontIcon startIcon = new MFXFontIcon(FontAwesomeSolid.CHECK);
+ MFXIconWrapper wrapper = new MFXIconWrapper(startIcon, 64)
+ .setAnimated(true)
+ .setAnimationProvider(MFXIconWrapper.AnimationPresets.CLIP)
+ .enableRippleGenerator(true);
+ wrapper.setBorder(Border.stroke(Color.RED));
+ //wrapper.setBackground(Background.fill(Color.PURPLE));
+
+ String css = """
+ .mfx-icon-wrapper .mfx-ripple-generator {
+ -mfx-ripple-color: lightgrey;
+ -mfx-ripple-radius: 64px;
+ }
+
+ .mfx-icon-wrapper .mfx-font-icon {
+ -mfx-size: 64px;
+ }
+ """;
+ String data = "data:base64," + new String(Base64.getEncoder().encode(css.getBytes(UTF_8)), UTF_8);
+ wrapper.getStylesheets().add(data);
+
+ Button b1 = new Button("To Check");
+ b1.setOnAction(e -> wrapper.setIcon(FontAwesomeSolid.CHECK));
+ Button b2 = new Button("To Minus");
+ b2.setOnAction(e -> wrapper.setIcon(FontAwesomeSolid.MINUS));
+ Button b3 = new Button("To X");
+ b3.setOnAction(e -> wrapper.setIcon(FontAwesomeSolid.XMARK));
+ HBox box = new HBox(30, b1, b2, b3);
+ box.setAlignment(Pos.CENTER);
+
+ pane.getChildren().addAll(wrapper, box);
+ Scene scene = new Scene(pane, 800, 600);
+ primaryStage.setScene(scene);
+ primaryStage.show();
+ ScenicView.show(scene);
+ }
+}
diff --git a/modules/resources/src/test/java/app/IconWrapperTest.java b/modules/resources/src/test/java/app/IconWrapperTest.java
new file mode 100644
index 00000000..8a8d9d58
--- /dev/null
+++ b/modules/resources/src/test/java/app/IconWrapperTest.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 Parisi Alessandro - alessandro.parisi406@gmail.com
+ * This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
+ *
+ * MaterialFX is free software: you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * as published by the Free Software Foundation; either version 3 of the License,
+ * or (at your option) any later version.
+ *
+ * MaterialFX 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 Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with MaterialFX. If not, see .
+ */
+
+package app;
+
+import io.github.palexdev.mfxeffects.ripple.MFXRippleGenerator;
+import io.github.palexdev.mfxresources.fonts.MFXFontIcon;
+import io.github.palexdev.mfxresources.fonts.MFXIconWrapper;
+import io.github.palexdev.mfxresources.fonts.fontawesome.FontAwesomeSolid;
+import javafx.application.Application;
+import javafx.collections.ObservableList;
+import javafx.scene.Node;
+import javafx.scene.Scene;
+import javafx.scene.layout.StackPane;
+import javafx.stage.Stage;
+
+public class IconWrapperTest extends Application {
+
+ @Override
+ public void start(Stage primaryStage) {
+ StackPane pane = new StackPane();
+
+ // Set up like this, the ripple is in front of icon
+ // But the set view order should fix it
+ // Expect the effect behind the icon
+ MFXIconWrapper icon = new MFXIconWrapper(FontAwesomeSolid.random(64), 128)
+ .enableRippleGenerator(true)
+ .makeRound(true);
+
+ // Check
+ ObservableList children = icon.getChildren();
+ assert children.get(0) instanceof MFXFontIcon;
+ assert children.get(1) instanceof MFXRippleGenerator;
+
+ pane.getChildren().add(icon);
+ Scene scene = new Scene(pane, 400, 400);
+ primaryStage.setScene(scene);
+ primaryStage.show();
+ //ScenicView.show(scene);
+ }
+}
diff --git a/modules/resources/src/test/java/app/Launcher.java b/modules/resources/src/test/java/app/ResourcesLauncher.java
similarity index 91%
rename from modules/resources/src/test/java/app/Launcher.java
rename to modules/resources/src/test/java/app/ResourcesLauncher.java
index e84db7bd..a9fe48db 100644
--- a/modules/resources/src/test/java/app/Launcher.java
+++ b/modules/resources/src/test/java/app/ResourcesLauncher.java
@@ -20,9 +20,9 @@
import javafx.application.Application;
-public class Launcher {
+public class ResourcesLauncher {
public static void main(String[] args) {
- Application.launch(IconsApp.class, args);
+ Application.launch(IconAnimation.class, args);
}
}