JavaFX: MiniIconAnimationButton

JavaFX: MiniIconAnimationButton

In the last JavaFX blog post I showed how a button with an glowing effect can be used to catch the users attention. In this blog post I develop a button based on StackPane to have a normal button in the back and an optional animating image icon in the front.

1. UI-Mockup

The code

2.1 Constructor

We need for the button a text, an icon and a mini-icon. As an optional parameter we can set the animation type.

/**
   * Extended button without an animation
   * @param text button text
   * @param graphic button icon
   * @param notifyImage mini icon
   */
  public MiniIconAnimationButton(String text, 
                                 Node graphic, 
                                 ImageView notifyImage) {
    this(text, graphic, notifyImage, AnimationType.NONE);
  }
 
  /**
   * Extended button with an animation
   * @param text button text
   * @param graphic button icon
   * @param notifyImage mini icon
   * @param type animation type for mini icon
   */
  public MiniIconAnimationButton(String text, 
                                 Node graphic, 
                                 ImageView notifyImage, 
                                 AnimationType type) { ... }

2.2 AnimationType

There are three animation types for this button.

  • NONE means that there is no animation. The mini-icon is just shown in the upper right corner.
  • JUMP means that the mini-icon moves up and down frequently.
  • BLINK means that the mini-icon changes the opacity from visible to invisible and back.
/**
   * Type of animation
   */
  public enum AnimationType { NONE, JUMP, BLINK };

2.3 add the controls to the stack

The button is an extension of the StackPane to position the button in the back and the mini-icon in the front. The mini-icon is placed on the upper right and 4 pixels away from the edges.

/**
  * add the button to the background and the mini-icon to the front
  */
 private void stackControls() {
   StackPane.setAlignment(imageView, Pos.TOP_RIGHT);
   StackPane.setMargin(imageView, new Insets(4, 4, 4, 4)) ;
   getChildren().addAll(button, imageView);
 }

2.4 bind the size of the mini-icon to the button size

The mini-icon should be smaller than the button self. I decided to make the mini-icon a quater of the size of the button. To resize the icon if the button is resized I add an ChangeListener to the binding.

/**
 * bind the size of the mini-icon to the button size
 */
private void addImageViewSizeBindings() {
  final ReadOnlyDoubleProperty widthProperty = button.widthProperty();
  final ReadOnlyDoubleProperty heightProperty = button.heightProperty();
  final DoubleBinding widthBinding = widthProperty.divide(4.0);
  final DoubleBinding heightBinding = heightProperty.divide(4.0);
  imageView.setFitWidth(widthBinding.doubleValue());
  imageView.setFitHeight(heightBinding.doubleValue());
  widthBinding.addListener(new ChangeListener() {
    @Override
    public void changed(ObservableValue o, Object oldVal, Object newVal) {
      imageView.setFitWidth(widthBinding.doubleValue());
    }
  });
  heightBinding.addListener(new ChangeListener() {
    @Override
    public void changed(ObservableValue o, Object oldVal, Object newVal) {
      imageView.setFitHeight(heightBinding.doubleValue());
    }
  });
}

2.5 add the animation

Based on the type the animation method is choosen.

/**
 * add the animation
 */
private void addAnimation() {
  switch (type) {
    case BLINK:
      addBlinkingAnimation();
      break;
    case JUMP:
      addJumpingAnimation();
      break;
    case NONE:
      // none is the default case
    default:
      // noting to animate
      break;
  }
}

2.5.1 JUMP

The jump is done with a transition. There is a start and an end value and the tranistion calculates the positions for each step.

/**
 * the jump animation changes the position of the mini-icon
 */
private void addJumpingAnimation() {
  final TranslateTransition translateTransition =  new TranslateTransition(Duration.millis(200), imageView);
  final double start = 0.0;
  final double end = start - 4.0;
  translateTransition.setFromY(start);
  translateTransition.setToY(end);
  translateTransition.setCycleCount(-1);
  translateTransition.setAutoReverse(true);
  translateTransition.setInterpolator(Interpolator.EASE_BOTH);
  translateTransition.play();
}

The blinking is done by changing the opacity of the mini-icon. This is done with a Timeline and a KeyFrame. The timeline changes the given property from the value at the beginnign to a new value and back.

/**
   * blinking animation changes the opacity of the mini-icon
   */
  private void addBlinkingAnimation() {
    final Timeline timeline = new Timeline();
    timeline.setCycleCount(Timeline.INDEFINITE);
    timeline.setAutoReverse(true);
    final KeyValue kv = new KeyValue(imageView.opacityProperty(), 0.0);
    final KeyFrame kf = new KeyFrame(Duration.millis(700), kv);
    timeline.getKeyFrames().add(kf);
    timeline.play();
  }

2.6 button delegation methods

The rest of the button code is simple delegation code which calls the button methods on the original button.

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
  // delegation methods for button, this class should behave like a normal button                                     //
  //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
 
  public void setDefaultButton(boolean value) {
    button.setDefaultButton(value);
  }
 
  public void setFont(Font value) {
    button.setFont(value);
  }
 
  public ObjectProperty contentDisplayProperty() {
    return button.contentDisplayProperty();
  }
 
  // ...

The main class

package de.billmann.javafx.demo;
 
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.SceneBuilder;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.TilePane;
import javafx.scene.layout.TilePaneBuilder;
import javafx.stage.Stage;
 
/**
 * @author abi
 */
public class Main extends Application {
 
  private ImageView icon1 = new ImageView(new Image(getClass().getResourceAsStream("/images/kcmpartitions.png")));
  private ImageView icon2 = new ImageView(new Image(getClass().getResourceAsStream("/images/kcmpci.png")));
  private ImageView icon3 = new ImageView(new Image(getClass().getResourceAsStream("/images/personal.png")));
 
  private ImageView miniIcon1 = new ImageView(new Image(getClass().getResourceAsStream("/images/bell.png")));
  private ImageView miniIcon2 = new ImageView(new Image(getClass().getResourceAsStream("/images/important.png")));
  private ImageView miniIcon3 = new ImageView(new Image(getClass().getResourceAsStream("/images/kcmdrkonqi.png")));
 
  public static void main(final String[] args) {
    Application.launch(args);
  }
 
  public void start(final Stage stage) {
    final MiniIconAnimationButton b1 = new MiniIconAnimationButton("1. Button", icon1, miniIcon1, MiniIconAnimationButton.AnimationType.JUMP);
    final MiniIconAnimationButton b2 = new MiniIconAnimationButton("2. Button", icon2, miniIcon2, MiniIconAnimationButton.AnimationType.BLINK);
    final MiniIconAnimationButton b3 = new MiniIconAnimationButton("3. Button", icon3, miniIcon3, MiniIconAnimationButton.AnimationType.NONE);
 
    final TilePane rootPane = TilePaneBuilder
        .create()
        .children(b1, b2, b3)
        .padding(new Insets(4, 4, 4, 4))
        .hgap(10)
        .build();
 
    final Scene scene = SceneBuilder
        .create()
        .width(474)
        .height(160)
        .root(rootPane)
        .build();
 
    stage.setScene(scene);
 
    stage.show();
  }
}

4. Video of the effect

5. Source code

Comments