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

(全文完)

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

发表回复

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