Android中的表情处理在于,发送的表情要转换成Unicode码进行发送,接收的时候再通过Unicode码解析成相应的表情图片,这个Unicode码是通用的,所以不会导致Android和Ios显示混乱,但是他们的表情资源是不一样的,也就是我们通常看见的两个系统的手机表情显示不一样,再加上Android版本的表情看上去不好看,所以我们需要有一套自己的表情资源,映射着统一的Unicode码。
Emoji-1是一个完善的可以设置google、ios、emojione、twitter四种表情资源的开源库,通过添加依赖的方式就能使用,只不过不能外部设置表情的大小。
你可以很轻松的读懂他的设置代码,并且根据自己的聊天界面去修改他demo中的代码。
第一次写聊天界面,记录一下:
首先是左右聊天信息显示的问题,我的思路是,建一个MessageData类,里面记录着每一条message所需要的信息,像头像地址(应该是静态的,在打开聊天界面的时候就应该通过网络获取初始化,包括对方的和自己的)、名字、消息内容、时间(如果有,在一定的时间间隔之外显示,后台计算,如果超过时间间隔就返回,否则不用返回,Android端有就显示,没有就不显示)等,然后我们还需要一个type字段,因为我们要知道这条消息是显示在左边还是右边(也就是接收到的消息还是发送出去的消息)。
package com.cloudcns.lottery.util.emoji;
/**
* @author 马鹏昊
* @date {2017.8.4}
* @des 聊天的每一条消息的属性类
* @updateAuthor
* @updateDate
* @updateDes
*/
public class MessageData {
//对方的头像地址
public static String otherPortrait ;
//我的头像地址
public static String myPortrait ;
//左侧显示
public static final int LEFT_SHOW = 0 ;
//右侧显示
public static final int RIGHT_SHOW = 1 ;
//标志着在左侧(接收的)显示还是右侧(发送的)
private int type ;
//发送者头衔
private String rank ;
//发送者昵称
private String name ;
//消息内容
private String content ;
//发送时间(比较上一条的时间,如果大于5分钟则显示)
private String date ;
public String getRank() {
return rank;
}
public void setRank(String rank) {
this.rank = rank;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
public int getType() {
return type;
}
public void setType(int type) {
this.type = type;
}
}
然后我们实现RecyclerView的adapter,在它的onBindViewHolder()方法中做文章,就是根据不同的type设置不同的效果:
关于左右分别显示,我的思路是ViewHolder中的View(也就是RecyclerView每一项的布局)最外层设置为LinearLayout,oriention设置成vertical,然后在其中,最上面定义显示时间的控件,再下面是整个消息体的显示布局,在ViewHolder中除了绑定需要显示数据的控件之外,把消息体显示布局的父容器也绑定,这样我们在onBindViewHolder中可以根据type去设置LinearLayout.LayoutParams的gravity属性为Gravity.LEFT还是Gravity.RIGHT来决定消息体是显示在左边还是右边。
时间显示可以根据后台返回的date是否为空来显示,有的话就把显示时间的控件设置为VISIBLE,并且setText(),没有则置为GONE。
package com.cloudcns.lottery.util.emoji;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;
import com.cloudcns.lottery.R;
import com.facebook.drawee.view.SimpleDraweeView;
import com.vanniktech.emoji.EmojiInformation;
import com.vanniktech.emoji.EmojiTextView;
import com.vanniktech.emoji.EmojiUtils;
import java.util.ArrayList;
import java.util.List;
public final class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ChatViewHolder> {
private final List<MessageData> messages = new ArrayList<>();
@Override
public ChatViewHolder onCreateViewHolder(final ViewGroup parent, final int viewType) {
final LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
return new ChatViewHolder(layoutInflater.inflate(R.layout.adapter_chat, parent, false));
}
@Override
public void onBindViewHolder(final ChatViewHolder chatViewHolder, final int position) {
final MessageData messageData = messages.get(position);
//类型
int type = messageData.getType();
LinearLayout.LayoutParams params ;
if (type == MessageData.LEFT_SHOW) {
//默认最左侧,但必须要设置,否则复用会混乱
params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.LEFT;
chatViewHolder.mContainer.setLayoutParams(params);
chatViewHolder.portrait.setImageURI(MessageData.otherPortrait);
}else if(type == MessageData.RIGHT_SHOW){
params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.RIGHT;
chatViewHolder.mContainer.setLayoutParams(params);
chatViewHolder.portrait.setImageURI(MessageData.myPortrait);
}
//发送者头衔
String rank = messageData.getRank();
//发送者昵称
String name = messageData.getName();
//消息内容
String content = messageData.getContent();
//发送时间(比较上一条的时间,如果大于5分钟则显示)
String date = messageData.getDate();
final EmojiInformation emojiInformation = EmojiUtils.emojiInformation(content);
final int res = R.dimen.emoji_size_default;
// if (emojiInformation.isOnlyEmojis && emojiInformation.emojis.size() == 1) {
// res = R.dimen.emoji_size_single_emoji;
// } else if (emojiInformation.isOnlyEmojis && emojiInformation.emojis.size() > 1) {
// res = R.dimen.emoji_size_only_emojis;
// } else {
// res = R.dimen.emoji_size_default;
// }
if (TextUtils.isEmpty(date)) {
chatViewHolder.date.setVisibility(View.GONE);
}else {
chatViewHolder.date.setVisibility(View.VISIBLE);
chatViewHolder.date.setText(date);
}
chatViewHolder.rank.setText(rank);
chatViewHolder.name.setText(name);
chatViewHolder.textView.setEmojiSizeRes(res, false);
chatViewHolder.textView.setText(content);
}
@Override
public int getItemCount() {
return messages.size();
}
/**
* 本方发送消息加入消息列表
*
* @param message 所编辑的本次发送的消息
*/
public void add(final MessageData message) {
messages.add(message);
notifyDataSetChanged();
}
static class ChatViewHolder extends RecyclerView.ViewHolder {
final RelativeLayout mContainer;
final TextView date;
final EmojiTextView textView;
final SimpleDraweeView portrait;
final TextView rank;
final TextView name;
ChatViewHolder(final View view) {
super(view);
mContainer = (RelativeLayout) view.findViewById(R.id.container);
date = (TextView) view.findViewById(R.id.date);
textView = (EmojiTextView) view.findViewById(R.id.adapter_chat_text_view);
portrait = (SimpleDraweeView) view.findViewById(R.id.portrait);
rank = (TextView) view.findViewById(R.id.rank);
name = (TextView) view.findViewById(R.id.name);
}
}
}
关于设置MessageData的部分(这里数据是写死的,替换成网络获取的即可):
final String text = editText.getText().toString().trim();
if (text.length() > 0) {
MessageData messageData = new MessageData();
messageData.setType(MessageData.RIGHT_SHOW);
messageData.setContent(text);
messageData.setRank("军长");
messageData.setName("Stephen.Curry");
String date = "15:34" ;//后台给的时间(如果和上次消息的时间间隔大于?分钟则显示,后台算)
if (!TextUtils.isEmpty(date)) {
messageData.setDate(date);
}
chatAdapter.add(messageData);
recyclerView.scrollToPosition(chatAdapter.getItemCount()-1);
editText.setText("");
}
注意recyclerView.scrollToPosition(chatAdapter.getItemCount()-1);显示添加的最新一条消息。
到此为止,还有一个问题,聊天界面通常有一个标题栏,上面有返回键和当前和你聊天的人的名字,或者还有更多设置,那么当你打开软键盘的时候,你会发现整个布局被顶到上面去了,这怎么行呢,你可以通过设置当前Activity的android:windowSoftInputMode="adjustResize|stateHidden..."来设置成不顶布局,但此时就变成了覆盖了,我的思路是覆盖的时候通过滚动RecyclerView来实现不覆盖,但是我没有找到当软键盘显示的时候的监听方法,在网上找到了一个大神写的可以实现监听软键盘显示/隐藏的监听器,完美解决了我的问题。使用这个监听器的方法:
mKeyboardChangeListener = new KeyboardChangeListener(this);
mKeyboardChangeListener.setKeyBoardListener(this);
ChatActivity extends MyBaseActivity implements KeyboardChangeListener.KeyBoardListener
@Override
public void onKeyboardChange(boolean isShow, int keyboardHeight) {
if (isShow){
recyclerView.scrollToPosition(chatAdapter.getItemCount()-1);
}
}
附上大神的实现代码:
package com.cloudcns.lottery.util.emoji;
import android.app.Activity;
import android.os.Build;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
/**
* simple and powerful Keyboard show/hidden listener,view {@android.R.id.content} and {@ViewTreeObserver.OnGlobalLayoutListener}
* Created by yes.cpu@gmail.com 2016/7/13.
*/
public class KeyboardChangeListener implements ViewTreeObserver.OnGlobalLayoutListener {
private static final String TAG = "ListenerHandler";
private View mContentView;
private int mOriginHeight;
private int mPreHeight;
private KeyBoardListener mKeyBoardListen;
public interface KeyBoardListener {
/**
* call back
* @param isShow true is show else hidden
* @param keyboardHeight keyboard height
*/
void onKeyboardChange(boolean isShow, int keyboardHeight);
}
public void setKeyBoardListener(KeyBoardListener keyBoardListen) {
this.mKeyBoardListen = keyBoardListen;
}
public KeyboardChangeListener(Activity contextObj) {
if (contextObj == null) {
Log.i(TAG, "contextObj is null");
return;
}
mContentView = findContentView(contextObj);
if (mContentView != null) {
addContentTreeObserver();
}
}
private View findContentView(Activity contextObj) {
return contextObj.findViewById(android.R.id.content);
}
private void addContentTreeObserver() {
mContentView.getViewTreeObserver().addOnGlobalLayoutListener(this);
}
@Override
public void onGlobalLayout() {
int currHeight = mContentView.getHeight();
if (currHeight == 0) {
Log.i(TAG, "currHeight is 0");
return;
}
boolean hasChange = false;
if (mPreHeight == 0) {
mPreHeight = currHeight;
mOriginHeight = currHeight;
} else {
if (mPreHeight != currHeight) {
hasChange = true;
mPreHeight = currHeight;
} else {
hasChange = false;
}
}
if (hasChange) {
boolean isShow;
int keyboardHeight = 0;
if (mOriginHeight == currHeight) {
//hidden
isShow = false;
} else {
//show
keyboardHeight = mOriginHeight - currHeight;
isShow = true;
}
if (mKeyBoardListen != null) {
mKeyBoardListen.onKeyboardChange(isShow, keyboardHeight);
}
}
}
public void destroy() {
if (mContentView != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
mContentView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
}
}
}
地址:
点击打开链接