JavaFX: MiniIcon(Animation)Button refactored

JavaFX: MiniIcon(Animation)Button refactored

Jonathan Giles wrote in his Blog that

This kind of code should be polished and added to the JFXtras project!

So I thought this would be a good start to learn more about JavaFX internals  and how to do it the right way. There is a talk at parleys where Jasper Potts and Jonathan Giles talk about creating a custom UI control. I wish I had seen this earlier.

1. Learn about the JavaFX seperation of concerns pattern

Like the most UIs, JavaFX has also a seperation of concerns pattern.

2. Refactor the MiniIconButton

So I have to break my 1-class component into 3 classes follow the API.

2.1 Control

The easiest one ist the Control. This is the model of the component and the part a developer is using in his code, like Button, TextField, Label, … I have to clean my class from all the code that has nothing to do with property handling. For each property a developer should be able to work with later on, I have to add a member variable XY based on

javafx.beans.property.Property

and add a getXY()-method a setXY()-method and a XY xYProperty()-method.

This results in the following code:

public class MiniIconButton extends Button {

    /**
     * Type of animation
     */
    public enum AnimationType { NONE, JUMP, BLINK };

    private ObjectProperty miniIcon;
    private ObjectProperty animationType = new SimpleObjectProperty(this, "animationType", AnimationType.NONE);
    private DoubleProperty animationDuration = new SimpleDoubleProperty(this, "animationDuration", 500);

   ...

    /**
     * The property for the animation type.
     * @return property of animation type
     * @see AnimationType
     */
    public final ObjectProperty animationTypeProperty() {
        return this.animationType;
    }

    /**
     * Returns the animation type. The default is AnimationType.NONE
     * @return the animation type
     * @see AnimationType
     */
    public AnimationType getAnimationType() {
        return this.animationType.getValue();
    }

    /**
     * Sets the animation type.
     * @param value
     * @see AnimationType
     */
    public void setAnimationType(final AnimationType value) {
        this.animationType.setValue(value);
    }

   ...

The MiniIconButton extends Button as it shares all the functionality with Button and just adds some new stuff.

2.2 CSS

But where is the part, where the skin is bound to the Control? That happens in the CSS! The Control has to override the

@Override  
protected String getUserAgentStylesheet() {  
  return getClass().getResource("/jfxtras/labs/internal/scene/control/" + 
         this.getClass().getSimpleName() + ".css").toString();
}

This stylesheet is responsible for the look and the connection to a skinning class. It is very important that the control sets a unique styleClass.

getStyleClass().add("mini-icon-button");

This styleClass is used to declare the connection.

.mini-icon-button {  
  -fx-skin: "jfxtras.labs.internal.scene.control.skin.MiniIconButtonSkin"; 
}

2.3 Skin

In the MiniIconButtonSkin class happens all the UI stuff. Create internal components, draw all the things. The MiniIconButton extends Button, so the MiniIconButtonSkin extends ButtonSkin. Most of the work is done in this class and I benefit from extending it.

All the Skin classes extend StackPane. In the last version of the MiniIconAnimationButton, I have extended the StackPane in my button. That makes the refactoring easy. The code for adding the mini-icon to the stackpane is already there. It was just in the wrong class.

public MiniIconButtonSkin(final MiniIconButton miniIconButton) {  
  super(miniIconButton);
  setMiniIcon(miniIconButton.getMiniIcon());
  positionMiniIcon(miniIconButton);
  calculateAndSetNewMiniIconSize(miniIconButton);
  defaultConfigJumpingAnimation();
  defaultConfigBlinkingAnimation();
  configureJumping(miniIconButton);
  configureBlinking(miniIconButton);
  startAnimation(miniIconButton);
  addImageViewSizeBindings();
  addChangeListeners();
}

And these are the implementations of the methods.

/**  
 * sets the given mini icon
 * @param miniIcon the mini icon
 */
private void setMiniIcon(final ImageView miniIcon) {  
    getChildren().add(miniIcon);
}

/**
 * positions the mini icon on the given {@link Pos}
 * @param miniIconButton
 */
private void positionMiniIcon(final MiniIconButton miniIconButton) {  
    final ImageView miniIcon = miniIconButton.getMiniIcon();
    StackPane.setAlignment(miniIcon, miniIconButton.getMiniIconPosition());
    StackPane.setMargin(miniIcon, new Insets(MARGIN, MARGIN, MARGIN, MARGIN));
}

Another change to the first version are the listeners added to properties. Without that, the button can never change after creation (except for the inheritat properties).

/**  
     * adds the change listeners to the {@link MiniIconButton} properties
     */
    private void addChangeListeners() {
        final MiniIconButton miniIconButton = (MiniIconButton)getSkinnable();
        miniIconButton.animationDurationProperty().addListener(new ChangeListener() {
            @Override
            public void changed(final ObservableValue observableValue,
                                final Number oldDuraction,
                                final Number newDuration) {
                stopAnimation(miniIconButton);
                configureJumping(miniIconButton);
                configureBlinking(miniIconButton);
                startAnimation(miniIconButton);
                requestLayout();
            }
        });

        miniIconButton.animationTypeProperty().addListener(new ChangeListener() {
            @Override
            public void changed(final ObservableValue observableValue,
                                final MiniIconButton.AnimationType oldAnimationType,
                                final MiniIconButton.AnimationType newAnimationType) {
                startAnimation(miniIconButton);
                requestLayout();
            }
        });

        miniIconButton.miniIconPositionProperty().addListener(new ChangeListener() {
            @Override
            public void changed(final ObservableValue observableValue,
                                final Pos oldPosition,
                                final Pos newPosition) {
                StackPane.setAlignment(miniIconButton.getMiniIcon(), newPosition);
                requestLayout();
            }
        });

        miniIconButton.miniIconProperty().addListener(new ChangeListener() {
            @Override
            public void changed(final ObservableValue observableValue,
                                final ImageView oldMiniIcon,
                                final ImageView newMiniIcon) {
                stopAnimation(miniIconButton);
                changeMiniIcon(oldMiniIcon, newMiniIcon);
                positionMiniIcon(miniIconButton);
                configureJumping(miniIconButton);
                configureBlinking(miniIconButton);
                calculateAndSetNewMiniIconSize(miniIconButton);
                startAnimation(miniIconButton);
                requestLayout();
            }
        });

        miniIconButton.miniIconRatioProperty().addListener(new ChangeListener() {
            @Override
            public void changed(final ObservableValue observableValue,
                                final Number oldNumber,
                                final Number newNumber) {
                stopAnimation(miniIconButton);
                calculateAndSetNewMiniIconSize(miniIconButton);
                startAnimation(miniIconButton);
                requestLayout();
            }
        });
    }

These possible changes are the reason why I decided to have the two animations as members in the skinning class. I need the possibility to start, stop and change them as a reaction to a property change. They could have been created lazy, but this is for the next version.

/**  
 * transition for the mini-icon jump
 */
private final TranslateTransition jumpTransition = new TranslateTransition();

/**
 * the timline for blinking
 */
private final Timeline blinkTimeline = new Timeline();

/**
 * configure the blinking has some steps to do
 * 
    *
  1. remove the old {@link KeyFrame} from the timeline
  2. *
  3. create a new {@link KeyValue}
  4. *
  5. create a new {@link KeyFrame}
  6. *
  7. add new {@link KeyFrame} to the timeline
  8. *
* @param miniIconButton the mini icon button */ private void configureBlinking(final MiniIconButton miniIconButton) { blinkTimeline.getKeyFrames().remove(kf); final KeyValue kv = new KeyValue(miniIconButton.getMiniIcon().opacityProperty(), MINIMUM_OPACITY_FOR_BLINKING); kf = new KeyFrame(Duration.millis(miniIconButton.getAnimationDuration()), kv); blinkTimeline.getKeyFrames().add(kf); } /** * configure the jumping animation * @param miniIconButton the mini icon button */ private void configureJumping(final MiniIconButton miniIconButton) { final ImageView miniIcon = miniIconButton.getMiniIcon(); jumpTransition.setNode(miniIcon); jumpTransition.setDuration(Duration.millis(miniIconButton.getAnimationDuration())); } /** * Add the animation based on the animation type. * @param miniIconButton the mini icon button */ private void startAnimation(final MiniIconButton miniIconButton) { switch (miniIconButton.getAnimationType()) { case BLINK: jumpTransition.stop(); blinkTimeline.play(); break; case JUMP: blinkTimeline.stop(); jumpTransition.play(); break; case NONE: default: blinkTimeline.stop(); jumpTransition.stop(); break; } } /** * Stops the animation and resets the start values * @param miniIconButton the mini icon button */ private void stopAnimation(final MiniIconButton miniIconButton) { final ImageView miniIcon = miniIconButton.getMiniIcon(); jumpTransition.stop(); blinkTimeline.stop(); miniIcon.setOpacity(1.0); miniIcon.setTranslateY(0.0); } /** * The jump animation changes the position of the mini-icon. */ private void defaultConfigJumpingAnimation() { final double start = 0.0; final double end = -JUMP_DISTANCE; jumpTransition.setFromY(start); jumpTransition.setToY(end); jumpTransition.setCycleCount(-1); jumpTransition.setAutoReverse(true); jumpTransition.setInterpolator(Interpolator.EASE_BOTH); } /** * Blinking animation changes the opacity of the mini-icon. */ private void defaultConfigBlinkingAnimation() { blinkTimeline.setCycleCount(Timeline.INDEFINITE); blinkTimeline.setAutoReverse(true); }

2.4 Behavior

The behavior classes are responsible for key and mouse handling. The MiniIconButton has no additional functionality and so no extra behavior class is needed.

2.5 Example

In the next Ensemble of the JFXtras Project, there is an example page for the MiniIconButton.

// default values  
// ----------------------------
// ratio: 0.25
// Pos.TOP_RIGHT
// animation duration: 500 ms
final MiniIconButton b1 = new MiniIconButton(new ImageView(bigIcon), new ImageView(smallIcon));  
b1.setAnimationType(MiniIconButton.AnimationType.BLINK);

// default values
// ----------------------------
// ratio: 0.25
// Pos.TOP_RIGHT
// animation duration: 500 ms
final MiniIconButton b2 = new MiniIconButton("2. Button", new ImageView(bigIcon), new ImageView(smallIcon));  
b2.setAnimationType(MiniIconButton.AnimationType.JUMP);

// default values
// ----------------------------
// AnimationType.NONE
// Pos.TOP_RIGHT
final MiniIconButton b3 = new MiniIconButton("3. Button", new ImageView(bigIcon), new ImageView(smallIcon));  
b3.setMiniIconRatio(0.1);

// default values
// ----------------------------
// AnimationType.NONE
// Pos.TOP_RIGHT
final MiniIconButton b4 = new MiniIconButton("4. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b4.setMiniIconRatio(1.0);

// default values
// ----------------------------
// AnimationType.NONE
// Pos.TOP_RIGHT
final MiniIconButton b5 = new MiniIconButton("5. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b5.setMiniIconRatio(0.25); // default

// default values
// ----------------------------
// AnimationType.NONE
// Pos.TOP_RIGHT
final MiniIconButton b6 = new MiniIconButton("6. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b6.setMiniIconRatio(0.1);

// default values
// ----------------------------
// AnimationType.NONE
// ratio: 0.25
final MiniIconButton b7 = new MiniIconButton("7. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b7.setMiniIconPosition(Pos.TOP_LEFT);

// default values
// ----------------------------
// AnimationType.NONE
// ratio: 0.25
final MiniIconButton b8 = new MiniIconButton("8. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b8.setMiniIconPosition(Pos.TOP_CENTER);

// default values
// ----------------------------
// AnimationType.NONE
// ratio: 0.25
final MiniIconButton b9 = new MiniIconButton("9. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b9.setMiniIconPosition(Pos.TOP_RIGHT);

// default values
// ----------------------------
// AnimationType.NONE
// ratio: 0.25
final MiniIconButton b10 = new MiniIconButton("10. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b10.setMiniIconPosition(Pos.CENTER_LEFT);

// default values
// ----------------------------
// AnimationType.NONE
// ratio: 0.25
final MiniIconButton b11 = new MiniIconButton("11. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b11.setMiniIconPosition(Pos.CENTER);

// default values
// ----------------------------
// AnimationType.NONE
// ratio: 0.25
final MiniIconButton b12 = new MiniIconButton("12. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b12.setMiniIconPosition(Pos.CENTER_RIGHT);

// default values
// ----------------------------
// AnimationType.NONE
// ratio: 0.25
final MiniIconButton b13 = new MiniIconButton("13. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b13.setMiniIconPosition(Pos.BOTTOM_LEFT);

// default values
// ----------------------------
// AnimationType.NONE
// ratio: 0.25
final MiniIconButton b14 = new MiniIconButton("14. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b14.setMiniIconPosition(Pos.BOTTOM_CENTER);

// default values
// ----------------------------
// AnimationType.NONE
// ratio: 0.25
final MiniIconButton b15 = new MiniIconButton("14. Button", new ImageView(bigIcon), new ImageView(mediumIcon));  
b15.setMiniIconPosition(Pos.BOTTOM_RIGHT);

// default values
// ----------------------------
// Pos.TOP_RIGHT
// ratio: 0.25
final MiniIconButton b16 = new MiniIconButton(new ImageView(bigIcon), new ImageView(mediumIcon));  
b16.setAnimationDuration(800);  
b16.setAnimationType(MiniIconButton.AnimationType.BLINK);

// default values
// ----------------------------
// Pos.TOP_RIGHT
// ratio: 0.25
final MiniIconButton b17 = new MiniIconButton(new ImageView(bigIcon), new ImageView(mediumIcon));  
b17.setAnimationDuration(200);  
b17.setAnimationType(MiniIconButton.AnimationType.JUMP);

final FlowPane rootPane = FlowPaneBuilder  
        .create()
        .children(b1, b2, b3, b4, b5, b6, b7, b8, b9, b10, b11, b12, b13, b14, b15, b16, b17, button)
        .hgap(20).vgap(20)
        .build();

3. Demo

4. Sources

The Sources are part of the JFXtras Project.

About Andreas Billmann

Neckarwestheim, Germany

Comments