程序员的英文代号
聊天UI是我们一直在努力的工作,而在今天的帖子中,我们将精确地构建它!
更好的是...我们将与Pubnub集成在一起,使该应用程序几乎可以作为基本的聊天应用程序正常运行,这非常壮观。 在本节中,我们将介绍UI,存储(外部化),Pubnub及其JSON API ......我们还将使用InteractionDialog
来显示传入消息的通知...
在开始之前,您需要登录pubnub.com并注册一个帐户,在订阅和推送时,您将需要两个ID。
我们还需要安装Pubnub cn1lib及其依赖项,将以下文件放置在项目层次结构下的lib目录中: BouncyCastleCN1Lib.cn1lib , Pubnub-CodeNameOne-3.7.4.cn1lib和json.cn1lib 。
将文件放入lib目录后,右键单击项目并选择“ Codename One→Refresh Libs”。 这会将库安装到您的类路径中,并允许您在享受代码完成等功能的同时使用它们。
您还将需要聊天气泡的图像,特别是以下图像:
而这个 :
主题变更
我们需要首先设置主题元素,稍后将用于气泡聊天,我们实际上需要上面提到的两个气泡以映射到UIID的BubbleMe
和BubbleThem
。 因此,我们首先添加主题元素BubbleMe
,在其中将透明度设置为0(因为我们将使用图像边框),将前景色设置为白色ffffff
:
然后,我们需要将语音气泡的填充设置为文本不会位于语音箭头或边框本身的顶部。 我们将填充设置为毫米,以保持设计的便携性,并在左侧设置3mm以便留出箭头的空间:
现在,我们需要使用图像边框向导和前面提到的chat-bubble-left.png
图像来剪切图像边框。 请注意,为了使边框正常工作,我们将线条尽可能地靠近彼此:
我们需要对BubbleThem
UIID进行BubbleThem
操作,唯一的区别是chat-bubble-right.png
和右侧而不是左侧的较大填充。
消息类
到目前为止,我们在单个文件中使用了大多数内部类,这使演示变得相当简单。 但是,现在我们将添加一个新的Message
类,该类将表示发送/接收的消息,并封装PubNub通信所需的JSON解析逻辑。
此类是可Externalizeable
,这意味着我们可以相对轻松地将其存储到存储中。 这对于保持对话中过去的消息很重要:
public class Message implements Externalizable {
private long time;
private String senderId;
private String recepientId;
private String picture;
private String name;
private String message;
/**
* Required default constructor for externalizable to work...
*/
public Message() {}
public Message(String senderId, String recepientId, String picture, String name, String message) {
this.senderId = senderId;
this.recepientId = recepientId;
this.picture = picture;
this.name = name;
this.message = message;
}
public Message(JSONObject obj) {
try {
time = Long.parseLong(obj.getString("time"));
senderId = obj.getString("fromId");
recepientId = obj.getString("toId");
message = obj.getString("message");
name = obj.getString("name");
picture = obj.getString("pic");
} catch (JSONException ex) {
// will this ever happen?
Log.e(ex);
}
}
public JSONObject toJSON() {
JSONObject obj = createJSONObject("fromId", senderId,
"toId", recepientId,
"name", name,
"pic", picture,
"time", Long.toString(System.currentTimeMillis()),
"message", message);
return obj;
}
/**
* Helper method to create a JSONObject
*/
JSONObject createJSONObject(String... keyValues) {
try {
JSONObject o = new JSONObject();
for(int iter = 0 ; iter < keyValues.length ; iter += 2) {
o.put(keyValues[iter], keyValues[iter + 1]);
}
return o;
} catch(JSONException err) {
// will this ever happen?
err.printStackTrace();
}
return null;
}
@Override
public int getVersion() {
return 1;
}
@Override
public void externalize(DataOutputStream out) throws IOException {
out.writeLong(time);
Util.writeUTF(senderId, out);
Util.writeUTF(recepientId, out);
Util.writeUTF(picture, out);
Util.writeUTF(name, out);
Util.writeUTF(message, out);
}
@Override
public void internalize(int version, DataInputStream in) throws IOException {
time = in.readLong();
senderId = Util.readUTF(in);
recepientId = Util.readUTF(in);
picture = Util.readUTF(in);
name = Util.readUTF(in);
message = Util.readUTF(in);
}
@Override
public String getObjectId() {
return "Message";
}
public long getTime() {
return time;
}
public String getSenderId() {
return senderId;
}
public String getRecepientId() {
return recepientId;
}
public String getPicture() {
return picture;
}
public String getName() {
return name;
}
public String getMessage() {
return message;
}
}
此类非常简单,请注意有关外部化的一些有趣的事情:
- 我们使用
Util.writeUTF
和Util.readUTF
,它们通过首先写入/读取一个布尔值以指示该值是否为null来增加对null字符串的支持。 -
getObjectId
是一个硬编码的字符串,而不是类似getClass().getName()
。 使用类名是一个非常常见的错误,这就是为什么我要提到它。 它可以在模拟器中工作,并且在开发过程中似乎可以工作,但是由于类名在某些设备上被混淆,升级将失败,并且可能导致严重的问题。 - 我们还需要默认的构造函数来支持外部化。
全局变量和初始化
首先,我们需要添加一些全局变量:
private Pubnub pb;
private Image roundedMeImage;
private final WeakHashMap<String, EncodedImage> roundedImagesOfFriends = new WeakHashMap<>();
这些应该很容易解释,pubnub代表用于推送的API。 roundedMeImage是我们之前创建的图像的缓存版本。 它允许我们以不同的形式重用该UI元素。 WeakHashMap
使我们可以缓存朋友的图片而不会触发内存泄漏...
我们还需要将其添加到init
方法中:
public void init(Object context) {
...
Util.register("Message", Message.class);
...
}
这有效地将Message
类注册到系统中,因此在加载时反序列化它时,我们可以识别该类。 开发人员经常会犯错误,即在类中使用静态初始化器代码进行自身注册。 这是有问题的,因为在读取类之前可能不会加载该类。
听消息
我们希望在登录showContactsForm
开始监听传入的消息,这种情况发生在showContactsForm
因此我们可以将此调用添加到该方法的顶部:
void showContactsForm(UserData data) {
listenToMessages();
...
}
private void listenToMessages() {
try {
pb = new Pubnub("pub-c-*********-****-****-****-*************", "sub-c-*********-****-****-****-*************");
pb.subscribe(tokenPrefix + uniqueId, new Callback() {
@Override
public void successCallback(String channel, Object message, String timetoken) {
Display.getInstance().callSerially(() -> {
respond(new Message((JSONObject)message));
});
}
});
} catch(PubnubException err) {
Log.e(err);
Dialog.show("Error", "There was a communication error: " + err, "OK", null);
}
}
通过pubnub订阅消息是微不足道的,我们转换了响应接收到的JSON对象,并将其发送到发布响应的方法。 请注意,由于响应是从EDT接收到的,因此我们将调用以串行方式包装在呼叫中,并且在处理与UI交互时,应该在EDT上进行处理。 稍后我们将更深入地进行响应处理...
PubNub的工作方式是提供您可以订阅和发布的消息队列,想象一下像电子邮件一样的消息,您可以在其中订阅邮件列表并处理传入的消息。 但是,如果您不听消息,则消息可能消失了......它们确实为持久队列提供了一个选项,该队列将为您保留最后100条消息,这对于此类应用程序非常有用!
我们将要使用的架构非常简单,每个人都只听自己的唯一ID。 将消息发送给特定人的方式只是发布到他的队列中,该人可以通过发布到您的人来回复。
我们在每封邮件中都包含发件人详细信息,从而使我们能够区分正在聊天的人。
聊天表格
这是我的Android设备上聊天的屏幕截图...聊天表格是通过这种方法创建的,虽然有点冗长但相对简单。
void showChatForm(ContactData d, Component source) {
Form chatForm = new Form(d.name);
// this identifies the person we are chatting with, so an incoming message will know if this is the right person...
chatForm.putClientProperty("cid", tokenPrefix + d.uniqueId);
chatForm.setLayout(new BorderLayout());
Toolbar tb = new Toolbar();
final Container chatArea = new Container(new BoxLayout(BoxLayout.Y_AXIS));
chatArea.setScrollableY(true);
chatArea.setName("ChatArea");
chatForm.setToolBar(tb);
chatForm.setBackCommand(new Command("Contacts") {
@Override
public void actionPerformed(ActionEvent evt) {
source.getComponentForm().showBack();
}
});
// Provides the ability to swipe the screen to go back to the previous form
SwipeBackSupport.bindBack(chatForm, (args) -> {
return source.getComponentForm();
});
// Gets a rounded version of our friends picture and caches it
Image roundedHimOrHerImage = getRoundedFriendImage(d.uniqueId, d.imageUrl);
// load the stored messages and add them to the form
java.util.List<Message> messages = (java.util.List<Message>)Storage.getInstance().readObject(tokenPrefix + d.uniqueId);
if(messages != null) {
for(Message m : messages) {
if(m.getRecepientId().equals(tokenPrefix + uniqueId)) {
respondNoLayout(chatArea, m.getMessage(), roundedHimOrHerImage);
} else {
sayNoLayout(chatArea, m.getMessage());
}
}
}
// to place the image on the right side of the toolbar we just use a command that does nothing...
Command himOrHerCommand = new Command("", roundedHimOrHerImage);
tb.addCommandToRightBar(himOrHerCommand);
// we type the message to the chat partner in the text field on the south side
TextField write = new TextField(30);
write.setHint("Write to " + d.name);
chatForm.addComponent(BorderLayout.CENTER, chatArea);
chatForm.addComponent(BorderLayout.SOUTH, write);
// the action listener for the text field creates a message object, converts it to JSON and publishes it to the listener queue
write.addActionListener((e) -> {
String text = write.getText();
final Component t = say(chatArea, text);
// we make outgoing messages translucent to indicate that they weren't received yet
t.getUnselectedStyle().setOpacity(120);
write.setText("");
final Message messageObject = new Message(tokenPrefix + uniqueId, tokenPrefix + d.uniqueId, imageURL, fullName, text);
JSONObject obj = messageObject.toJSON();
pb.publish(tokenPrefix + d.uniqueId, obj, new Callback() {
@Override
public void successCallback(String channel, Object message) {
// a message was received, we make it opauqe and add it to the storage
t.getUnselectedStyle().setOpacity(255);
addMessage(messageObject);
}
@Override
public void errorCallback(String channel, PubnubError error) {
chatArea.removeComponent(t);
chatArea.revalidate();
Dialog.show("Error", "Connection error message wasn't sent", "OK", null);
}
});
});
chatForm.show();
}
上面的方法有几件有趣的事情:
- chatArea包含所有聊天条目,请注意,我们明确为其命名! 以后很有用,当聊天消息到达时(如果我们处于聊天表单中),我们希望将该消息添加到该表单中...
- 我们在聊天表单
cid
上使用client属性来存储与我们聊天的用户。 这样,如果我们与其他联系人聊天,则来自另一联系人的传入消息将不会被推送到该对话中。 - 聊天区域是可滚动的,文本字段在南部。 注意,由于我们将布局设置为边框布局,因此窗体的内容窗格的默认可滚动性被隐式禁用。
- 我们使用了一个不执行任何操作的命令来将联系人图像放置在工具栏上,该命令比我们在上一个表格中的效果更简单,但是仍然非常不错。
- 如果存在来自存储的可用消息,则使用
Storage
类的对象外部化功能加载它们。 - 默认情况下,我们有一个
say
和respond
方法封装气泡聊天组件的创建。 但是,它们会执行一个很好的传入动画,当收到新消息时,我们不希望这样做,因此我们使用的版本在构建表单时不会对两者都进行布局 - 这是使用PubNub所需的全部代码! 真是太酷了...其他一切都交给我们处理了。
此方法使用了几种重要的方法,我们将一一介绍它们:
private Component say(Container chatArea, String text) {
Component t = sayNoLayout(chatArea, text);
t.setY(chatArea.getHeight());
t.setWidth(chatArea.getWidth());
t.setHeight(40);
chatArea.animateLayoutAndWait(300);
chatArea.scrollComponentToVisible(t);
return t;
}
private Component sayNoLayout(Container chatArea, String text) {
SpanLabel t = new SpanLabel(text);
t.setIcon(roundedMeImage);
t.setTextBlockAlign(Component.LEFT);
t.setTextUIID("BubbleMe");
chatArea.addComponent(t);
return t;
}
这两种方法实质上可以打印出我们必须说的聊天气泡。 后一种方法只是将跨度标签图标设置为我的图片(我们之前做过),然后将块向左对齐。 它还将我们先前创建的气泡UIID设置为span标签的文本部分。
前一种方法是通过设置组件的大小/位置,然后进行动画布局,使气泡流入正确的位置,从而使气泡从底部开始动画。
基本的响应方法几乎相同,但有一些小的更改:
private void respond(Container chatArea, String text, Image roundedHimOrHerImage) {
Component answer = respondNoLayout(chatArea, text, roundedHimOrHerImage);
answer.setX(chatArea.getWidth());
answer.setWidth(chatArea.getWidth());
answer.setHeight(40);
chatArea.animateLayoutAndWait(300);
chatArea.scrollComponentToVisible(answer);
}
private Component respondNoLayout(Container chatArea, String text, Image roundedHimOrHerImage) {
SpanLabel answer = new SpanLabel(text);
answer.setIcon(roundedHimOrHerImage);
answer.setIconPosition(BorderLayout.EAST);
answer.setTextUIID("BubbleThem");
answer.setTextBlockAlign(Component.RIGHT);
chatArea.addComponent(answer);
return answer;
}
这里的主要区别是UIID和右边的对齐方式,您还会注意到我们将发言人的图像作为参数传递,因为它可能不可用...
但是最大的区别是,这些方法不会直接为传入的聊天条目调用,而是使用:
private void respond(Message m) {
String clientId = (String)Display.getInstance().getCurrent().getClientProperty("cid");
addMessage(m);
EncodedImage rounded = getRoundedFriendImage(m.getSenderId(), m.getPicture());
if(clientId == null || !clientId.equals(m.getSenderId())) {
// show toast, we aren't in the chat form...
InteractionDialog toast = new InteractionDialog();
toast.setUIID("Container");
toast.setLayout(new BorderLayout());
SpanButton messageButton = new SpanButton(m.getMessage());
messageButton.setIcon(rounded);
toast.addComponent(BorderLayout.CENTER, messageButton);
int h = toast.getPreferredH();
toast.show(Display.getInstance().getDisplayHeight() - h - 10, 10, 10, 10);
UITimer uit = new UITimer(() -> {
toast.dispose();
});
uit.schedule(3000, false, Display.getInstance().getCurrent());
messageButton.addActionListener((e) -> {
uit.cancel();
toast.dispose();
showChatForm(getContactById(m.getSenderId()), Display.getInstance().getCurrent());
});
} else {
Container chatArea = getChatArea(Display.getInstance().getCurrent().getContentPane());
respond(chatArea, m.getMessage(), rounded);
}
}
您可以从原始的订阅调用中回想起,这是从pubnub收到消息时内部调用的方法。 那时我们已经登录,但是在这两种情况中的一种情况下,我们可能正在与其他人聊天,或者甚至可能处于联系人表单中,因此id将为null或与当前ID不同,因此我们将显示一个交互您可以在下面的视频中看到对话框效果,否则我们将调用上面看到的常规response方法:
交互对话框只是放置在分层窗格上的容器。 因此,它不会像常规对话框那样阻止输入,因此,如果我在消息到达时与某人聊天,这不会造成问题。 我们使用UITimer ` to automatically dispose of the dialog, the `UITimer
很方便,因为它在EDT上的调用不同于常规计时器,因此工作量很小。
我们使用通知按钮,因此我们可以单击它,然后直接转到聊天窗口,如上面视频结尾所示。
private Container getChatArea(Container cnt) {
String n = cnt.getName();
if(n != null && n.equals("ChatArea")) {
return cnt;
}
for(Component cmp : cnt) {
if(cmp instanceof Container) {
Container cur = getChatArea((Container)cmp);
if(cur != null) {
return cur;
}
}
}
return null;
}
您会注意到我们使用getChatArea
上面的getChatArea
作为抽象聊天区域的简单工具。 我们也可以将对chatArea的引用保存在类本身中,但这可能会导致内存泄漏,因此这很简单。 我不太担心线程或竞争条件,因为几乎所有内容都在EDT上。
private EncodedImage getRoundedFriendImage(String uid, String imageUrl) {
EncodedImage roundedHimOrHerImage = roundedImagesOfFriends.get(uid);
if(roundedHimOrHerImage == null) {
roundedHimOrHerImage = URLImage.createToStorage(roundPlaceholder, "rounded" + uid, imageUrl, URLImage.createMaskAdapter(mask));
roundedImagesOfFriends.put(uid, roundedHimOrHerImage);
}
return roundedHimOrHerImage;
}
我们在更早的时候提到了这种方法,它相对简单,所以我不加理会。 它与我们之前文章中的“我的照片”大致相同,仅用于朋友的照片。
private void addMessage(Message m) {
String personId;
// if this is a message to me then store based on sender otherwise store based on recepient
if(m.getRecepientId().equals(tokenPrefix + uniqueId)) {
personId = m.getSenderId();
} else {
personId = m.getRecepientId();
}
java.util.List messages = (java.util.List)Storage.getInstance().readObject(personId);
if(messages == null) {
messages = new ArrayList();
}
messages.add(m);
Storage.getInstance().writeObject(personId, messages);
}
最后一种感兴趣的方法将消息数据存储到存储器中。 我们使用消息的数组列表,这很简单。
潜在的改进
下次我们将讨论推送消息,在此版本的应用程序中,未到达的消息会丢失...这是一个问题。 在这种情况下,我们将使用来自操作系统的推送通知来提醒另一端。
我们还可以为消息队列启用存储,从而允许消息保持高速缓存,直到用户下次登录为止。
如果用户从多台设备登录时他看不到聊天记录,并且一台设备将捕获可能无法到达另一台设备的传入消息,则会出现一个问题。这可以通过中央数据库体系结构或可能的持久队列轻松解决。 pubnub(尽管我在那里没有经验)。
其他可能的改进可以是:
- Facebook的邀请朋友和分享按钮
- iOS的未读计数和图标标记
- 通知栏条目
- 附件和更复杂的数据
所有这些都应该是微不足道的。 我们将在本系列的下一个更新中发布完整的源代码和项目,让大家一起玩。
本系列其他文章
这是一系列持续不断的帖子,包括以下部分:
- 第1部分–初始用户界面
- 第2部分–使用Google登录
- 第3部分–使用Facebook登录
- 第4部分–联系人表格
- 第5部分–聊天表格
- 第6部分-集成Push(即将推出)
翻译自: https://www.javacodegeeks.com/2015/08/building-a-chat-app-with-codename-one-part-5.html
程序员的英文代号