JavaFX button with ripple effect

I watched material design principle from google and I wanted to try implement some of it in JavaFX.
At first I tried to create ripple effect. I picked button class, learn internal code from JavaFX and this is result:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
public class MaterialDesignButton extends Button {
private Circle circleRipple;
private Rectangle rippleClip = new Rectangle();
private Duration rippleDuration = Duration.millis(250);
private double lastRippleHeight = 0;
private double lastRippleWidth = 0;
private Color rippleColor = new Color(0, 0, 0, 0.11);
public MaterialDesignButton(String text) {
super(text);
getStyleClass().addAll("md-button");
createRippleEffect();
}
@Override
protected Skin<?> createDefaultSkin() {
final ButtonSkin buttonSkin = new ButtonSkin(this);
// Adding circleRipple as fist node of button nodes to be on the bottom
this.getChildren().add(0, circleRipple);
return buttonSkin;
}
private void createRippleEffect() {
circleRipple = new Circle(0.1, rippleColor);
circleRipple.setOpacity(0.0);
// Optional box blur on ripple - smoother ripple effect
//circleRipple.setEffect(new BoxBlur(3, 3, 2));
// Fade effect bit longer to show edges on the end of animation
final FadeTransition fadeTransition = new FadeTransition(rippleDuration, circleRipple);
fadeTransition.setInterpolator(Interpolator.EASE_OUT);
fadeTransition.setFromValue(1.0);
fadeTransition.setToValue(0.0);
final Timeline scaleRippleTimeline = new Timeline();
final SequentialTransition parallelTransition = new SequentialTransition();
parallelTransition.getChildren().addAll(
scaleRippleTimeline,
fadeTransition
);
// When ripple transition is finished then reset circleRipple to starting point
parallelTransition.setOnFinished(event -> {
circleRipple.setOpacity(0.0);
circleRipple.setRadius(0.1);
});
this.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
parallelTransition.stop();
// Manually fire finish event
parallelTransition.getOnFinished().handle(null);
circleRipple.setCenterX(event.getX());
circleRipple.setCenterY(event.getY());
// Recalculate ripple size if size of button from last time was changed
if (getWidth() != lastRippleWidth || getHeight() != lastRippleHeight)
{
lastRippleWidth = getWidth();
lastRippleHeight = getHeight();
rippleClip.setWidth(lastRippleWidth);
rippleClip.setHeight(lastRippleHeight);
// try block because of possible null of Background, fills ...
try {
rippleClip.setArcHeight(this.getBackground().getFills().get(0).getRadii().getTopLeftHorizontalRadius());
rippleClip.setArcWidth(this.getBackground().getFills().get(0).getRadii().getTopLeftHorizontalRadius());
circleRipple.setClip(rippleClip);
} catch (Exception e) {
}
// Getting 45% of longest button's length, because we want edge of ripple effect always visible
double circleRippleRadius = Math.max(getHeight(), getWidth()) * 0.45;
final KeyValue keyValue = new KeyValue(circleRipple.radiusProperty(), circleRippleRadius, Interpolator.EASE_OUT);
final KeyFrame keyFrame = new KeyFrame(rippleDuration, keyValue);
scaleRippleTimeline.getKeyFrames().clear();
scaleRippleTimeline.getKeyFrames().add(keyFrame);
}
parallelTransition.playFromStart();
});
}
public void setRippleColor(Color color) {
circleRipple.setFill(color);
}
}

I don’t describe the code because of comments that you can find in more important fragments.
This is only test case class, not production ready.

You can also watch how it look in practise:

I inserted sample application from video on github -> https://github.com/nonameplum/md-button-fx-sample

Have fun.

My fork of jidefx-oss

I spend some time on looking deeper into validation in JavaFx. I found some useful API in ControlsFX, Jide-oss and FXValidation.

I found that on my point of view the best API is in Jide-oss. But Jide-oss library was last updated in February 2014 and was not compatible with jdk1.8.0_20.

Fortunately only a few corrections had to do to fix the problem.
Next I started to look into missing features that I think can be useful.

Here you can find my fork: https://github.com/nonameplum/jidefx-oss

First of all I wanted to add posibility to easy customization for decoration of validation. Now you can do this by passing your custom lambda that will create decorator or use one of prepared by me decoration creators defined in ValidationDecorators class (for now graphic and FontAwesome). Also you can easly customize look and feel of validated controls and validators by css.

The next thing that I missed was the ability to easily check status of few validators. It can be useful for define disabled property e.g. button. For that purpose you can use ValidationGroup class.

Use case example:

1
2
ValidationGroup validationGroup = new ValidationGroup(emailField, passwordField);
signUpButton.disableProperty().bind(validationGroup.invalidProperty());

I added also some utils methods e.g. to:

  • get decorators from control
  • show tooltip manualy on validation decorator
  • install required decrator

A brief description of the changes:

  • validation support for datepicker
  • better ability to css customization
  • more freedom of decoration creation (support for lambda expression)
  • ability to create decoration using FontAwesome font.
  • support for observable status of validation group
  • some helper methods

You can check example project witch I modified to show new API features (code changes mostly in ValidationDemo class)

My changes was also merged by Jide-oss into orignal repo - pull request: https://github.com/jidesoft/jidefx-oss/pull/21

JavaFX tooltip event propagation quick tip

Using tooltips in different environments than JavaFx, like VLC Delphi or C# WPF, I’m used to behaviour that when user click outside of tooltip or tooltip owner then tooltip is hidden and also event is transmitted to original event target.
First part (hiding) working as expected but event propagation not. But JavaFX developers not forget about this and created opportunity to push event further.
All you have to do is to change property using method setConsumeAutoHidingEvents to false:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Specifies whether the event, which caused the Popup to hide, should be
* consumed. Having the event consumed prevents it from triggering some
* additional UI response in the Popup's owner window.
* @defaultValue true
* @since JavaFX 2.2
*/
private BooleanProperty consumeAutoHidingEvents =
new SimpleBooleanProperty(this, "consumeAutoHidingEvents",
true);
public final void setConsumeAutoHidingEvents(boolean value) {
consumeAutoHidingEvents.set(value);
}

As you can see consumeAutoHidingEvents property default value is true, so event is not propagated. This code is from PopupWindow class and Tooltip class inherits this property.
In so far as it may be appropriate for popupWindow, it is not good solution for tooltip, where do you not expect to click two times, first one only to hide tooltip and second time to make action on some another control.

JavaFX tooltip styleable parent bug

Few days ago I found problem with tooltip styling.
I wanted to show tooltip on demand (when TextField get focus) and I also wanted to style this tooltip using custom css like this:

1
2
3
4
5
6
7
8
9
.label-with-tooltip .tooltip {
-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.8), 10, 0, 0, 0);
-fx-font-weight: bold;
-fx-padding: 5;
-fx-border-width:1;
-fx-background-color: #FBEFEF;
-fx-text-fill: #cc0033;
-fx-border-color:#cc0033;
}

I got result that when I hovered mouse on TextField then my style was applied correctly but when I show tooltip on focus event using tooltip.show(...) then tooltip didn’t get my style.

This is due to getStyleableParent method of Tooltip class with is responsible for style “propagation”. In Tooltip class getStyleableParent looks like this:

1
2
3
public Styleable getStyleableParent() {
return BEHAVIOR.hoveredNode;
}

BEHAVIOR is static class responsible for properly showing and hiding tooltip when mouse is over node. hoveredNode property is set in mouse move event handler on the node that have installed tooltip supported by BEHAVIOR class. Therefore, when you use show method to manually show tooltip then BEHAVIOR.hoveredNode property is not set and also you don’t have chance to change it, because BEHAVIOR is private class.

I found solution by overriding getStyleableParent and returning ownerNode which is set when you show tooltip using method of Tooltip class:

1
2
3
public void show(Node ownerNode, double anchorX, double anchorY) {
...
}

Here is example application where you see how it works:

JavaFX tooltip fix example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
public class Main extends Application {
public static class TooltipFix extends Tooltip {
public TooltipFix(String text) {
super(text);
}
@Override
public Styleable getStyleableParent() {
Styleable styleableParent = super.getStyleableParent();
// if default behavior is not returning styleable parent
// then return owner node
if (styleableParent != null) {
return styleableParent;
} else {
return getOwnerNode();
}
}
}
@Override
public void start(Stage primaryStage) throws Exception{
GridPane gridPane = new GridPane();
Parent root = gridPane;
primaryStage.setTitle("Hello World");
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
primaryStage.getScene().getStylesheets().clear();
primaryStage.getScene().getStylesheets().add(getClass().getResource("style.css").toExternalForm());
Label lblTooltipBug = new Label("Tooltip bugged");
lblTooltipBug.getStyleClass().add("label-with-tooltip");
// original tooltip
Tooltip tooltipBug = new Tooltip("I'm broken");
lblTooltipBug.setTooltip(tooltipBug);
gridPane.add(lblTooltipBug, 0, 0);
Label lblTooltipFix = new Label("Tooltip fixed");
lblTooltipFix.getStyleClass().add("label-with-tooltip");
gridPane.add(lblTooltipFix, 0, 1);
// fixed tooltip
TooltipFix tooltipFix = new TooltipFix("I'm repaired");
lblTooltipFix.setTooltip(tooltipFix);
Button btnShowTooltip = new Button("Show tooltips");
btnShowTooltip.setOnAction(event -> {
showTooltip(lblTooltipBug);
showTooltip(lblTooltipFix);
});
gridPane.add(btnShowTooltip, 0, 2);
gridPane.setVgap(20);
gridPane.setAlignment(Pos.CENTER);
}
public static void showTooltip(Label label) {
if (label != null && label.getTooltip() != null) {
// offset
Point2D point = label.localToScene(100, 0);
label.getTooltip().setAutoHide(true);
label.getTooltip().show(label, point.getX()
+ label.getScene().getX() + label.getScene().getWindow().getX(), point.getY()
+ label.getScene().getY() + label.getScene().getWindow().getY());
}
}
public static void main(String[] args) {
launch(args);
}
}