TweetWallFX – Part 2

Hi,

In my TweetWallFX – Part 1 article I presented to you how you could simply create a start screen with some effects available in JavaFX. In this article we’re going to see how to create the wall of tweets itself. We will not use any Twitter API but generate random tweets in order to focus on JavaFX.

“Business” part

Before to deal with the UI we are going to create some business classes on which the application will be built on. First of all we create a class representing a tweet in order not to be dependent by any Twitter API you may use.

public class Tweet implements Serializable {
  private Calendar postDate;
  private String userName, firstName, lastName;
  private String content;
  // Be careful to use the javafx.scene.image.Image
  private Image avatar;

  public Tweet() {}

  public Tweet(String userName, String firstName, 
               String lastName, String content, 
               Image avatar, Calendar postDate) {

    this.userName = userName;
    this.firstName = firstName;
    this.lastName = lastName;
    this.content = content;
    this.avatar = avatar;
    this.postDate = postDate;
  }

  // Create getters & setters
}

Next we create a simple Twitter DAO in order to provide some services, like getting some tweets. As I usually like to do, I create an interface representing my DAO, and then some implementation. This allows me to change the implementation used at any time without having to change and check my whole code. The interface will be:

public interface TweetDAO extends Serializable {
  List<Tweet> getTweetForHashtag(String hashTag, int maxResult);
}

And my foo implementation is:

public class FooTweetDAO implements TweetDAO {

  @Override
  public List<Tweet> getTweetForHashtag(String hashTag, int maxResult) {
    List<Tweet> tweets = new ArrayList<Tweet>(0);
    Tweet tweet;
    Random random = new Random();

    for(short nb = 0; nb < maxResult; nb++) {
      tweet = new Tweet("jdoe", "John", "doe", null, null, Calendar.getInstance());
      tweet.setContent("My random tweet with #" + hashTag + "-" + random.nextInt(1000));
      tweet.setAvatar(new Image(getClass().getResourceAsStream("/com/twasyl/tweetwallfx/resources/images/tweetEgg.png")));
      tweets.add(tweet);
    }
    return tweets;
  }
}

UI time

Now we’re able to get some “tweets” we will create the UI and some logic code in order to update the wall. Remember I said that I think that with JavaFX it’s the first time you can really implement the MVC design pattern in Java desktop apps. When you come from the Swing world like me, maybe you put everything of your UI in a class, instantiate manually everything and so on. But if you come from the JSF world like me too, you easily separate your view from the backing code. The first times you create some JFX apps, you may be nostalgic and want to do everything by hand, like in the old times (yeah I know this), or maybe you’re just used to do it. My advice for you is to ask yourself this question: “Do I really NEED to do everything all by myself for the UI?”. It will be hard the first times, but you’re getting better everyday! After this little introduction, let’s get back to work! I want my wall to be filled by “bubbles”, and each bubble will contain the information about a tweet. I have two main possibilities for creating this bubble:

  • Old fashioned way: create everything in the Java code by creating a wonderful class having a Label, a picture, and so on.
  • The way you’re always dreamt about: create an external file that will contain that UI element

Of course we will use the second way and create our bubble using FXML. So here we go:

<HBox xmlns:fx="http://javafx.com/fxml" fx:id="tweetBox" 
    prefHeight="50" prefWidth="500" spacing="10"
    fx:controller="com.twasyl.tweetwallfx.controllers.TweetBubbleController"
    style="-fx-background-color: linear-gradient(white 0%, gray 100%); -fx-background-radius: 10,10,10,10; -fx-padding: 5,5,5,5">
  <children>
    <ImageView id="avatarImageView" fx:id="avatarImageView" />
    <VBox spacing="10">
      <HBox spacing="10">
        <Label id="nameLabel" fx:id="nameLabel" />
        <Label id="userNameLabel" fx:id="userNameLabel" />
      </HBox>
      <Label id="tweetLabel" fx:id="tweetLabel" />
    </VBox>
  </children>
</HBox>

This code gives you a Node like this when it’s filled:

The FXML is pretty simple and everything should be known by you if you read the previous article. Nothing more, nothing less. Then we will create an FXML that will be our wall and contain some bubbles.

<Tab xmlns:fx="http://javafx.com/fxml" id="wallTab" fx:id="wallTab" 
    fx:controller="com.twasyl.tweetwallfx.controllers.WallController"
    closable="true">

  <content>
    <AnchorPane id="wallPane" fx:id="wallPane" style="-fx-background-color: linear-gradient(#5C5C5C 0%, #2E2E2E 100%);" minWidth="1024" minHeight="768">
      <children>
      </children>
    </AnchorPane>
  </content>
</Tab>

As you can see, the root element isn’t a “regular” Pane but a Tab. Indeed I decided to create a tab-based app. So here, instead of creating a Pane that will be added in a Tab in my backing code, I directly create a Tab. This Tab only contains an AnchorPane which is going to contain our bubbles and that has a linear gradient with some gray colors. You should also be familiar with the fx:controller attribute that defines a backing class as controller for the FXML.

Controller time

For now we’re already done with the UI part! (yeah that was quick) We have to create our controllers and you will see that is pretty straight forward to do. The most simple one is the TweetBubbleController so let’s take a look at it.

public class TweetBubbleController implements Initializable {
  // Some default sizes
  public static final int DEFAULT_WIDTH = 500;
  public static final int DEFAULT_HEIGHT = 80;
  public static final double DEFAULT_AVATAR_PICTURE_SIZE = 50;
  private Tweet tweet;

  // Binding with the FXML
  @FXML private ImageView avatarImageView;
  @FXML private Label tweetLabel;
  @FXML private Label nameLabel;
  @FXML private Label userNameLabel;

  public TweetBubbleController() {}

  public boolean isEmpty() {
    return getContent() == null;
  }

  public void setContent(Object content) {
    if (content instanceof Tweet) {
      this.tweet = (Tweet) content;

      // Getting the greater size: height or width
      // We don't want the pic to be greater than the bubble height
      double maxDim = Math.max(tweet.getAvatar().getWidth(), tweet.getAvatar().getHeight());

      // Setting the image of the ImageView
      this.avatarImageView.setImage(this.tweet.getAvatar());
      this.avatarImageView.setPreserveRatio(true);
      this.avatarImageView.setSmooth(true);
      this.avatarImageView.setCache(true);

      if (maxDim == tweet.getAvatar().getHeight()) {
        this.avatarImageView.setFitHeight(DEFAULT_AVATAR_PICTURE_SIZE);
      } else {
        this.avatarImageView.setFitWidth(DEFAULT_AVATAR_PICTURE_SIZE);
      }

      // Updating other labels
      this.nameLabel.setText(String.format("%1$s %2$s", this.tweet.getFirstName(), this.tweet.getLastName().toUpperCase()));
      this.userNameLabel.setText(String.format("@%1$s", this.tweet.getUserName()));
      
      this.tweetLabel.setText(this.tweet.getContent());
      this.tweetLabel.setWrapText(true);
      this.tweetLabel.setMaxWidth(DEFAULT_WIDTH);
      this.tweetLabel.setMaxHeight(DEFAULT_HEIGHT);
    }
  }

  public Object getContent() {
    return this.tweet;
  }

  @Override
  public void initialize(URL arg0, ResourceBundle arg1) {}
}

Now the most tricky one but not really difficult: the WallController that updates the wall:

public class WallController implements Initializable {

  private double HORIZONTAL_SPACE_BETWEEN_TWEETS = 5;
  private double VERTICAL_SPACE_BETWEEN_TWEETS = 10;
  private double WIDTH;
  private double HEIGHT;
  @FXML private AnchorPane wallPane;
    private Box<TweetBubbleController>[] boxes;
    private int numberOfRows, numberOfColumns;

  /** This method check if there is an "available" place on the wall */
  private boolean isFull() {
    // Do your implementation
  }

  /** Return the X and Y pos for the next "empty place" */
  private Coupl<Double, Double> nextEmptyBox() {
    // Do your implementation
  }

  /** Throwing the exception here is just to get the code simplier */
  public void addTweet(Tweet tweet) throws IOException {
    if (tweet != null) {
      if (isFull()) {
        // If the wall is full, do some animation of your bubbles
      }
      Couple<Double, Double> emptyPlace = nextEmptyBox();
      // Now load the TweetBubble FXML file
      FXMLLoader loader = new FXMLLoader();
      Parent root = (Parent) loader.load(getClass().getResource("/com/twasyl/tweetwallfx/fxmls/TweetBubble.fxml").openStream());
      
      // Now the file is loaded, place your bubble on the screen
      root.setLayoutX(emptyPlace.getX());
      root.setLayoutY(emptyPlace.getY());
      
      // Get the controller of the bubble and define its content
      TweetBubbleController controller = (TweetBubbleController) loader.getController();
      controller.setContent(tweet);

      // Finally, add the bubble to the screen
      this.wallPane.getChildren().add(root);
    }
  }

  @Override
  public void initialize(URL arg0, ResourceBundle arg1) {
    // Just calculate how much columns and rows you can display
    numberOfColumns = (int) (WIDTH / (TweetBubbleController.DEFAULT_WIDTH + HORIZONTAL_SPACE_BETWEEN_TWEETS));
    numberOfRows = (int) (HEIGHT / (TweetBubbleController.DEFAULT_HEIGHT + VERTICAL_SPACE_BETWEEN_TWEETS));
  }
}

Well this is the main part of the controller. Moving your tweets is algorithmic so go ahead for your own implementation. I’ll do a specific article to present you some animation stuff you’ll be able to use here. The last controller to update is the start screen one. Remember that in the previous article, we defined the onAction=”#startTweetWall” attribute on the search button. Now we’re going to create the method named startTweetWall in our controller which is:

@FXML private void startTweetWall(ActionEvent event) {
  Tab wallTab = null, statsTab = null;
  WallController wallController = null;
  
  // Load the FXML
  try {
    FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/com/twasyl/tweetwallfx/fxmls/wall-tab.fxml"));
    wallTab = (Tab) fxmlLoader.load();
    wallController = (WallController) fxmlLoader.getController();
  } catch (IOException ex) {
    Logger.getLogger(TweetWallFXController.class.getName()).log(Level.SEVERE, null, ex);
  }
  if(wallTab != null) {
    // Set the text of the tab by retrieving the text
    // entered in the search text field, binded in
    // our controller
    wallTab.setText(String.format("#%1$s", this.hashtagTextField.getText()));
    
    // Add the tab to the TabPane of our application
    // also binded in our controller        
    this.tabPane.getTabs().add(wallTab);
  }
}

Well this is it. Now what you have to do for example in order to generate tweets is to create a basic TimerTask taking your instance of WallController into parameter, and then, with your DAO, generate tweets, and add them using the addTweet(Tweet t); method of your controller.
Well that’s it for this article. Stay tuned.

3 Responses to TweetWallFX – Part 2

  1. Pingback: Java desktop links of the week, May 21 | Jonathan Giles

  2. Pingback: JavaFX links of the week, May 21 // JavaFX News, Demos and Insight // FX Experience

  3. Pingback: TweetWallFX – Part 5 « Thierry WASYL : Java blog

Leave a comment