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;
- }
- }
- }
示例的效果:
示例比较简单,做的就比较粗糙了,实践做项目时候,可能还会遇到一些细小问题,比如表情不居中或者不和文字对齐,这些就要对症处理了。
示例工程链接:
pan.baidu.com/s/1i4X2re1提取密码:d4jq
来源: https://juejin.im/entry/5a22a889f265da431d3c7de2