I have a ripple button that once it's clicked, plays a ripple effect animation.
Inside of the button's skin, there's a Circle
whose opacity is set to 0
at the beginning of the execution of the program and once the button is clicked, the opacity is set to 1
and the radius of the button gets larger.
There is a DoubleBinding
binding, whose definition is the following:
DoubleBinding circleRippleRadius = new DoubleBinding() {
{
bind(heightProperty(), widthProperty()); // Sets the parameters of the bind method.
}
@Override // Overrides the computeValue method, which computes the value of the binding.
protected double computeValue() {
return Math.max(heightProperty().get(), widthProperty().get()); // Return the greatest of both numbers
}
};
Instead of using the button's heightProperty
and widthProperty
properties, I'd like to use the height and width of the scene to which the button is added, since I want the circle that appears once the button is clicked to fill the entire screen.
How could I achieve this?
UPDATE: Here is the code that defines the animation's component's values and the animation itself:
private void createRippleEffect(Circle circleRipple) {
circleRipple.setOpacity(1.0); // Sets the opacity of the circleRipple to 0, since it must not be showed yet.
/*Fade Transition*/
FadeTransition fadeTransition = new FadeTransition(RIPPLE_DURATION, circleRipple);
fadeTransition.setInterpolator(Interpolator.EASE_OUT);
fadeTransition.setFromValue(1.0); // Sets the opacity to %100
fadeTransition.setToValue(1.0); // The opacity doesn't change.
/*Scale Transition*/
Timeline scaleRippleTimeline = new Timeline();
NumberBinding circleRippleRadius =
Bindings.max(Bindings.selectDouble(sceneProperty(), "width"),
Bindings.selectDouble(sceneProperty(), "height"));
circleRippleRadius.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> { // Each time it changes
KeyValue scaleValue = new KeyValue(circleRipple.radiusProperty(), newValue, Interpolator.EASE_OUT);
KeyFrame scaleFrame = new KeyFrame(RIPPLE_DURATION, scaleValue);
scaleRippleTimeline.getKeyFrames().add(scaleFrame);
});
private void createRippleEffect(Circle circleRipple) {
circleRipple.setOpacity(1.0); // Sets the opacity of the circleRipple to 0, since it must not be showed yet.
/*Fade Transition*/
FadeTransition fadeTransition = new FadeTransition(RIPPLE_DURATION, circleRipple);
fadeTransition.setInterpolator(Interpolator.EASE_OUT);
fadeTransition.setFromValue(1.0); // Sets the opacity to %100
fadeTransition.setToValue(1.0); // The opacity doesn't change.
/*Scale Transition*/
Timeline scaleRippleTimeline = new Timeline();
NumberBinding circleRippleRadius =
Bindings.max(Bindings.selectDouble(sceneProperty(), "width"),
Bindings.selectDouble(sceneProperty(), "height"));
circleRippleRadius.addListener((ObservableValue<? extends Number> observable, Number oldValue, Number newValue) -> { // Each time it changes
KeyValue scaleValue = new KeyValue(circleRipple.radiusProperty(), newValue, Interpolator.EASE_OUT);
KeyFrame scaleFrame = new KeyFrame(RIPPLE_DURATION, scaleValue);
scaleRippleTimeline.getKeyFrames().add(scaleFrame);
});
SequentialTransition rippleTransition = new SequentialTransition(); // The circle must change its opacity and scale at the same time
rippleTransition.getChildren().addAll(
scaleRippleTimeline,
fadeTransition
);
ParallelTransition parallelTransition = new ParallelTransition();
getStyleClass().addListener((ListChangeListener.Change<? extends String> c) -> { // Don't pay attention to this. The style changes if
// the CSS file has "toggle" or "flat" inside its list,
// but these are never added so it doesn't matter.
if (c.getList().indexOf("flat") == -1 && c.getList().indexOf("toggle") == -1) {
setMinWidth(88);
setEffect(new DropShadow(BlurType.GAUSSIAN, Color.rgb(0, 0, 0, 0.30), 5, 0.10, 0, 2));
parallelTransition.getChildren().addAll(rippleTransition); // parallelTransition is basically the same as rippleTransition, since
// "toggle" and "flat" are never added to the CSS's list.
} else {
parallelTransition.getChildren().addAll(rippleTransition);
setMinWidth(USE_COMPUTED_SIZE);
setEffect(null);
}
});
this.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
parallelTransition.stop(); // In case that the parallelTransition is already running, stop it.
circleRipple.setOpacity(0.0); // Sets the opacity of the circle to 0, since the animation must play from he beginning.
circleRipple.setRadius(0.1); // Sets the radius to 0.1 for the same reason as the circle's opacity.
circleRipple.setCenterX(event.getX()); // The center of the circle is the location in which the mouse was clicked.
circleRipple.setCenterY(event.getY()); // The center of the circle is the location in which the mouse was clicked.
parallelTransition.playFromStart(); // Plays the animation.
});
}
being this the method that defines the button's skin:
@Override
public Skin<?> createDefaultSkin() {
ButtonSkin buttonSkin = getButtonSkin();
if (buttonSkin == null) {
buttonSkin = new ButtonSkin(this);
Circle circleRipple = new Circle(0.1, RIPPLE_COLOR);
buttonSkin.getChildren().add(0, circleRipple);
setSkin(buttonSkin);
createRippleEffect(circleRipple);
getStyleClass().add("ripple-button"); // What the CSS does is changing the button's color and text size, nothing important.
}
return buttonSkin;
}
Thanks in advance.
SOLUTION:
The problem of the circleRippleRadius
's value being shorter than what the stage's one was probably due to that the stage's size was different than the scene's. I didn't know that, now I do.
For having the button's circle filling the entire screen, all I had to do was passing the widthProperty
and heightProperty
properties of the stage as parameters through the button's constructor.
Inside of the button's class, I create two ReadOnlyDoubleProperty
properties for the width and the height that are empty until a button is created; in which case, the values of the defined ReadOnlyDoubleProperty
properties are overwritten by the value of the widthProperty
and heightProperty
properties passed as parameters.
Having done that, all what I have to do is adding a listener to each property and changing the value of the circleRippleRadius
to the greater of the properties' values each time that one of them changes.
You should not be extending DoubleBinding. Instead, you should create bindings from the existing factory methods:
NumberBinding circleRippleRadius =
Bindings.max(widthProperty(), heightProperty());
// Optional
DoubleExpression circleRippleRadiusAsDouble =
DoubleExpression.doubleExpression(circleRippleRadius);
To bind to scene properties, you want to use Bindings.selectDouble, which can handle the initially null scene property of your button:
NumberBinding size =
Bindings.max(
Bindings.selectDouble(button.sceneProperty(), "width"),
Bindings.selectDouble(button.sceneProperty(), "height"));