io.github.palexdev.mfxresources.fonts.MFXIconWrapper Maven / Gradle / Ivy
Show all versions of materialfx-all Show documentation
/*
* Copyright (C) 2023 Parisi Alessandro - [email protected]
* 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 io.github.palexdev.mfxresources.fonts;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
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.application.Platform;
import javafx.collections.ObservableList;
import javafx.css.*;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
/**
* This component is intended for wrapping {@link MFXFontIcon}s, extends {@link StackPane} and offers
* some common features that one may want to use with font icons like:
* - Generate ripple effects on click
*
- Make the icon round
*
- Switching icons with an animation
*
* 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.
*
* 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 you 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's list and let the management be manual. But I didn't want to
* break this rule, so 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 on 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!
*
* In the latest version, I also addressed another issue. You see, the animation is used only and only if the icon's instance
* changed. In other words, if you had a stylesheet changing the icon, the animation would not play as it would not
* change the instance but the icon's description. For this reason, I added a new CSS property {@link #iconDescriptionProperty()},
* which allows you to change the icon's instance from CSS, and thus using transitions.
*/
public class MFXIconWrapper extends StackPane {
//================================================================================
// Properties
//================================================================================
private final String STYLE_CLASS = "mfx-icon-wrapper";
private final IconProperty icon = new IconProperty() {
@Override
public void set(MFXFontIcon newValue) {
MFXFontIcon oldValue = get();
if (isAnimated() && newValue != null) {
super.set(newValue);
if (animation != null) animation.stop(MFXIconWrapper.this);
if (!isDisabled() && animationProvider != null) {
newValue.setVisible(false);
addChild(newValue);
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
//================================================================================
public MFXIconWrapper() {
this(new MFXFontIcon(), -1.0);
}
public MFXIconWrapper(MFXFontIcon icon) {
this(icon, -1.0);
}
public MFXIconWrapper(MFXFontIcon icon, double size) {
initialize();
setIcon(icon);
setSize(size);
}
//================================================================================
// Methods
//================================================================================
private void initialize() {
getStyleClass().add(STYLE_CLASS);
setMinSize(USE_PREF_SIZE, USE_PREF_SIZE);
setMaxSize(USE_PREF_SIZE, USE_PREF_SIZE);
addEventHandler(MouseEvent.MOUSE_PRESSED, e -> requestFocus());
}
/**
* Calls {@link #enableRippleGenerator(boolean, Function)}, the function to determine the ripple location
* uses the {@link MouseEvent#getX()} and {@link MouseEvent#getY()} coordinates.
*/
public MFXIconWrapper enableRippleGenerator(boolean enable) {
return enableRippleGenerator(enable, e -> Position.of(e.getX(), e.getY()));
}
/**
* Enables or disables the ripple effect for this wrapper depending on the given boolean flag.
*
* If the flag is false, the ripple generator is removed from the container and set to null.
*
* If the flag is true, a new ripple generator is created, the given function determines where ripple effects will be
* generated. An {@link EventHandler} is also added to generate ripples on {@link MouseEvent#MOUSE_PRESSED} when
* the clicked button is the primary.
*
* @throws IllegalStateException if the boolean flag is true and the ripple generator is already present
*/
public MFXIconWrapper enableRippleGenerator(boolean enable, Function positionFunction) {
if (!enable) {
if (rg != null && pressHandler != null) {
removeEventHandler(MouseEvent.MOUSE_PRESSED, pressHandler);
removeEventHandler(MouseEvent.MOUSE_RELEASED, releaseHandler);
removeEventHandler(MouseEvent.MOUSE_EXITED, exitHandler);
removeChild(rg);
rg = null;
}
return this;
}
if (rg != null)
throw new IllegalStateException("Ripple generator has already been enabled for this icon!");
if (pressHandler == null) {
pressHandler = e -> {
if (e.getButton() == MouseButton.PRIMARY)
rg.generate(e);
};
releaseHandler = e -> rg.release();
exitHandler = e -> rg.release();
}
rg = new MFXRippleGenerator(this);
rg.setManaged(false);
rg.setMeToPosConverter(positionFunction);
addEventHandler(MouseEvent.MOUSE_PRESSED, pressHandler);
addEventHandler(MouseEvent.MOUSE_RELEASED, releaseHandler);
addEventHandler(MouseEvent.MOUSE_EXITED, exitHandler);
// If while an animation is playing, the ripple generator is being enabled/disabled, we want to stop the animation
// to ensure the wrapper is in a consistent state before calling updateChildren()
if (animation != null) animation.stop(this);
addChild(rg);
setEnableRipple(true);
return this;
}
/**
* Makes this container round by applying a {@link Circle} clip on it.
*
* If the given boolean flag is false, the clip is removed.
*
* If the given boolean flag is true and the clip is already set, simply returns.
*/
public MFXIconWrapper makeRound(boolean state) {
if (!state) {
setClip(null);
return this;
}
Node clip = getClip();
if (clip != null) return this;
Circle circle = new Circle();
circle.radiusProperty().bind(widthProperty().divide(2.0));
circle.centerXProperty().bind(widthProperty().divide(2.0));
circle.centerYProperty().bind(heightProperty().divide(2.0));
setClip(circle);
setRound(true);
return this;
}
/**
* Makes this container round by applying a {@link Circle} clip on it, uses the given radius for the circle.
*
* If the given boolean flag is false, the clip is removed.
*
* If the given boolean flag is true and the clip is already set, simply returns.
*/
public MFXIconWrapper makeRound(boolean state, double radius) {
if (!state) {
setClip(null);
return this;
}
Node clip = getClip();
if (clip != null) return this;
Circle circle = new Circle(radius);
circle.centerXProperty().bind(widthProperty().divide(2.0));
circle.centerYProperty().bind(heightProperty().divide(2.0));
setClip(circle);
setRound(true);
return this;
}
/**
* Responsible for managing the children of this container.
* We want the ripple generator to always "appear" in the back of the icon.
*
* 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.
*/
protected void updateChildren() {
MFXFontIcon icon = getIcon();
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) {
if (!Platform.isFxApplicationThread()) {
Platform.runLater(() -> super.getChildren().add(child));
return;
}
super.getChildren().add(child);
}
/**
* Convenience method for calling {@code super.getChildren().remove(child)}.
*/
protected void removeChild(Node child) {
if (!Platform.isFxApplicationThread()) {
Platform.runLater(() -> super.getChildren().remove(child));
return;
}
super.getChildren().remove(child);
}
//================================================================================
// Overridden Methods
//================================================================================
/**
* {@inheritDoc}
*
* Overridden to return {@link #getChildrenUnmodifiable()}
*
* @return
*/
@Override
public ObservableList getChildren() {
return getChildrenUnmodifiable();
}
/**
* {@inheritDoc}
*
* Overridden to also compute the {@link #sizeProperty()} when its value is set to "-1", the size
* is computed as the maximum between the icon's width and height and takes into account the padding.
*/
@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) {
double iW = icon.prefWidth(-1);
double iH = icon.prefHeight(-1);
double size = Math.max(
snappedLeftInset() + iW + snappedRightInset(),
snappedTopInset() + iH + snappedBottomInset()
);
setSize(size);
}
}
//================================================================================
// 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 StyleableStringProperty iconDescription = new SimpleStyleableStringProperty(
StyleableProperties.ICON,
this,
"icon",
""
) {
@Override
protected void invalidated() {
String desc = get();
if (!icon.isBound()) setIcon(desc);
}
@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;
}
};
private final StyleableBooleanProperty enableRipple = new SimpleStyleableBooleanProperty(
StyleableProperties.ENABLE_RIPPLE,
this,
"enableRipple",
false
) {
@Override
protected void invalidated() {
boolean state = get();
if (!state && rg != null) enableRippleGenerator(false);
if (state && rg == null) enableRippleGenerator(true);
updateChildren();
}
@Override
public StyleOrigin getStyleOrigin() {
return StyleOrigin.USER_AGENT;
}
};
private final StyleableBooleanProperty round = new SimpleStyleableBooleanProperty(
StyleableProperties.ROUND,
this,
"round",
false
) {
@Override
protected void invalidated() {
boolean state = get();
makeRound(state);
}
@Override
public StyleOrigin getStyleOrigin() {
return StyleOrigin.USER_AGENT;
}
};
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);
}
protected String getIconDescription() {
return iconDescription.get();
}
/**
* This property allows changing the icon instance from CSS and as a consequence, use icon transitions without the
* need of code. For this reason, the property is {@code protected}, this is intended for CSS use only!
*
* The relative CSS property is: '-mfx-icon'.
*/
protected StyleableStringProperty iconDescriptionProperty() {
return iconDescription;
}
protected void setIconDescription(String iconDescription) {
this.iconDescription.set(iconDescription);
}
public double getSize() {
return size.get();
}
/**
* Specifies the size of this container, when set to -1 it will figure out the value automatically at layout time.
*
* Settable in CSS via the property: '-mfx-size'.
*/
public StyleableDoubleProperty sizeProperty() {
return size;
}
public MFXIconWrapper setSize(double size) {
this.size.set(size);
return this;
}
public boolean isEnableRipple() {
return enableRipple.get();
}
/**
* A useful property to enable ripple effect from CSS, the property will automatically call
* {@link #enableRippleGenerator(boolean)}.
*
* Settable in CSS via the property: '-mfx-enable-ripple'.
*/
public StyleableBooleanProperty enableRippleProperty() {
return enableRipple;
}
public MFXIconWrapper setEnableRipple(boolean enableRipple) {
this.enableRipple.set(enableRipple);
return this;
}
public boolean isRound() {
return round.get();
}
/**
* A useful property to make this container round from CSS, the property will automatically call
* {@link #makeRound(boolean)}.
*
* Settable in CSS via the property: '-mfx-round'.
*/
public StyleableBooleanProperty roundProperty() {
return round;
}
public MFXIconWrapper setRound(boolean round) {
this.round.set(round);
return this;
}
//================================================================================
// CssMetaData
//================================================================================
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 ICON =
FACTORY.createStringCssMetaData(
"-mfx-icon",
MFXIconWrapper::iconDescriptionProperty,
""
);
private static final CssMetaData SIZE =
FACTORY.createSizeCssMetaData(
"-mfx-size",
MFXIconWrapper::sizeProperty,
-1.0
);
private static final CssMetaData ENABLE_RIPPLE =
FACTORY.createBooleanCssMetaData(
"-mfx-enable-ripple",
MFXIconWrapper::enableRippleProperty,
false
);
private static final CssMetaData ROUND =
FACTORY.createBooleanCssMetaData(
"-mfx-round",
MFXIconWrapper::roundProperty,
false
);
static {
List> data = new ArrayList<>(StackPane.getClassCssMetaData());
Collections.addAll(data, ANIMATED, ANIMATION_PRESET, ICON, SIZE, ENABLE_RIPPLE, ROUND);
cssMetaDataList = List.copyOf(data);
}
}
public static List> getClassCssMetaData() {
return StyleableProperties.cssMetaDataList;
}
@Override
public List> getCssMetaData() {
return MFXIconWrapper.getClassCssMetaData();
}
//================================================================================
// Getters/Setters
//================================================================================
/**
* @return the {@link MFXRippleGenerator} instance for this wrapper, note that
* if the generator is not enabled this will return null
*/
public MFXRippleGenerator getRippleGenerator() {
return rg;
}
public MFXFontIcon getIcon() {
return icon.get();
}
/**
* Specifies the currently contained {@link MFXFontIcon}.
*/
public IconProperty iconProperty() {
return icon;
}
public MFXIconWrapper setIcon(MFXFontIcon icon) {
this.icon.set(icon);
return this;
}
/**
* Convenience method to set the {@link #iconProperty()} to a new {@link MFXFontIcon} instance given an icon
* description/name.
*/
public MFXIconWrapper setIcon(String desc) {
setIcon(new MFXFontIcon(desc));
return this;
}
/**
* Convenience method to set the {@link #iconProperty()} to a new {@link MFXFontIcon} instance given an {@link IconProvider} and
* an icon description/name.
*/
public MFXIconWrapper setIcon(IconProvider provider, String desc) {
setIcon(new MFXFontIcon().setIconsProvider(provider).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);
}
}
}