JavaFx實現仿微信聊天界面

效果:
在這裏插入圖片描述
這個界面實現並不是很難,之前想要用Swing實現一個,但是寫到一般發現自己是在不是很適應Swing的GUI界面製作的模式,前面學了不算短的時間的前端,還是比較適應這種模式。

其次,根據我的使用觀感來說,Swing的各種組件的特性方面似乎有些落後,自適應性、樣式調整等幾個方面似乎不是很與時俱進,當然,個人意見,如果是我學藝不精,還請不吝賜教。

左邊是一個成員列表,右上是消息區,右下輸入區,成員列表和輸入區添加較簡單。

難得就是消息區,首先是滾動區域,很明顯外面需要套一個ScrollPane,但是此時有一個小功能需要實現,就是當我們聊天的時候界面要始終保持在最下方,但是當我們看歷史消息的時候不能總是直接拉到最低部。實現這個功能最大的困難在於,按照正常想法應該是判斷添加時是否在最底部,如果是就在添加完成後將滾輪保持在底部,可問題在於渲染時間,很難保證在渲染後再移動滾輪,我的解決辦法是添加滾輪移動事件監聽,判斷移動種類,得到是否要保持滾輪在最底部。

第二層需要在裏面一行一行的添加消息,我弟一瞬間想到的是VBox,但其實用VBox的花會有一個很大的問題,就是消息必定是動態添加的,加上ScorollPane顯示滾動條的條件,這就涉及到第二層組件的高度動態變化。但VBox高度無法自動擴展,在這種情況下,因爲消息高度在顯示前無法獲取,VBox的動態變化管理也確實有難度,所以在最後把第二層組件換成了FlowPane,這部分解決。

接下來就是每個消息單元,消息單元使用HBox完美解決,目前每個單元裏包含頭像,消息兩個部分,根據是否是自己發出的消息判斷得到消息單元內的組件排列順序以及對齊方式就可以得到靠左靠右的效果。

最後的部分就是這個氣泡了,基於Label組件,添加圓角效果-fx-background-radius: 8px;,添加背景顏色-fx-background-color: rgb(179,231,244);就已經差不多了,接下來是那個三角,三角可以直接使用JavaFx的圖形組件Polygon製作一個三角形並調整相對位置,完美解決

接下來奉上代碼:
FXML部分:

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.layout.FlowPane?>

<AnchorPane focusTraversable="true" prefHeight="700.0" prefWidth="520.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="UI.chatroom.ChatroomController">
   <children>
      <TextArea fx:id="message" layoutX="139.0" layoutY="618.0" prefHeight="82.0" prefWidth="381.0" promptText="在此輸入消息......" wrapText="true" />
      <ScrollPane fx:id="scrollPane" hbarPolicy="NEVER" layoutX="140.0" prefHeight="619.0" prefWidth="380.0">
         <content>
            <FlowPane fx:id="messagesList" prefHeight="45.0" prefWidth="366.0" />
         </content></ScrollPane>
      <ScrollPane hbarPolicy="NEVER" layoutX="-1.0" layoutY="-1.0" prefHeight="703.0" prefWidth="140.0" style="-fx-background: rgb(39,43,45);">
         <content>
            <FlowPane fx:id="membersList" prefHeight="46.0" prefWidth="125.0" />
         </content>
      </ScrollPane>
   </children>
</AnchorPane>

Java代碼部分:

import database.struct.Member;
import database.struct.Message;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TextArea;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.scene.text.Font;

import java.net.URL;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Date;
import java.util.Objects;
import java.util.ResourceBundle;

public class ChatroomController implements Initializable {

    @FXML
    private FlowPane membersList;

    @FXML
    private TextArea message;

    @FXML
    private FlowPane messagesList;

    @FXML
    private ScrollPane scrollPane;

    public static int MyId;
    public static ArrayList<Member> members = new ArrayList<Member>();
    public ArrayList<Message> messages = new ArrayList<Message>();

    private boolean last = true;

    @Override
    public void initialize(URL url, ResourceBundle resources) {
        membersList.setPadding(new Insets(5));
        VBox.setVgrow(messagesList, Priority.ALWAYS);

        message.setOnKeyPressed(new EventHandler<KeyEvent>() {
            @Override
            public void handle(KeyEvent keyEvent) {
                if (keyEvent.getCode() == KeyCode.ENTER) {
                    String text = message.getText();
                    message.setText("");
                    Message theMessage = new Message(MyId, text, new Timestamp(new Date().getTime()));
                    messages.add(theMessage);
                    addMessageBox(theMessage);
                }
            }
        });
        scrollPane.vvalueProperty().addListener(new ChangeListener<Number>() {
            @Override
            public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
                if (last) {
                    scrollPane.setVvalue(1.0);
                    last = false;
                }
            }
        });

        getPersonalData();
        addMembers();

        for (Message message: messages) {
            addMessageBox(message);
        }
        scrollPane.setVvalue(1);
    }

    private void getPersonalData() {
        MyId = 2;
        members.add(new Member(1, "avatar.png", "一號", true));
        members.add(new Member(2, "avatar.png", "二號", true));
        members.add(new Member(3, "avatar.png", "三號", true));
        members.add(new Member(4, "avatar.png", "四號", true));
        members.add(new Member(5, "avatar.png", "五號", true));

        Message message1 = new Message(1, "那天你消失在人海里", new Timestamp(new Date().getTime()));
        Message message2 = new Message(4, "你的背影沉默得讓人恐懼 你說的那些問題 我回答得很堅定", new Timestamp(new Date().getTime()));
        Message message3 = new Message(2, "偏偏那個時候我最想你", new Timestamp(new Date().getTime()));
        Message message4 = new Message(2, "我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,我不曾愛過你", new Timestamp(new Date().getTime()));
        Message message5 = new Message(5, "我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,我不曾愛過你我不曾愛過你,我自己騙自己,", new Timestamp(new Date().getTime()));
        Message message6 = new Message(2, "我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,我不曾愛過你我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,", new Timestamp(new Date().getTime()));
        Message message7 = new Message(4, "我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,我不曾愛過你我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,那天你消失在人海里", new Timestamp(new Date().getTime()));
        Message message8 = new Message(4, "我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,我不曾愛過你我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,那天你消失在人海里那天你消失在人海里", new Timestamp(new Date().getTime()));
        Message message9 = new Message(4, "我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,我不曾愛過你我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,那天你消失在人海里那天你消失在人海里那天你消失在人海里那天你消失在人海里", new Timestamp(new Date().getTime()));
        Message message10 = new Message(4, "我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,我不曾愛過你我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,那天你消失在人海里那天你消失在人海里", new Timestamp(new Date().getTime()));
        Message message11 = new Message(4, "我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,我不曾愛過你我不曾愛過你,我自己騙自己,已經給你寫了信,又被我丟進海里,那天你消失在人海里那天你消失在人海里", new Timestamp(new Date().getTime()));

        messages.add(message1);
        messages.add(message2);
//        messages.add(message3);
        messages.add(message4);
        messages.add(message5);
        messages.add(message6);
        messages.add(message7);
        messages.add(message8);
        messages.add(message9);
    }

    private void addMembers() {
        for (Member member: members) {
            Image headImg = new Image(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(member.getHead())));
            ImageView head = new ImageView();
            head.setImage(headImg);
            head.setFitWidth(40);
            head.setFitHeight(40);

            Label name = new Label(member.getName());
            name.setTextFill(Color.rgb(255, 255, 255));
            Label status = new Label(member.getStatus() ? "在線" : "離線");
            status.setTextFill(Color.rgb(255, 255, 255));
            VBox info = new VBox(8, name, status);
            info.setPadding(new Insets(2, 0, 0, 8));
            membersList.getChildren().add(new HBox(head, info));
        }
    }

    private Member getMemberById(int senderId) {
        for (Member member: members) {
            if (member.getId() == senderId) {
                return member;
            }
        }
        return null;
    }

    private void addMessageBox(Message message) {
        Member sender = getMemberById(message.getSenderId());
        assert sender != null;
        Image headImg = new Image(Objects.requireNonNull(getClass().getClassLoader().getResourceAsStream(sender.getHead())));
        ImageView head = new ImageView();
        head.setImage(headImg);
        head.setFitWidth(40);
        head.setFitHeight(40);

        Label messageBubble = new Label(message.getMessage());
        messageBubble.setWrapText(true);
        messageBubble.setMaxWidth(220);
        messageBubble.setStyle("-fx-background-color: rgb(179,231,244); -fx-background-radius: 8px;");
        messageBubble.setPadding(new Insets(6));
        messageBubble.setFont(new Font(14));
        HBox.setMargin(messageBubble, new Insets(8, 0, 0, 0));

        boolean isMine = message.getSenderId() == MyId;
        double[] points;
        if (!isMine) {
            points = new double[]{
                    0.0, 5.0,
                    10.0, 0.0,
                    10.0, 10.0
            };
        } else {
            points = new double[]{
                    0.0, 0.0,
                    0.0, 10.0,
                    10.0, 5.0
            };
        }
        Polygon triangle = new Polygon(points);
        triangle.setFill(Color.rgb(179,231,244));
        HBox messageBox = new HBox();
        messageBox.setPrefWidth(366);
        messageBox.setPadding(new Insets(10, 5, 10, 5));
        if (isMine) {
            HBox.setMargin(triangle, new Insets(15, 10, 0, 0));
            messageBox.getChildren().addAll(messageBubble, triangle, head);
            messageBox.setAlignment(Pos.TOP_RIGHT);
        } else {
            HBox.setMargin(triangle, new Insets(15, 0, 0, 10));
            messageBox.getChildren().addAll(head, triangle, messageBubble);
        }

        last = scrollPane.getVvalue() == 1.0;
        messagesList.getChildren().add(messageBox);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章