一、前言
这两天在做Emoji表情开发,查查了资料,这让我知道如果没有规则是多么一件可怕的事情,正常Emoji应该是同一套的编码才对,但是发现Android IOS 上虽然编码是一致的但是Emoji表情图片完全不一致啊,竟然连小小的三星都自己自己搞一套Emoji图片,这对用户体验是一件多么残酷的事情啊,我真是日了狗了,大家请看下面的图片:
我说你们这两家在搞什么鬼?这哪天一端给另一端发图片,如果不作处理,还真不知道发的什么意思,我现在的心情只能用一张图来表示了
为了良好的用户体验,更为了我不丢失程序猿的饭碗,只能两端实现统一的表情,那么就开始实现我们的表情,首先那,正常思路应该这样的,假如我们发了一段文字是这样的“我好[喜欢]你啊”,我们需要根据正则,解析出喜欢对应的资源ID,在用spannablestring来实现,大概就这样,先看看我们自己实现的表情界面
正常的Emoji应该这样,谷歌的那一套的图片确实也很可爱,但是我觉得这一套简洁明了。所以就用了这个,下面就让我们进入开发流程!
二、实现
首先那我们需要一套图片,就是Emoji图片,Demo上传自行拿吧,我们还需要一个配置文件,大概是这样的
就是图片跟文字相对应,当然你随便改文字,只要两端实现统一就行!
1:我们需要把这个加载出来
package com.qingyuan.emojidemo.emojiutils;
import android.content.Context;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
/**
* 读取表情配置文件
* @author xiaoyuan
*/
public class EmojiFileUtils {
public static List<String> getEmojiFile(Context context) {
try {
List<String> list = new ArrayList<String>();
InputStream in = context.getResources().getAssets().open("emoji");
BufferedReader br = new BufferedReader(new InputStreamReader(in,"UTF-8"));
String str = null;
while ((str = br.readLine()) != null) {
list.add(str);
}
return list;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
就是
直接把文件读出来,把对应的文字取出来
2:表情类
package com.qingyuan.emojidemo.emojiutils;
/**
* 表情对象
* @author xiaoyuan
*/
public class EmojiEntry {
/** 表情资源图片对应的ID */
private int id;
/** 表情资源对应的文字描述 */
private String character;
/** 表情资源的文件名 */
private String faceName;
/** 表情资源图片对应的ID */
public int getId() {
return id;
}
/** 表情资源图片对应的ID */
public void setId(int id) {
this.id=id;
}
/** 表情资源对应的文字描述 */
public String getCharacter() {
return character;
}
/** 表情资源对应的文字描述 */
public void setCharacter(String character) {
this.character=character;
}
/** 表情资源的文件名 */
public String getFaceName() {
return faceName;
}
/** 表情资源的文件名 */
public void setFaceName(String faceName) {
this.faceName=faceName;
}
}
表情类有一些我们需要的属性,注释已经很清楚了,自己看吧
3:我们需要算一算表情在下面一共多少页,我这没页是21个,算上删除键,正好是7页,140个表情,当然你感觉表情不够,你可以往里面拷贝,配置文件重新配置一下,我们下面这个类里面算多少页,每页多少个表情,解析表情等方法
package com.qingyuan.emojidemo.emojiutils;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.TextUtils;
import android.text.style.ImageSpan;
import android.util.Log;
import android.view.WindowManager;
import com.qingyuan.emojidemo.MyApp;
import com.qingyuan.emojidemo.R;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 表情工具类
* @author xiaoyuan
*/
public enum EmojiConversionUtils {
INSTANCE;
/**
* 每一页表情的个数
*/
private int pageSize = 20;
/**
* 保存于内存中的表情HashMap
*/
private HashMap<String, String> emojiMap = new HashMap<String, String>();
/**
* 保存于内存中的表情集合
*/
private List<EmojiEntry> emojis = new ArrayList<EmojiEntry>();
/**
* 表情分页的结果集合
*/
public List<List<EmojiEntry>> emojiLists = new ArrayList<List<EmojiEntry>>();
private EmojiConversionUtils() {
}
/**
* 得到一个SpanableString对象,通过传入的字符串,并进行正则判断
*
* @param context
* @param str
* @return
*/
public SpannableString getExpressionString(Context context, String str) {
SpannableString spannableString = new SpannableString(str);
// 正则表达式比配字符串里是否含有表情,如: 我好[开心]啊
String zhengze = "\\[[^\\]]+\\]";
// 通过传入的正则表达式来生成一个pattern
Pattern sinaPatten = Pattern.compile(zhengze, Pattern.CASE_INSENSITIVE);
try {
dealExpression(context, spannableString, sinaPatten, 0);
} catch (Exception e) {
Log.e("dealExpression", e.getMessage());
}
return spannableString;
}
/**
* 添加表情
*
* @param context
* @param imgId
* @param spannableString
* @return
*/
public SpannableString addFace(Context context, int imgId,
String spannableString) {
if (TextUtils.isEmpty(spannableString)) {
return null;
}
Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), imgId);
bitmap = Bitmap.createScaledBitmap(bitmap, 50, 50, true);
ImageSpan imageSpan = new ImageSpan(context, bitmap);
SpannableString spannable = new SpannableString(spannableString);
spannable.setSpan(imageSpan, 0, spannableString.length(),
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannable;
}
/**
* 对spanableString进行正则判断,如果符合要求,则以表情图片代替
*
* @param context
* @param spannableString
* @param patten
* @param start
* @throws Exception
*/
private void dealExpression(Context context,
SpannableString spannableString, Pattern patten, int start)
throws Exception {
Matcher matcher = patten.matcher(spannableString);
while (matcher.find()) {
String key = matcher.group();
// 返回第一个字符的索引的文本匹配整个正则表达式,ture 则继续递归
if (matcher.start() < start) {
continue;
}
String value = emojiMap.get(key);
if (TextUtils.isEmpty(value)) {
continue;
}
int resId = context.getResources().getIdentifier(value, "drawable",
context.getPackageName());
// 通过上面匹配得到的字符串来生成图片资源id
// Field field=R.drawable.class.getDeclaredField(value);
// int resId=Integer.parseInt(field.get(null).toString());
if (resId != 0) {
Bitmap bitmap = BitmapFactory.decodeResource(
context.getResources(), resId);
bitmap = Bitmap.createScaledBitmap(bitmap, getEmojiSize(), getEmojiSize(), true);
// 通过图片资源id来得到bitmap,用一个ImageSpan来包装
@SuppressWarnings("deprecation")
ImageSpan imageSpan = new ImageSpan(bitmap);
// 计算该图片名字的长度,也就是要替换的字符串的长度
int end = matcher.start() + key.length();
// 将该图片替换字符串中规定的位置中
spannableString.setSpan(imageSpan, matcher.start(), end,
Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
if (end < spannableString.length()) {
// 如果整个字符串还未验证完,则继续。。
dealExpression(context, spannableString, patten, end);
}
break;
}
}
}
/**
* 初始化
*/
public void init(Context context) {
ParseData(EmojiFileUtils.getEmojiFile(context), context);
}
/**
* 解析字符
*
* @param data
*/
private synchronized void ParseData(List<String> data, Context context) {
if (this.emojiLists.size() > 0) return;
if (data == null) {
return;
}
System.out.println("this.emojiPageViews.size():" + this.emojiLists.size());
EmojiEntry emojEentry;
try {
for (String str : data) {
String[] text = str.split(",");
String fileName = text[0]
.substring(0, text[0].lastIndexOf("."));
emojiMap.put(text[1], fileName);
int resID = context.getResources().getIdentifier(fileName,
"drawable", context.getPackageName());
if (resID != 0) {
emojEentry = new EmojiEntry();
emojEentry.setId(resID);
emojEentry.setCharacter(text[1]);
emojEentry.setFaceName(fileName);
emojis.add(emojEentry);
}
}
int pageCount = (int) Math.ceil(emojis.size() / 20);
System.out.println("this.emojis.size():" + emojis.size() + ",pageCount:" + pageCount);
for (int i = 0; i < pageCount; i++) {
emojiLists.add(getData(i));
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取分页数据
*
* @param page
* @return
*/
private List<EmojiEntry> getData(int page) {
int startIndex = page * pageSize;
int endIndex = startIndex + pageSize;
if (endIndex > emojis.size()) {
endIndex = emojis.size();
}
List<EmojiEntry> list = new ArrayList<EmojiEntry>();
List<EmojiEntry> sublist = emojis.subList(startIndex, endIndex);
list.addAll(sublist);
if (list.size() < pageSize) {
for (int i = list.size(); i < pageSize; i++) {
EmojiEntry object = new EmojiEntry();
list.add(object);
}
}
if (list.size() == pageSize) {
EmojiEntry object = new EmojiEntry();
object.setId(R.drawable.emoji_item_delete);
list.add(object);
}
return list;
}
//适配安卓屏幕分辨率
private int getEmojiSize() {
WindowManager wm = (WindowManager) MyApp.getmContext()
.getSystemService(Context.WINDOW_SERVICE);
int size = wm.getDefaultDisplay().getHeight() * 200 / 1920;
return size;
}
}
类 有点长,不过我已经全部加注释了,每个方法,一些重要的地方!
4:就下面表情的显示。初始化游标,表情点击事件的处理,如下
package com.qingyuan.emojidemo.emojiutils;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.os.Message;
import android.support.v4.view.ViewPager;
import android.support.v4.view.ViewPager.OnPageChangeListener;
import android.text.SpannableString;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout.LayoutParams;
import com.qingyuan.emojidemo.R;
import java.util.ArrayList;
import java.util.List;
public class EmojiWidget implements OnItemClickListener {
private Context context;
private Activity activity; // 用来表明是哪个activity发起的
/** 显示表情页的ViewPager */
private ViewPager emoji_viewpage;
/** 游标显示布局 */
private LinearLayout emoji_cursor;
/** 表情页界面集合 */
private ArrayList<View> emojiPageViews;
/** 表情数据填充器 */
private List<EmojiAdapter> emojiAdapters;
/** 游标点集合 */
private ArrayList<ImageView> emojiCursorViews;
/** 表情集合 */
private List<List<EmojiEntry>> emojis;
/** 当前表情页 */
private int current = 0;
/** 输入框*/
private EditText et_sendmessage;
/**标志*/
private int UI_FLAG;
/**更新UI*/
private Handler mUIHandler;
public EmojiWidget(Activity activity, final Context context, int UI_FLAG,Handler mUIHandler, EditText et_sendmessage) {
this.UI_FLAG = UI_FLAG;
this.mUIHandler = mUIHandler;
this.emojis = EmojiConversionUtils.INSTANCE.emojiLists;
if (this.emojis == null || this.emojis.size() == 0) {
EmojiConversionUtils.INSTANCE.init(context);
this.emojis = EmojiConversionUtils.INSTANCE.emojiLists;
}
this.et_sendmessage = et_sendmessage;
this.activity = activity;
this.context = context;
Init_View();
Init_viewPager();
Init_Point();
Init_Data();
}
/** 初始化控件 */
private void Init_View() {
this.emoji_viewpage = (ViewPager) activity.findViewById(R.id.emoji_viewpage);
this.emoji_cursor = (LinearLayout) activity.findViewById(R.id.emoji_cursor);
}
/** 初始化显示表情的ViewPager */
@SuppressWarnings("deprecation")
private void Init_viewPager() {
this.emojiPageViews = new ArrayList<View>();
// 左侧添加空页
View nullView1 = new View(context);
// 设置透明背景
nullView1.setBackgroundColor(Color.TRANSPARENT);
emojiPageViews.add(nullView1);
// 中间添加表情页
this.emojiAdapters = new ArrayList<EmojiAdapter>();
for (int i = 0; i < emojis.size(); i++) {
GridView view = new GridView(context);
EmojiAdapter adapter = new EmojiAdapter(context, emojis.get(i));
view.setAdapter(adapter);
emojiAdapters.add(adapter);
view.setOnItemClickListener(this);
view.setNumColumns(7);
view.setBackgroundColor(Color.TRANSPARENT);
view.setHorizontalSpacing(1);
view.setVerticalSpacing(1);
view.setStretchMode(GridView.STRETCH_COLUMN_WIDTH);
view.setCacheColorHint(0);
view.setPadding(5, 0, 5, 0);
view.setSelector(new ColorDrawable(Color.TRANSPARENT));
view.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT,
LayoutParams.WRAP_CONTENT));
view.setGravity(Gravity.CENTER);
this.emojiPageViews.add(view);
}
// 右侧添加空页面
View nullView2 = new View(context);
// 设置透明背景
nullView2.setBackgroundColor(Color.TRANSPARENT);
this.emojiPageViews.add(nullView2);
}
/** 初始化填充数据 */
private void Init_Data() {
this.emoji_viewpage.setAdapter(new ViewPagerAdapter(this.emojiPageViews));
this.emoji_viewpage.setCurrentItem(1);
current = 0;
this.emoji_viewpage.setOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageSelected(int arg0) {
refreshUI(PAGE_SELECTED, arg0); // 页面更新
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
@Override
public void onPageScrollStateChanged(int arg0) {
}
});
}
/** 初始化游标 */
private void Init_Point() {
this.emojiCursorViews = new ArrayList<ImageView>();
ImageView imageView;
for (int i = 0; i < emojiPageViews.size(); i++) {
imageView = new ImageView(context);
imageView.setBackgroundResource(R.drawable.emoji_cursor_1);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(
new ViewGroup.LayoutParams(LayoutParams.WRAP_CONTENT,
LayoutParams.WRAP_CONTENT));
layoutParams.leftMargin = 10;
layoutParams.rightMargin = 10;
layoutParams.width = 8;
layoutParams.height = 8;
emoji_cursor.addView(imageView, layoutParams);
if (i == 0 || i == emojiPageViews.size() - 1) {
imageView.setVisibility(View.GONE);
}
if (i == 1) {
imageView.setBackgroundResource(R.drawable.emoji_cursor_2);
}
this.emojiCursorViews.add(imageView);
}
}
/** 刷新UI向主线程*/
private void refreshUI(int arg1,Object obj) {
Message msg = this.mUIHandler.obtainMessage();
msg.what = this.UI_FLAG;
msg.arg1 = arg1;
msg.obj = obj;
msg.sendToTarget();
}
public static final int PAGE_SELECTED = 0x1024;
/** 用来对接UI的刷新,保证始终在主线程*/
public void refreshWidgetUI(Message msg) {
switch(msg.arg1) {
case PAGE_SELECTED:onPageSelected((Integer)msg.obj);
}
}
/** 当切换页面时会回调该方法*/
private void onPageSelected(int arg0) {
current = arg0 - 1;
// 描绘分页点
draw_Point(emojiCursorViews, arg0);
// 如果是第一屏或者是最后一屏禁止滑动,其实这里实现的是如果滑动的是第一屏则跳转至第二屏,如果是最后一屏则跳转到倒数第二屏.
if (arg0 == emojiCursorViews.size() - 1 || arg0 == 0) {
if (arg0 == 0) {
emoji_viewpage.setCurrentItem(arg0 + 1);// 第二屏 会再次实现该回调方法实现跳转.
emojiCursorViews.get(1).setBackgroundResource(R.drawable.emoji_cursor_2);
} else {
emoji_viewpage.setCurrentItem(arg0 - 1);// 倒数第二屏
emojiCursorViews.get(arg0 - 1).setBackgroundResource(
R.drawable.emoji_cursor_2);
}
}
}
/** 绘制游标背景 */
private void draw_Point(ArrayList<ImageView> emojiCursorViews, int index) {
for (int i = 1; i < emojiCursorViews.size(); i++) {
if (index == i) {
emojiCursorViews.get(i).setBackgroundResource(R.drawable.emoji_cursor_2);
} else {
emojiCursorViews.get(i).setBackgroundResource(R.drawable.emoji_cursor_1);
}
}
}
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int position, long arg3) {
EmojiEntry emoji = (EmojiEntry) this.emojiAdapters.get(this.current).getItem(position);
if (emoji.getId() == R.drawable.emoji_item_delete) {
int selection = et_sendmessage.getSelectionStart();
String text = et_sendmessage.getText().toString();
if (selection > 0) {
String text2 = text.substring(selection - 1);
if ("]".equals(text2)) {
int start = text.lastIndexOf("[");
int end = selection;
et_sendmessage.getText().delete(start, end);
return;
}
et_sendmessage.getText().delete(selection - 1, selection);
}
}
if (!TextUtils.isEmpty(emoji.getCharacter())) {
SpannableString spannableString = EmojiConversionUtils.INSTANCE
.addFace(this.context, emoji.getId(), emoji.getCharacter());
et_sendmessage.append(spannableString);
}
}
}
5:表情的适配器
package com.qingyuan.emojidemo.emojiutils;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import com.qingyuan.emojidemo.R;
import java.util.List;
/**
* 表情适配器
* @author xiaoyuan
*/
public class EmojiAdapter extends BaseAdapter {
private List<EmojiEntry> data;
private LayoutInflater inflater;
private int size = 0;
public EmojiAdapter(Context context, List<EmojiEntry> list) {
this.inflater = LayoutInflater.from(context);
this.data = list;
this.size = list.size();
}
@Override
public int getCount() {
return this.size;
}
@Override
public Object getItem(int position) {
return data.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@SuppressWarnings("deprecation")
@Override
public View getView(int position, View convertView, ViewGroup parent) {
EmojiEntry emoji = data.get(position);
ViewHolder viewHolder = null;
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = inflater.inflate(R.layout.emoji_item, null);
viewHolder.emoji_item_face = (ImageView) convertView
.findViewById(R.id.emoji_item_face);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
if (emoji.getId() == R.drawable.emoji_item_delete) {
convertView.setBackgroundDrawable(null);
viewHolder.emoji_item_face.setImageResource(emoji.getId());
} else if (TextUtils.isEmpty(emoji.getCharacter())) {
convertView.setBackgroundDrawable(null);
viewHolder.emoji_item_face.setImageDrawable(null);
} else {
viewHolder.emoji_item_face.setTag(emoji);
viewHolder.emoji_item_face.setImageResource(emoji.getId());
}
return convertView;
}
/** 缓存的holder*/
class ViewHolder {
public ImageView emoji_item_face;
}
}
6:ViewPager适配器
package com.qingyuan.emojidemo.emojiutils;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.view.View;
import java.util.List;
/**
* ViewPager适配器
* @author xiaoyuan
*/
public class ViewPagerAdapter extends PagerAdapter {
private List<View> pageViews;
public ViewPagerAdapter(List<View> pageViews) {
super();
this.pageViews=pageViews;
}
// 显示数目
@Override
public int getCount() {
return pageViews.size();
}
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == arg1;
}
@Override
public int getItemPosition(Object object) {
return super.getItemPosition(object);
}
@Override
public void destroyItem(View arg0, int arg1, Object arg2) {
((ViewPager)arg0).removeView(pageViews.get(arg1));
}
/***
* 获取每一个item�?类于listview中的getview
*/
@Override
public Object instantiateItem(View arg0, int arg1) {
((ViewPager)arg0).addView(pageViews.get(arg1));
return pageViews.get(arg1);
}
}
7:最后一步,我们只需要调一句话就可以把表情显示出来。
package com.qingyuan.emojidemo;
import android.content.Context;
import android.text.SpannableString;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import com.qingyuan.emojidemo.emojiutils.EmojiConversionUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Created by xiaoyuan on 16/3/11.
* 适配器
*/
public class ChatAdapter extends BaseAdapter {
private List<Person> persons = new ArrayList<>();
private Context mContext;
public ChatAdapter(Context context){
this.mContext = context;
}
public void setData(Person person) {
persons.add(person);
}
@Override
public int getCount() {
return persons.size();
}
@Override
public Object getItem(int position) {
return persons.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public int getItemViewType(int position) {
return persons.get(position).direction ? 0 : 1;
}
public int getViewTypeCount() {
return 2;
}
ViewHolder viewHolder = null;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
int view_type = getItemViewType(position);
Person person = persons.get(position);
if(convertView == null){
switch (view_type){
case 0:
viewHolder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.chat_right,
parent, false);
viewHolder.content = (TextView) convertView
.findViewById(R.id.right_content);
convertView.setTag(viewHolder);
break;
case 1:
viewHolder = new ViewHolder();
convertView = LayoutInflater.from(mContext).inflate(R.layout.chat_left,
parent, false);
viewHolder.content = (TextView) convertView
.findViewById(R.id.left_content);
convertView.setTag(viewHolder);
break;
}
}else {
viewHolder = (ViewHolder) convertView.getTag();
}
/**
* 解析表情
*/
SpannableString spannableString = EmojiConversionUtils.INSTANCE.getExpressionString(mContext,person.content);
viewHolder.content.setText(spannableString);
return convertView;
}
public class ViewHolder{
TextView content;
}
}
没错,就是解析表情下面那一句代码,就可以实现我们上面图中的效果,呦西,好了,对了,还有几个布局的类我就不贴上了,自己下载自己看吧!Emoji表情做完了,长篇大论我也不会啊,注释写的已经很明白了,如果有不明白的地方可以问我或者自行研究,
三:总结
表情算是告一段落了,但是我还希望所有平台都能是同一套的编码跟图片,无规矩不成方圆吗!对不对,如果上述代码有什么bug或者不妥之处,请指出,共同学习嘛!
完事了
四:疑问
我发现微信跟支付宝是第三方键盘的Emoji也能显示同一套的图片,不知道是怎么做的,还是内置了图片,但是环形跟脉脉其他的就不能显示一样,果然大厂出来的就是不一样,如果谁知道可以告诉我一下,毕竟这对用户体验非常好,两端实现一致性!