com.jfoenix.controls.JFXRippler Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.jfoenix.controls;
import com.jfoenix.converters.RipplerMaskTypeConverter;
import com.jfoenix.utils.JFXNodeUtils;
import com.sun.javafx.css.converters.BooleanConverter;
import com.sun.javafx.css.converters.PaintConverter;
import com.sun.javafx.css.converters.SizeConverter;
import javafx.animation.*;
import javafx.beans.DefaultProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.geometry.Bounds;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Rectangle;
import javafx.scene.shape.Shape;
import javafx.util.Duration;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* JFXRippler is the material design implementation of a ripple effect.
* the ripple effect can be applied to any node in the scene. JFXRippler is
* a {@link StackPane} container that holds a specified node (control node) and a ripple generator.
*
* UPDATE NOTES:
* - fireEventProgrammatically(Event) method has been removed as the ripple controller is
* the control itself, so you can trigger manual ripple by firing mouse event on the control
* instead of JFXRippler
*
* @author Shadi Shaheen
* @version 1.0
* @since 2016-03-09
*/
@DefaultProperty(value = "control")
public class JFXRippler extends StackPane {
public enum RipplerPos {
FRONT, BACK
}
public enum RipplerMask {
CIRCLE, RECT, FIT
}
protected RippleGenerator rippler;
protected Pane ripplerPane;
protected Node control;
private static final double RIPPLE_MAX_RADIUS = 300;
private boolean enabled = true;
private boolean forceOverlay = false;
private Interpolator rippleInterpolator = Interpolator.SPLINE(0.0825,
0.3025,
0.0875,
0.9975); //0.1, 0.54, 0.28, 0.95);
/**
* creates empty rippler node
*/
public JFXRippler() {
this(null, RipplerMask.RECT, RipplerPos.FRONT);
}
/**
* creates a rippler for the specified control
*
* @param control
*/
public JFXRippler(Node control) {
this(control, RipplerMask.RECT, RipplerPos.FRONT);
}
/**
* creates a rippler for the specified control
*
* @param control
* @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
*/
public JFXRippler(Node control, RipplerPos pos) {
this(control, RipplerMask.RECT, pos);
}
/**
* creates a rippler for the specified control and apply the specified mask to it
*
* @param control
* @param mask can be either rectangle/cricle
*/
public JFXRippler(Node control, RipplerMask mask) {
this(control, mask, RipplerPos.FRONT);
}
/**
* creates a rippler for the specified control, mask and position.
*
* @param control
* @param mask can be either rectangle/cricle
* @param pos can be either FRONT/BACK (position the ripple effect infront of or behind the control)
*/
public JFXRippler(Node control, RipplerMask mask, RipplerPos pos) {
initialize();
setMaskType(mask);
setPosition(pos);
createRippleUI();
setControl(control);
// listen to control position changed
position.addListener(observable -> updateControlPosition());
setPickOnBounds(false);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
}
protected final void createRippleUI() {
// create rippler panels
rippler = new RippleGenerator();
ripplerPane = new StackPane();
ripplerPane.setMouseTransparent(true);
ripplerPane.getChildren().add(rippler);
getChildren().add(ripplerPane);
}
/***************************************************************************
* *
* Setters / Getters *
* *
**************************************************************************/
public void setControl(Node control) {
if (control != null) {
this.control = control;
// position control
positionControl(control);
// add control listeners to generate / release ripples
initControlListeners();
}
}
// Override this method to create JFXRippler for a control outside the ripple
protected void positionControl(Node control) {
if(this.position.get() == RipplerPos.BACK){
getChildren().add(control);
}else{
getChildren().add(0, control);
}
}
protected void updateControlPosition() {
if (this.position.get() == RipplerPos.BACK) {
ripplerPane.toBack();
} else {
ripplerPane.toFront();
}
}
public Node getControl() {
return control;
}
public void setEnabled(boolean enable) {
this.enabled = enable;
}
// methods that can be changed by extending the rippler class
/**
* generate the clipping mask
* @return the mask node
*/
protected Node getMask() {
double borderWidth = ripplerPane.getBorder() != null ? ripplerPane.getBorder().getInsets().getTop() : 0;
Bounds bounds = control.getBoundsInParent();
double width = control.getLayoutBounds().getWidth();
double height = control.getLayoutBounds().getHeight();
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
double diffMaxX = Math.abs(control.getBoundsInLocal().getMaxX() - control.getLayoutBounds().getMaxX());
double diffMaxY = Math.abs(control.getBoundsInLocal().getMaxY() - control.getLayoutBounds().getMaxY());
Node mask;
switch (getMaskType()) {
case RECT:
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
bounds.getMinY() + diffMinY - snappedTopInset(),
width - 2 * borderWidth,
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
break;
case CIRCLE:
double radius = Math.min((width / 2) - 2 * borderWidth, (height / 2) - 2 * borderWidth);
mask = new Circle((bounds.getMinX() + diffMinX + bounds.getMaxX() - diffMaxX) / 2 - snappedLeftInset(),
(bounds.getMinY() + diffMinY + bounds.getMaxY() - diffMaxY) / 2 - snappedTopInset(),
radius,
Color.BLUE);
break;
case FIT:
mask = new Region();
if(control instanceof Shape){
((Region) mask).setShape((Shape) control);
}else if(control instanceof Region){
((Region) mask).setShape(((Region) control).getShape());
JFXNodeUtils.updateBackground(((Region) control).getBackground(), (Region) mask);
}
mask.resize(width, height);
mask.relocate(bounds.getMinX() + diffMinX, bounds.getMinY() + diffMinY);
break;
default:
mask = new Rectangle(bounds.getMinX() + diffMinX - snappedLeftInset(),
bounds.getMinY() + diffMinY - snappedTopInset(),
width - 2 * borderWidth,
height - 2 * borderWidth); // -0.1 to prevent resizing the anchor pane
break;
}
return mask;
}
/**
* compute the ripple radius
* @return the ripple radius size
*/
protected double computeRippleRadius() {
double width2 = control.getLayoutBounds().getWidth() * control.getLayoutBounds().getWidth();
double height2 = control.getLayoutBounds().getHeight() * control.getLayoutBounds().getHeight();
return Math.min(Math.sqrt(width2 + height2), RIPPLE_MAX_RADIUS) * 1.1 + 5;
}
/**
* init mouse listeners on the control
*/
protected void initControlListeners() {
// if the control got resized the overlay rect must be rest
control.layoutBoundsProperty().addListener(observable -> resetRippler());
if(getChildren().contains(control))
control.boundsInParentProperty().addListener(observable -> resetRippler());
control.addEventHandler(MouseEvent.MOUSE_PRESSED,
(event) -> createRipple(event.getX(), event.getY()));
// create fade out transition for the ripple
control.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> releaseRipple());
}
/**
* creates Ripple effect
*/
protected void createRipple(double x, double y) {
if(!isRipplerDisabled()) {
rippler.setGeneratorCenterX(x);
rippler.setGeneratorCenterY(y);
rippler.createRipple();
}
}
protected void releaseRipple() {
rippler.releaseRipple();
}
/**
* creates Ripple effect in the center of the control
* @return a runnable to release the ripple when needed
*/
public Runnable createManualRipple() {
rippler.setGeneratorCenterX(control.getLayoutBounds().getWidth() / 2);
rippler.setGeneratorCenterY(control.getLayoutBounds().getHeight() / 2);
rippler.createRipple();
return () -> {
// create fade out transition for the ripple
releaseRipple();
};
}
/**
* show/hide the ripple overlay
* @param visible
* @param forceOverlay used to hold the overlay after ripple action
*/
public void setOverlayVisible(boolean visible, boolean forceOverlay){
this.forceOverlay = forceOverlay;
setOverlayVisible(visible);
}
/**
* show/hide the ripple overlay
* NOTE: setting overlay visibility to false will reset forceOverlay to false
* @param visible
*/
public void setOverlayVisible(boolean visible){
if(visible){
showOverlay();
}else{
forceOverlay = !visible ? false : forceOverlay;
hideOverlay();
}
}
/**
* this method will be set to private in future versions of JFoenix,
* user the method {@link #setOverlayVisible(boolean)}
*/
@Deprecated
public void showOverlay() {
if (rippler.overlayRect != null) {
rippler.overlayRect.outAnimation.stop();
}
rippler.createOverlay();
rippler.overlayRect.inAnimation.play();
}
@Deprecated
public void hideOverlay() {
if(!forceOverlay){
if (rippler.overlayRect != null) {
rippler.overlayRect.inAnimation.stop();
}
if (rippler.overlayRect != null) {
rippler.overlayRect.outAnimation.play();
}
}else{
System.err.println("Ripple Overlay is forced!");
}
}
/**
* Generates ripples on the screen every 0.3 seconds or whenever
* the createRipple method is called. Ripples grow and fade out
* over 0.6 seconds
*/
final class RippleGenerator extends Group {
private double generatorCenterX = 0;
private double generatorCenterY = 0;
private OverLayRipple overlayRect;
private AtomicBoolean generating = new AtomicBoolean(false);
private boolean cacheRipplerClip = false;
private boolean resetClip = false;
private Queue ripplesQueue = new LinkedList();
RippleGenerator() {
// improve in performance, by preventing
// redrawing the parent when the ripple effect is triggered
this.setManaged(false);
this.setCache(true);
this.setCacheHint(CacheHint.SPEED);
}
void createRipple() {
if (enabled) {
if (!generating.getAndSet(true)) {
// create overlay once then change its color later
createOverlay();
if (this.getClip() == null || (getChildren().size() == 1 && !cacheRipplerClip) || resetClip) {
this.setClip(getMask());
}
this.resetClip = false;
// create the ripple effect
final Ripple ripple = new Ripple(generatorCenterX, generatorCenterY);
getChildren().add(ripple);
ripplesQueue.add(ripple);
// animate the ripple
overlayRect.outAnimation.stop();
overlayRect.inAnimation.play();
ripple.inAnimation.play();
}
}
}
private void releaseRipple() {
Ripple ripple = ripplesQueue.poll();
if(ripple!=null) {
ripple.inAnimation.stop();
ripple.outAnimation = new Timeline(
new KeyFrame(Duration.millis(Math.min(800, (0.9 * 500) / ripple.getScaleX()))
, ripple.outKeyValues));
ripple.outAnimation.setOnFinished((event) -> getChildren().remove(ripple));
ripple.outAnimation.play();
if (generating.getAndSet(false)) {
if (overlayRect != null) {
overlayRect.inAnimation.stop();
if (!forceOverlay)
overlayRect.outAnimation.play();
}
}
}
}
void cacheRippleClip(boolean cached) {
cacheRipplerClip = cached;
}
void createOverlay() {
if (overlayRect == null) {
overlayRect = new OverLayRipple();
overlayRect.setClip(getMask());
getChildren().add(0, overlayRect);
if (ripplerFill.get() instanceof Color) {
overlayRect.setFill(new Color(((Color) ripplerFill.get()).getRed(),
((Color) ripplerFill.get()).getGreen(),
((Color) ripplerFill.get()).getBlue(),
0.2));
}else{
overlayRect.setFill(Color.TRANSPARENT);
}
}
}
void setGeneratorCenterX(double generatorCenterX) {
this.generatorCenterX = generatorCenterX;
}
void setGeneratorCenterY(double generatorCenterY) {
this.generatorCenterY = generatorCenterY;
}
private final class OverLayRipple extends Rectangle {
// Overlay ripple animations
Animation inAnimation = new Timeline(new KeyFrame(Duration.millis(300),
new KeyValue(opacityProperty(), 1, Interpolator.EASE_IN)));
Animation outAnimation = new Timeline(new KeyFrame(Duration.millis(300),
new KeyValue(opacityProperty(), 0, Interpolator.EASE_OUT)));
OverLayRipple() {
super(control.getLayoutBounds().getWidth(), control.getLayoutBounds().getHeight());
this.getStyleClass().add("jfx-rippler-overlay");
// update initial position
if(JFXRippler.this.getChildrenUnmodifiable().contains(control)) {
double diffMinX = Math.abs(control.getBoundsInLocal().getMinX() - control.getLayoutBounds().getMinX());
double diffMinY = Math.abs(control.getBoundsInLocal().getMinY() - control.getLayoutBounds().getMinY());
Bounds bounds = control.getBoundsInParent();
this.setX(bounds.getMinX() + diffMinX - snappedLeftInset());
this.setY(bounds.getMinY() + diffMinY - snappedTopInset());
}
// set initial attributes
setOpacity(0);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
setManaged(false);
}
}
private final class Ripple extends Circle {
KeyValue[] outKeyValues;
Animation outAnimation = null;
Animation inAnimation = null;
private Ripple(double centerX, double centerY) {
super(centerX,
centerY,
ripplerRadius.get().doubleValue() == Region.USE_COMPUTED_SIZE ?
computeRippleRadius() : ripplerRadius.get().doubleValue(), null);
setCache(true);
setCacheHint(CacheHint.SPEED);
setCacheShape(true);
setManaged(false);
KeyValue[] inKeyValues = new KeyValue[isRipplerRecenter() ? 4 : 2];
outKeyValues = new KeyValue[isRipplerRecenter() ? 5 : 3];
inKeyValues[0] = new KeyValue(scaleXProperty(), 0.9, rippleInterpolator);
inKeyValues[1] = new KeyValue(scaleYProperty(), 0.9, rippleInterpolator);
outKeyValues[0] = new KeyValue(this.scaleXProperty(), 1, rippleInterpolator);
outKeyValues[1] = new KeyValue(this.scaleYProperty(), 1, rippleInterpolator);
outKeyValues[2] = new KeyValue(this.opacityProperty(), 0, rippleInterpolator);
if (isRipplerRecenter()) {
double dx = (control.getLayoutBounds().getWidth() / 2 - centerX) / 1.55;
double dy = (control.getLayoutBounds().getHeight() / 2 - centerY) / 1.55;
inKeyValues[2] = outKeyValues[3] = new KeyValue(translateXProperty(),
Math.signum(dx) * Math.min(Math.abs(dx),
this.getRadius() / 2),
rippleInterpolator);
inKeyValues[3] = outKeyValues[4] = new KeyValue(translateYProperty(),
Math.signum(dy) * Math.min(Math.abs(dy),
this.getRadius() / 2),
rippleInterpolator);
}
inAnimation = new Timeline(new KeyFrame(Duration.ZERO,
new KeyValue(scaleXProperty(),
0,
rippleInterpolator),
new KeyValue(scaleYProperty(),
0,
rippleInterpolator),
new KeyValue(translateXProperty(),
0,
rippleInterpolator),
new KeyValue(translateYProperty(),
0,
rippleInterpolator),
new KeyValue(opacityProperty(),
1,
rippleInterpolator)
), new KeyFrame(Duration.millis(900), inKeyValues));
setScaleX(0);
setScaleY(0);
if (ripplerFill.get() instanceof Color) {
Color circleColor = new Color(((Color) ripplerFill.get()).getRed(),
((Color) ripplerFill.get()).getGreen(),
((Color) ripplerFill.get()).getBlue(),
0.3);
setStroke(circleColor);
setFill(circleColor);
} else {
setStroke(ripplerFill.get());
setFill(ripplerFill.get());
}
}
}
public void clear() {
getChildren().clear();
generating.set(false);
}
}
private void resetOverLay() {
if (rippler.overlayRect != null) {
rippler.overlayRect.inAnimation.stop();
final RippleGenerator.OverLayRipple oldOverlay = rippler.overlayRect;
rippler.overlayRect.outAnimation.setOnFinished((finish) -> rippler.getChildren().remove(oldOverlay));
rippler.overlayRect.outAnimation.play();
rippler.overlayRect = null;
}
}
private void resetClip() {
this.rippler.resetClip = true;
}
protected void resetRippler() {
resetOverLay();
resetClip();
}
/***************************************************************************
* *
* Stylesheet Handling *
* *
**************************************************************************/
/**
* Initialize the style class to 'jfx-rippler'.
*
* This is the selector class from which CSS can be used to style
* this control.
*/
private static final String DEFAULT_STYLE_CLASS = "jfx-rippler";
private void initialize() {
this.getStyleClass().add(DEFAULT_STYLE_CLASS);
}
/**
* the ripple recenter property, by default it's false.
* if true the ripple effect will show gravitational pull to the center of its control
*/
private StyleableObjectProperty ripplerRecenter = new SimpleStyleableObjectProperty<>(
StyleableProperties.RIPPLER_RECENTER,
JFXRippler.this,
"ripplerRecenter",
false);
public Boolean isRipplerRecenter() {
return ripplerRecenter == null ? false : ripplerRecenter.get();
}
public StyleableObjectProperty ripplerRecenterProperty() {
return this.ripplerRecenter;
}
public void setRipplerRecenter(Boolean radius) {
this.ripplerRecenter.set(radius);
}
/**
* the ripple radius size, by default it will be automatically computed.
*/
private StyleableObjectProperty ripplerRadius = new SimpleStyleableObjectProperty<>(
StyleableProperties.RIPPLER_RADIUS,
JFXRippler.this,
"ripplerRadius",
Region.USE_COMPUTED_SIZE);
public Number getRipplerRadius() {
return ripplerRadius == null ? Region.USE_COMPUTED_SIZE : ripplerRadius.get();
}
public StyleableObjectProperty ripplerRadiusProperty() {
return this.ripplerRadius;
}
public void setRipplerRadius(Number radius) {
this.ripplerRadius.set(radius);
}
/**
* the default color of the ripple effect
*/
private StyleableObjectProperty ripplerFill = new SimpleStyleableObjectProperty<>(StyleableProperties.RIPPLER_FILL,
JFXRippler.this,
"ripplerFill",
Color.rgb(0,
200,
255));
public Paint getRipplerFill() {
return ripplerFill == null ? Color.rgb(0, 200, 255) : ripplerFill.get();
}
public StyleableObjectProperty ripplerFillProperty() {
return this.ripplerFill;
}
public void setRipplerFill(Paint color) {
this.ripplerFill.set(color);
}
/**
* mask property used for clipping the rippler.
* can be either CIRCLE/RECT
*/
private StyleableObjectProperty maskType = new SimpleStyleableObjectProperty<>(
StyleableProperties.MASK_TYPE,
JFXRippler.this,
"maskType",
RipplerMask.RECT);
public RipplerMask getMaskType() {
return maskType == null ? RipplerMask.RECT : maskType.get();
}
public StyleableObjectProperty maskTypeProperty() {
return this.maskType;
}
public void setMaskType(RipplerMask type) {
this.maskType.set(type);
}
/**
* the ripple disable, by default it's false.
* if true the ripple effect will be hidden
*/
private StyleableBooleanProperty ripplerDisabled = new SimpleStyleableBooleanProperty(
StyleableProperties.RIPPLER_DISABLED,
JFXRippler.this,
"ripplerDisabled",
false);
public Boolean isRipplerDisabled() {
return ripplerDisabled == null ? false : ripplerDisabled.get();
}
public StyleableBooleanProperty ripplerDisabledProperty() {
return this.ripplerDisabled;
}
public void setRipplerDisabled(Boolean disabled) {
this.ripplerDisabled.set(disabled);
}
/**
* indicates whether the ripple effect is infront of or behind the node
*/
protected ObjectProperty position = new SimpleObjectProperty<>();
public void setPosition(RipplerPos pos) {
this.position.set(pos);
}
public RipplerPos getPosition() {
return position == null ? RipplerPos.FRONT : position.get();
}
public ObjectProperty positionProperty() {
return this.position;
}
private static final class StyleableProperties {
private static final CssMetaData RIPPLER_RECENTER =
new CssMetaData("-jfx-rippler-recenter",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerRecenter == null || !control.ripplerRecenter.isBound();
}
@Override
public StyleableProperty getStyleableProperty(JFXRippler control) {
return control.ripplerRecenterProperty();
}
};
private static final CssMetaData RIPPLER_DISABLED =
new CssMetaData("-jfx-rippler-disabled",
BooleanConverter.getInstance(), false) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerDisabled == null || !control.ripplerDisabled.isBound();
}
@Override
public StyleableProperty getStyleableProperty(JFXRippler control) {
return control.ripplerDisabledProperty();
}
};
private static final CssMetaData RIPPLER_FILL =
new CssMetaData("-jfx-rippler-fill",
PaintConverter.getInstance(), Color.rgb(0, 200, 255)) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerFill == null || !control.ripplerFill.isBound();
}
@Override
public StyleableProperty getStyleableProperty(JFXRippler control) {
return control.ripplerFillProperty();
}
};
private static final CssMetaData RIPPLER_RADIUS =
new CssMetaData("-jfx-rippler-radius",
SizeConverter.getInstance(), Region.USE_COMPUTED_SIZE) {
@Override
public boolean isSettable(JFXRippler control) {
return control.ripplerRadius == null || !control.ripplerRadius.isBound();
}
@Override
public StyleableProperty getStyleableProperty(JFXRippler control) {
return control.ripplerRadiusProperty();
}
};
private static final CssMetaData MASK_TYPE =
new CssMetaData("-jfx-mask-type",
RipplerMaskTypeConverter.getInstance(), RipplerMask.RECT) {
@Override
public boolean isSettable(JFXRippler control) {
return control.maskType == null || !control.maskType.isBound();
}
@Override
public StyleableProperty getStyleableProperty(JFXRippler control) {
return control.maskTypeProperty();
}
};
private static final List> STYLEABLES;
static {
final List> styleables =
new ArrayList<>(Parent.getClassCssMetaData());
Collections.addAll(styleables,
RIPPLER_RECENTER,
RIPPLER_RADIUS,
RIPPLER_FILL,
MASK_TYPE,
RIPPLER_DISABLED
);
STYLEABLES = Collections.unmodifiableList(styleables);
}
}
@Override
public List> getCssMetaData() {
return getClassCssMetaData();
}
public static List> getClassCssMetaData() {
return StyleableProperties.STYLEABLES;
}
}