1

I'm trying to make a WhatsApp-Like Conversation-View in JavaFX.

In order to make the sent messages appear on the right and the received messages appear on the left then I cannot use TextArea. How can I do it? I tried GridPane without TextArea but it didn't make things easier.

Moreover, is it a good practice to make controls static?

Extra: if you can also help me do the chat bubble behind the text, it would be great.

Here is my code:

public class ConversationView implements WhatAppView {
    private static Label nameLabel, statusLabel;
    private static TextField messageTextField;
    static TextArea messagesTextArea;
    private static GridPane conversationSection;
    private static Label changeViewLink;
    private static Button sendMsgButton;

// private static int rowIndex = 1;

public void showView() {
    AppMain.stage.setResizable(false);
    AppMain.stage.setWidth(350);
    AppMain.stage.setHeight(550);
    BorderPane rootPane = new BorderPane();
    rootPane.setPadding(new Insets(5, 5, 5, 5));

    final int sectionHeight = 55;

    StackPane contactSection = new StackPane();
    nameLabel = new Label("RW");
    statusLabel = new Label("Online");
    changeViewLink = new Label("Go Back");
    changeViewLink.setStyle("-fx-text-fill: blue;");
    changeViewLink.styleProperty().bind(
            Bindings.when(changeViewLink.hoverProperty())
                    .then(new SimpleStringProperty("-fx-underline: true; -fx-text-fill: blue;"))
                    .otherwise(new SimpleStringProperty("-fx-underline: false; -fx-text-fill: blue;")));
    changeViewLink.setOnMouseClicked(new EventHandler<MouseEvent>() {
        public void handle(MouseEvent event) {
            AppMain.changeView(new ChatsView());
        }
    });
    contactSection.getChildren().addAll(nameLabel, statusLabel, changeViewLink);
    StackPane.setAlignment(changeViewLink, Pos.TOP_RIGHT);
    StackPane.setAlignment(statusLabel, Pos.BOTTOM_CENTER);
    contactSection.setPrefHeight(sectionHeight);

    conversationSection = new GridPane();
    conversationSection.setStyle("-fx-background-image: url('whatsapp-wallpaper.jpg')");

    messagesTextArea = new TextArea();
    messagesTextArea.setEditable(false);
    // conversationSection.getColumnConstraints().addAll(new
    // ColumnConstraints(AppMain.stage.getWidth()/2 - 10), new
    // ColumnConstraints(AppMain.stage.getWidth()/2 - 10));
    conversationSection.add(messagesTextArea, 0, 0);
    conversationSection.setPrefSize(AppMain.stage.getWidth(), AppMain.stage.getHeight());
    // conversationSection.getStylesheets().add("conversation.css");
    ScrollPane scroll = new ScrollPane();
    scroll.setPrefSize(conversationSection.getWidth(), conversationSection.getHeight());
    scroll.setContent(conversationSection);

    FlowPane messageSection = new FlowPane();
    sendMsgButton = new Button("_Send");
    sendMsgButton.setDisable(true);
    sendMsgButton.setOnAction(new EventHandler<ActionEvent>() {
        @Override
        public void handle(ActionEvent event) {
            sendMsg();
        }
    });
    sendMsgButton.setPrefHeight(sectionHeight);
    Tooltip sendMsgToolTip = new Tooltip("Send Message");
    Tooltip.install(sendMsgButton, sendMsgToolTip);

    FlowPane.setMargin(sendMsgButton, new Insets(0, 0, 0, 5));
    messageTextField = new TextField();
    messageTextField.setPromptText("Type your message here...");
    Platform.runLater(new Runnable() { // 100% focus
        public void run() {
            messageTextField.requestFocus();
        }
    });
    messageTextField.setPrefWidth(AppMain.stage.getWidth() - AppMain.stage.getWidth() / 5);
    messageTextField.setPrefHeight(sectionHeight);
    messageTextField.setAlignment(Pos.TOP_LEFT);
    messageTextField.setOnKeyTyped(new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {
            if (messageTextField.getText() != null && !messageTextField.getText().isEmpty()) {
                sendMsgButton.setDisable(false);
            } else {
                sendMsgButton.setDisable(true);
            }
        }
    });
    messageTextField.setOnKeyPressed(new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent event) {
            if (event.getCode().equals(KeyCode.ENTER) && messageTextField.getText() != null
                    && !messageTextField.getText().isEmpty()) {
                sendMsg();
            }
        }
    });
    messageSection.getChildren().add(messageTextField);
    messageSection.getChildren().add(sendMsgButton);
    messageSection.setPrefHeight(sectionHeight);

    rootPane.setTop(contactSection);
    rootPane.setCenter(conversationSection);
    rootPane.setBottom(messageSection);

    Scene scene = new Scene(rootPane);
    AppMain.stage.setScene(scene);
    AppMain.stage.setTitle("WhatsApp");
}
}



public class AppMain extends Application {
static Stage stage;

@Override
public void start(Stage primaryStage) throws Exception {
    stage = primaryStage;
    AppMain.stage.show();
    changeView(new ConversationView());
}
public static void changeView(WhatAppView view) {
   view.showView();
}
}

public interface WhatAppView {
    public void showView();
}

1 Answers1

3

You can create a custom control to determine message alignment and aesthetics such as the bubble like appearance. As a fan of HBox and VBox, I would recommend their usage in combination with an SVGPath to decorate the message.

SVGPath's let you draw custom shapes by providing information on the lines, arcs etc. These aren't unique to java so there are a few resources available to see some basic/advanced examples. My recommendation would be to read here: SVGPath and use the TryitEditor to experiment

Here are two quick examples: enter image description here


When it comes to laying out the messages a VBox would suffice. You can bind the viewable children to an ObservableList of messages you would be able to iterate later. The added benefit of this is that adding to the list will update the UI automatically, and you'll also be able to iterate these later in the event you implement additional features such as delete, forward etc

I'd recommend reading up on the Bindings api, particularly bindContentBidirectional for more information on this


Using my above recommendations i've written a small example below you can reference. It's not visually impressive, but hopefully you can get some ideas from it, particularly this:

Extra: if you can also help me do the chat bubble behind the text, it would be great.

The messages/speech bubbles:

enum SpeechDirection{
    LEFT, RIGHT
}

public class SpeechBox extends HBox{
    private Color DEFAULT_SENDER_COLOR = Color.GOLD;
    private Color DEFAULT_RECEIVER_COLOR = Color.LIMEGREEN;
    private Background DEFAULT_SENDER_BACKGROUND, DEFAULT_RECEIVER_BACKGROUND;

    private String message;
    private SpeechDirection direction;

    private Label displayedText;
    private SVGPath directionIndicator;

    public SpeechBox(String message, SpeechDirection direction){
        this.message = message;
        this.direction = direction;
        initialiseDefaults();
        setupElements();
    }

    private void initialiseDefaults(){
        DEFAULT_SENDER_BACKGROUND = new Background(
                new BackgroundFill(DEFAULT_SENDER_COLOR, new CornerRadii(5,0,5,5,false), Insets.EMPTY));
        DEFAULT_RECEIVER_BACKGROUND = new Background(
                new BackgroundFill(DEFAULT_RECEIVER_COLOR, new CornerRadii(0,5,5,5,false), Insets.EMPTY));
    }

    private void setupElements(){
        displayedText = new Label(message);
        displayedText.setPadding(new Insets(5));
        displayedText.setWrapText(true);
        directionIndicator = new SVGPath();

        if(direction == SpeechDirection.LEFT){
            configureForReceiver();
        }
        else{
            configureForSender();
        }
    }

    private void configureForSender(){
        displayedText.setBackground(DEFAULT_SENDER_BACKGROUND);
        displayedText.setAlignment(Pos.CENTER_RIGHT);
        directionIndicator.setContent("M10 0 L0 10 L0 0 Z");
        directionIndicator.setFill(DEFAULT_SENDER_COLOR);

        HBox container = new HBox(displayedText, directionIndicator);
        //Use at most 75% of the width provided to the SpeechBox for displaying the message
        container.maxWidthProperty().bind(widthProperty().multiply(0.75));
        getChildren().setAll(container);
        setAlignment(Pos.CENTER_RIGHT);
    }

    private void configureForReceiver(){
        displayedText.setBackground(DEFAULT_RECEIVER_BACKGROUND);
        displayedText.setAlignment(Pos.CENTER_LEFT);
        directionIndicator.setContent("M0 0 L10 0 L10 10 Z");
        directionIndicator.setFill(DEFAULT_RECEIVER_COLOR);

        HBox container = new HBox(directionIndicator, displayedText);
        //Use at most 75% of the width provided to the SpeechBox for displaying the message
        container.maxWidthProperty().bind(widthProperty().multiply(0.75));
        getChildren().setAll(container);
        setAlignment(Pos.CENTER_LEFT);
    }
}

Conversation window:

public class ConversationView extends VBox{
    private String conversationPartner;
    private ObservableList<Node> speechBubbles = FXCollections.observableArrayList();

    private Label contactHeader;
    private ScrollPane messageScroller;
    private VBox messageContainer;
    private HBox inputContainer;

    public ConversationView(String conversationPartner){
        super(5);
        this.conversationPartner = conversationPartner;
        setupElements();
    }

    private void setupElements(){
        setupContactHeader();
        setupMessageDisplay();
        setupInputDisplay();
        getChildren().setAll(contactHeader, messageScroller, inputContainer);
        setPadding(new Insets(5));
    }

    private void setupContactHeader(){
        contactHeader = new Label(conversationPartner);
        contactHeader.setAlignment(Pos.CENTER);
        contactHeader.setFont(Font.font("Comic Sans MS", 14));
    }

    private void setupMessageDisplay(){
        messageContainer = new VBox(5);
        Bindings.bindContentBidirectional(speechBubbles, messageContainer.getChildren());

        messageScroller = new ScrollPane(messageContainer);
        messageScroller.setVbarPolicy(ScrollBarPolicy.AS_NEEDED);
        messageScroller.setHbarPolicy(ScrollBarPolicy.NEVER);
        messageScroller.setPrefHeight(300);
        messageScroller.prefWidthProperty().bind(messageContainer.prefWidthProperty().subtract(5));
        messageScroller.setFitToWidth(true);
        //Make the scroller scroll to the bottom when a new message is added
        speechBubbles.addListener((ListChangeListener<Node>) change -> {
             while (change.next()) {
                 if(change.wasAdded()){
                     messageScroller.setVvalue(messageScroller.getVmax());
                 }
             }
        });
    }

    private void setupInputDisplay(){
        inputContainer = new HBox(5);

        TextField userInput = new TextField();
        userInput.setPromptText("Enter message");

        Button sendMessageButton = new Button("Send");
        sendMessageButton.disableProperty().bind(userInput.lengthProperty().isEqualTo(0));
        sendMessageButton.setOnAction(event-> {
            sendMessage(userInput.getText());
            userInput.setText("");
        });

        //For testing purposes
        Button receiveMessageButton = new Button("Receive");
        receiveMessageButton.disableProperty().bind(userInput.lengthProperty().isEqualTo(0));
        receiveMessageButton.setOnAction(event-> {
            receiveMessage(userInput.getText());
            userInput.setText("");
        });

        inputContainer.getChildren().setAll(userInput, sendMessageButton, receiveMessageButton);
    }

    public void sendMessage(String message){
        speechBubbles.add(new SpeechBox(message, SpeechDirection.RIGHT));
    }

    public void receiveMessage(String message){
        speechBubbles.add(new SpeechBox(message, SpeechDirection.LEFT));
    }
}

Output:

enter image description here

Peter
  • 1,592
  • 13
  • 20
  • For some reason, text keep getting clipped off with ellipsis at the end when I used your code in my SWT based desktop application. Could you spare some thoughts on this? Thanks a lot for your valuable comment at the first place. – Rahul Jan 14 '19 at 12:20