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

com.jfoenix.controls.JFXRippler Maven / Gradle / Ivy

There is a newer version: 9.0.10
Show newest version
/*
 * 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 javafx.animation.*;
import javafx.beans.DefaultProperty;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.css.*;
import javafx.css.converter.BooleanConverter;
import javafx.css.converter.PaintConverter;
import javafx.css.converter.SizeConverter;
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; protected 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; } protected void setOverLayBounds(Rectangle overlay) { overlay.setWidth(control.getLayoutBounds().getWidth()); overlay.setHeight(control.getLayoutBounds().getHeight()); } /** * 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() { if (!isRipplerDisabled()) { rippler.setGeneratorCenterX(control.getLayoutBounds().getWidth() / 2); rippler.setGeneratorCenterY(control.getLayoutBounds().getHeight() / 2); rippler.createRipple(); return () -> { // create fade out transition for the ripple releaseRipple(); }; } return () -> { }; } /** * 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); overlayRect.fillProperty().bind(Bindings.createObjectBinding(() -> { if (ripplerFill.get() instanceof Color) { return new Color(((Color) ripplerFill.get()).getRed(), ((Color) ripplerFill.get()).getGreen(), ((Color) ripplerFill.get()).getBlue(), 0.2); } else { return Color.TRANSPARENT; } }, ripplerFill)); } } 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(); setOverLayBounds(this); 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); setSmooth(true); 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(); rippler.overlayRect = null; 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<>(StackPane.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; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy