Android仿微信表情的实现

Android仿微信表情的实现

1 需求来源

开发App时,常常有信息多样化、美观化的需求,比如信息支持表情的显示。如果使用系统原生的emoji表情,会出现较多的不符合需求的问题,比如显示不够美观,在不同手机显示可能不一样,更要命的是低版本手机可能会显示黑白表情甚至直接不支持显示,这些问题都是比较不友好的,严重影响体验,所以就不得不实现自定义表情样式了。

上图,左边是短信的输入表情,系统自带的,右边为微信UI 的表情输入,有没有发现原生的很丑样?对比微信的表情,会发现微信的做都非常美观大方,而且在不同手机上显示也是一致的。

2 开发分析

分析下微信的表情实现,会发现它的表情数据其实是个中括号的字符串,把微信表情信息选中复制,拷贝到其他App 输入框内粘贴就可以看到原始数据了,如下图:

Android 开发中,如果了解过Span 的开发者应该就能大概猜测到这个是怎么实现的了。使用ImageSpan 就可以达到这样的效果。ImageSpan 继承 DynamicDrawableSpan,而DynamicDrawableSpan 继承ReplacementSpan,由这个关系就可以知道ImageSpan 可以把某些信息代替成Image来显示。

微信的表情,实际上就是一大堆比如”[大笑]、[撇嘴]、[色]……”这样的一个词表,每个词对应着一个图片信息。由此,就可以使用ImageSpan 把字符串内的那些”[大笑]、[撇嘴]、[色]……”等代替成一个Image 显示出来。通过构造SpannableString,把对应的字符串设置成ImageSpan来显示,即可到达微信表情的效果。

3 实现

由上面信息,先设计个字符串到图片等映射表,代码如下:


import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;public class EmotionData {
public static LinkedHashMap<String, Integer> EMOTION_CLASSIC_MAP;
static {
EMOTION_CLASSIC_MAP = new LinkedHashMap<>();
EMOTION_CLASSIC_MAP.put("[微笑]", R.drawable.expression_1);
EMOTION_CLASSIC_MAP.put("[撇嘴]", R.drawable.expression_2);
EMOTION_CLASSIC_MAP.put("[色]", R.drawable.expression_3);
EMOTION_CLASSIC_MAP.put("[发呆]", R.drawable.expression_4);
EMOTION_CLASSIC_MAP.put("[得意]", R.drawable.expression_5);
EMOTION_CLASSIC_MAP.put("[流泪]", R.drawable.expression_6);
EMOTION_CLASSIC_MAP.put("[害羞]", R.drawable.expression_7);
EMOTION_CLASSIC_MAP.put("[闭嘴]", R.drawable.expression_8);
EMOTION_CLASSIC_MAP.put("[睡]", R.drawable.expression_9);
EMOTION_CLASSIC_MAP.put("[大哭]", R.drawable.expression_10);
EMOTION_CLASSIC_MAP.put("[尴尬]", R.drawable.expression_11);
EMOTION_CLASSIC_MAP.put("[发怒]", R.drawable.expression_12);
EMOTION_CLASSIC_MAP.put("[调皮]", R.drawable.expression_13);
EMOTION_CLASSIC_MAP.put("[呲牙]", R.drawable.expression_14);
EMOTION_CLASSIC_MAP.put("[惊讶]", R.drawable.expression_15);
EMOTION_CLASSIC_MAP.put("[难过]", R.drawable.expression_16);
// empty 17
EMOTION_CLASSIC_MAP.put("[囧]", R.drawable.expression_18);
EMOTION_CLASSIC_MAP.put("[抓狂]", R.drawable.expression_19);
EMOTION_CLASSIC_MAP.put("[吐]", R.drawable.expression_20);
EMOTION_CLASSIC_MAP.put("[偷笑]", R.drawable.expression_21);
EMOTION_CLASSIC_MAP.put("[愉快]", R.drawable.expression_22);
EMOTION_CLASSIC_MAP.put("[白眼]", R.drawable.expression_23);
EMOTION_CLASSIC_MAP.put("[傲慢]", R.drawable.expression_24);
// 这里省略了很多映射代码
EMOTION_CLASSIC_MAP.put("[耶]", R.drawable.expression_113);
EMOTION_CLASSIC_MAP.put(emojiString(0x1F47B), R.drawable.expression_114);
EMOTION_CLASSIC_MAP.put(emojiString(0x1F64F), R.drawable.expression_115);
EMOTION_CLASSIC_MAP.put(emojiString(0x1F4AA), R.drawable.expression_116);
EMOTION_CLASSIC_MAP.put(emojiString(0x1F389), R.drawable.expression_117);
EMOTION_CLASSIC_MAP.put(emojiString(0x1F381), R.drawable.expression_118);
EMOTION_CLASSIC_MAP.put("[红包]", R.drawable.expression_111);
}
private static String emojiString(int code) {
return new String(Character.toChars(code));
}
public static int size() {
return EMOTION_CLASSIC_MAP.size();
}
public static int getImgByName(String imgName) {
Integer integer = EMOTION_CLASSIC_MAP.get(imgName);
return integer == null ? -1 : integer;
}
}

上面代码通过LinkedHashMap 来关联着表情字符串和表情图片,主要是使得通过字符串(通过EmotionData.getImgByName)能过快速找到图片的ResId 信息。

字符串和图片关联搞定了,那么如何在一个正常的字符串内,快速优雅的找到对应的字符串之后构造ImageSpan呢——用正则表达式。微信表情中,每个都是使用 中括号”[]”包起来,这样也就是使得方便些正则表达式了。正则表达式如下:

private static Pattern sPatternEmotion =
Pattern.compile("\\[([\u4e00-\u9fa5\\w])+\\]|[\\ud83c\\udc00-\\ud83c\\udfff]|[\\ud83d\\udc00-\\ud83d\\udfff]|[\\u2600-\\u27ff]");

有正则表达式了,就可以方便编写一个SpannableMaker了,代码如下:

import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.ImageSpan;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class SpannableMaker {
private static Pattern sPatternEmotion =
Pattern.compile("\\[([\u4e00-\u9fa5\\w])+\\]|[\\ud83c\\udc00-\\ud83c\\udfff]|[\\ud83d\\udc00-\\ud83d\\udfff]|[\\u2600-\\u27ff]");
public static Spannable buildEmotionSpannable(Context context, String text, int textSize) {
Matcher matcherEmotion = sPatternEmotion.matcher(text);
SpannableString spannableString = new SpannableString(text);
while (matcherEmotion.find()) {
String key = matcherEmotion.group();
int imgRes = EmotionData.getImgByName(key);
if (imgRes != -1) {
int start = matcherEmotion.start();
ImageSpan span = createImageSpanByRes(imgRes, context, textSize);
spannableString.setSpan(span, start, start + key.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
return spannableString;
}
private static ImageSpan createImageSpanByRes(int imgRes, Context context, int textSize) {
Resources res = context.getResources();
Bitmap bitmap = BitmapFactory.decodeResource(res, imgRes);
ImageSpan span = null;
int size = textSize * 13 / 10;
Bitmap scaleBitmap = Bitmap.createScaledBitmap(bitmap, size, size, true);
span = new ImageSpan(context, scaleBitmap);
return span;
}
}

注意上面代码有两个细节:

1)构造SpannableString 时候需要传入textSize,主要目的是使得在不同控件上,能够根据字体大小构造出对应大小的Image,否则构造出的Image 不和控件的字体相应大小,就很难看了。

2)正则表达式内有四部分,如下:
“\\[([\u4e00-\u9fa5\\w])+\\]”
“[\\ud83c\\udc00-\\ud83c\\udfff]”
“[\\ud83d\\udc00-\\ud83d\\udfff]”
“[\\u2600-\\u27ff]”

第一行是匹配中文的,后面几个是因为微信表情中,有几个表情字符串不是使用中文,而是使用了原生的emoji,而原生的emoji本质也是字符串,后面三个正则就是匹配微信的非中文的其他几个emoji 字符串码了。把所有微信字符串表情拷贝出来,就会发现它有三小段是emoji 字符码的了下图为其中一部分

没有搞懂为什么微信会使用几个emoji码潜在里面而不全部使用中文的。如果不完全模仿微信,去掉那几个特定的emoji, 直接使用  “\\[([\u4e00-\u9fa5\\w])+\\]” 来匹配就可以了的,这个就是根据自己的工程来定的了。

总结上面的步骤:

1)根据原字符串新建一个spannableString;
2)把原字符通过正则表达式匹配到表情字符串;
3)通过LinkedHashMap,查找到对应的Image 资源,在加载资源构造一个ImageSan;
4)最后把 ImageSpan 设置到spannableString内;
5)把这个spannableString 设置到要现实的地方即可。

到此,对于微信表情的模仿基本就是完成了。剩下的就是要做一个表情面板,点击面板内的表情项后在相应的位置插入表情了,这个不是本文的重点。下面就是直接上代码简略带过了。

4 示例工程

下面是事例的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center_horizontal"
android:background="#f5f6f7"
>
<EditText
android:id="@+id/ed_emoji"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="#f5f6f7"
android:gravity="left|top"
android:padding="5dp"
android:layout_marginTop="10dp"
/>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="#dcdcdc"
/>
<GridView
android:id="@+id/grip_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:numColumns="7"
android:horizontalSpacing="2dp"
android:verticalSpacing="2dp"
android:padding="10dp"
android:clipToPadding="false"
/>
</LinearLayout>
</android.support.constraint.ConstraintLayout>

示例MainActivity的代码:

public class MainActivity extends AppCompatActivity {
private List<Note> mNote = EmotionData.getNotes();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
GridView gridView = (GridView)findViewById(R.id.grip_view);
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
Note note = (Note)adapterView.getAdapter().getItem(i);
EditText editText = (EditText)findViewById(R.id.ed_emoji);
int start = editText.getSelectionStart();
Editable editable = editText.getEditableText();
Spannable spannable = SpannableMaker.buildEmotionSpannable(MainActivity.this, note.getText(), (int)editText.getTextSize());
editable.insert(start, spannable);
}
});
gridView.setAdapter(new GridViewAdapter());
}
private class GridViewAdapter extends BaseAdapter {
@Override
public View getView(int position, View view, ViewGroup parent) {
if (!(view instanceof ImageView)) {
view = new ImageView(MainActivity.this);
}
ImageView imageView = (ImageView) view;
imageView.setImageResource(((Note) getItem(position)).getIconRes());
return view;
}
@Override
public int getCount() {
return mNote.size();
}
@Override
public Object getItem(int position) {
return mNote.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
}
}

示例的效果:

示例比较简单,做的就比较粗糙了,实践做项目时候,可能还会遇到一些细小问题,比如表情不居中或者不和文字对齐,这些就要对症处理了。

示例工程链接:https://pan.baidu.com/s/1i4X2re1 提取密码:d4jq

(全文完)

原文链接:https://blog.yuccn.net/archives/287.html

(欢迎转载本站文章,但请注明作者和出处 云域 – Yuccn

发表评论

电子邮件地址不会被公开。 必填项已用*标注