All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.palexdev.mfxeffects.ripple.CircleRipple Maven / Gradle / Ivy

There is a newer version: 11.26.0
Show newest version
package io.github.palexdev.mfxeffects.ripple;

import io.github.palexdev.mfxeffects.animations.Animations;
import io.github.palexdev.mfxeffects.animations.Animations.KeyFrames;
import io.github.palexdev.mfxeffects.animations.Animations.ParallelBuilder;
import io.github.palexdev.mfxeffects.animations.Animations.PauseBuilder;
import io.github.palexdev.mfxeffects.animations.Animations.TimelineBuilder;
import io.github.palexdev.mfxeffects.animations.ConsumerTransition;
import io.github.palexdev.mfxeffects.animations.motion.Motion;
import io.github.palexdev.mfxeffects.beans.Offset;
import io.github.palexdev.mfxeffects.beans.Size;
import io.github.palexdev.mfxeffects.ripple.base.Ripple;
import io.github.palexdev.mfxeffects.ripple.base.RippleGenerator;
import io.github.palexdev.mfxeffects.ripple.base.RippleGeneratorBase;
import io.github.palexdev.mfxeffects.utils.ColorUtils;
import javafx.animation.Animation;
import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.PauseTransition;
import javafx.scene.layout.Background;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.util.Duration;

/**
 * Implementation of {@link Ripple} and most common type of ripple as it also extends {@link Circle}.
 * This complies with the new behavior and features of the new {@link MFXRippleGenerator}.
 * 

* It's responsible for animating the 'generation' phase, the 'release' phase, as well as animating in/out the * generator's background color. Most of the properties that customize the animations, such as their durations and * interpolation curves, are made protected so that they can be easily changed by extending the class, even inline. * When doing so, make sure that the animations are up-to-date with the new parameters by calling {@link #buildAnimations()}. *

* Now the complex part: How this works? *

* Well, I want to re-assert here too, that the new implementation tries to stay as close as possible to the original * effect shown in Material Design, and accomplishing such accuracy has been really tough. *

* There are 4 animations in total that are built by {@link #buildAnimations()}: *

1) The {@code radIn} animation is responsible for expanding the ripple, as well as translating it towards the * generator's center. I'll describe why later on when talking about the ripple sizes. *

2) The {@code fadeIn} animation is responsible for increasing the ripple opacity to 1.0 over time, and also * for animating the generator's background to the color specified by {@link RippleGenerator#backgroundColorProperty()} *

3) The {@code fadeOut} animation is responsible for decreasing the ripple opacity to 0.0 over time, and also * for animating the generator's background, the color specified by {@link RippleGenerator#backgroundColorProperty()} will * have the {@code alpha} gradually set to 0.0 *

4) The {@code pause} animation. This is a very important animation of type {@link PauseTransition}. * This helps make things smoother and consistent across various input devices (touchscreens, trackpads, mouse). * If we don't use this animation you will see an ugly effect because values will change very fast. And this is especially * true for touch devices (touchscreens or trackpads) that have a very low latency compared to mouse. To mitigate this, * we ideally want the 'in' animations to play for at least some time before actually passing to the 'release' phase, * by default that time is of 150ms. So, the actual duration of this 'pause' animations is given by 150 minus the current * time of the {@code radIn} animation. Consider the following two examples: *

1) Let's say I use my mouse to generate the ripple. In my testings the average latency is between 20ms/50ms. * In such case the 'pause' animation will have a duration between 130ms/100ms *

2) Now let's consider an example with a touch device. In my testings the average latency never goes up the 3ms/5ms. * Which means that the 'release' phase will occur only after 140ms/150ms (approx.) *

* The effect will always be the same, no matter the input device. *

* The next question is: Why separate 'in' animations for the radius and the opacity? *

* Pretty much the same reason as above, to make things smoother. When we have to pass to the 'release' phase, we have * to also stop the 'in' animations. However, we don't want to stop the radius animation as this would result in a * ugly/strange effect. We just want to stop the {@code fadeIn} animation as it would conflict with the * {@code fadeOut} that is going to be played next *

* Last but not least: the ripple sizing and positioning. *

* The algorithm responsible for determining the ripple size has also been updated to match more closely the one shown * by Material Design Guidelines. It's quite simple, there are now two sizes: *

- The {@code initRad} is the radius of the ripple at the start, set just before the 'in' animations are played. * This is computed to be tha maximum between the generator's width and height, and then multiplied by the {@link #INIT_RAD_MULTIPLIER} * factor that by default makes it the 20% of the found max *

- The {@code targetRad} is the final radius the ripple will have once the {@code radIn} animation finishes. * This is computed as the diagonal size of the generator divided by 2 since we want the radius, and plus 5 to * make it smoother. Now as you may guess, if the ripple is generated at the center of the generator than it will cover * all of its surface. But if the position is near or at one of the corners than the target size won't be enough. * And that's why the devs behind the ripple effect came up with a very nice solution. The ripple is also moved towards the * center, but in combination with the growth animation, the user won't perceive the translation, and instead he will * perceive it as just a big growth. The implications of such trick are not to be underestimated, because this has a * huge impact on the {@code radIn} animation as smaller values also mean slower animation which is balanced by a small * duration and an ease interpolator. The other pro of such trick is probably performance, now the framework doesn't need * to draw a huge circle anymore, despite this being unconfirmed I strongly believe this will benefit performance! */ public class CircleRipple extends Circle implements Ripple { //================================================================================ // Properties //================================================================================ private final RippleGeneratorBase generator; protected double INIT_RAD_MULTIPLIER = 0.2; protected double initRad; protected double targetRad; protected double initX = 0; protected double initY = 0; protected Interpolator CURVE = Motion.EASE; protected Duration RAD_IN = Duration.millis(300); protected Duration FADE_IN = Duration.millis(100); protected Duration FADE_OUT = Duration.millis(300); protected Duration BG = Duration.millis(300); protected double MIN_IN_MILLIS = 150.0; private Animation radIn; private Animation fadeIn; private Animation fadeOut; private Animation pause; //================================================================================ // Constructors //================================================================================ public CircleRipple(RippleGeneratorBase generator) { this.generator = generator; } //================================================================================ // Methods //================================================================================ /** * Sets the ripple's fill to {@link RippleGenerator#rippleColorProperty()}, determines the sizes by calling * {@link #determineRippleSize()} and finally initializes the animations by calling {@link #buildAnimations()} */ public void init() { setFill(generator.getRippleColor()); determineRippleSize(); buildAnimations(); } /** * Responsible for building all the animations described by {@link CircleRipple}. *

* Note that the background animations will be built and added only if {@link RippleGenerator#animateBackgroundProperty()} * is true. */ protected void buildAnimations() { KeyFrame radInKF = KeyFrames.of(RAD_IN, radiusProperty(), targetRad, CURVE); KeyFrame xInKF = KeyFrames.of(RAD_IN, centerXProperty(), generator.getLayoutBounds().getCenterX(), CURVE); KeyFrame yInKF = KeyFrames.of(RAD_IN, centerYProperty(), generator.getLayoutBounds().getCenterY(), CURVE); radIn = TimelineBuilder.build() .add(radInKF) .add(xInKF) .add(yInKF) .getAnimation(); boolean animateBackground = generator.doAnimateBackground(); Color bgColor = generator.getBackgroundColor(); KeyFrame fadeInKF = KeyFrames.of(FADE_IN, opacityProperty(), 1.0); KeyFrame fadeOutKF = KeyFrames.of(FADE_OUT, opacityProperty(), 0.0); if (animateBackground) { fadeIn = ParallelBuilder.build() .add(fadeInKF) .add(() -> ConsumerTransition.of(dt -> { double alpha = dt * bgColor.getOpacity(); Color color = ColorUtils.atAlpha(bgColor, alpha); generator.setBackground(Background.fill(color)); }, BG).setInterpolatorFluent(CURVE)) .getAnimation(); fadeOut = ParallelBuilder.build() .add(fadeOutKF) .add(() -> ConsumerTransition.of(dt -> { double bgAlpha = bgColor.getOpacity(); double alpha = bgAlpha - (dt * bgAlpha); Color color = ColorUtils.atAlpha(bgColor, alpha); generator.setBackground(Background.fill(color)); }, BG).setInterpolatorFluent(CURVE)) .getAnimation(); } else { fadeIn = TimelineBuilder.build() .add(fadeInKF) .getAnimation(); fadeOut = TimelineBuilder.build() .add(fadeOutKF) .getAnimation(); } } /** * Determines the ripple's initial radius and target radius as described by {@link CircleRipple}. */ protected void determineRippleSize() { Size pref = generator.getRipplePrefSize(); if (!Size.invalid().equals(pref)) { initRad = 0; targetRad = Math.max(pref.getWidth(), pref.getWidth()); return; } double w = generator.getWidth(); double h = generator.getHeight(); double diag = new Offset(w, h).getDistance(); initRad = Math.floor(Math.max(w, h) * INIT_RAD_MULTIPLIER); targetRad = diag / 2 + 5; } /** * The {@link #position(double, double)} method specified by the public API is automatically called by the generator * when a request for a new effect is sent. However, we still don't want to change the position at such time, we first * need to stop the 'out' and 'pause' animations. *

* This is responsible for setting the opacity to 0.0, the radius to {@code initRad} computed previously by {@link #determineRippleSize()}, * and finally set the {@link #centerXProperty()} and {@link #centerYProperty()} properties to the requested position, * the two values are stored by {@link #position(double, double)} and then actually used here. */ protected void doPosition() { setOpacity(0.0); setRadius(initRad); setCenterX(initX); setCenterY(initY); } //================================================================================ // Overridden Methods //================================================================================ @Override public Circle toNode() { return this; } /** * This just saves the new ripple position as class variables, see {@link #doPosition()}. */ @Override public void position(double x, double y) { initX = x; initY = y; } /** * Responsible for playing the animations for the 'generation' phase. *

* First of all we need to stop both the 'pause' and 'out' animations, then we reposition the ripple with * {@link #doPosition()} and finally we can start both the {@code fadeIn} and {@code radIn} animations by * using {@link Animation#playFromStart()}. */ @Override public void playIn() { if (Animations.isPlaying(pause)) pause.stop(); fadeOut.stop(); doPosition(); fadeIn.playFromStart(); radIn.playFromStart(); } @Override public void playOut() { double ct = radIn.getCurrentTime().toMillis(); double delay = MIN_IN_MILLIS - ct; if (delay > 0) { if (pause != null) pause.stop(); pause = PauseBuilder.build() .setDuration(delay) .setOnFinished(e -> { fadeIn.stop(); fadeOut.playFromStart(); }) .getAnimation(); pause.play(); return; } fadeOut.playFromStart(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy