最近发现了一个emoji表情框架,在githubhttps://github.com/rockerhieu/emojicon中,图片多到不能自理,不过了解了实现原理之后基本可以根据自己需要修改或者自己定制emoji图片表情。
调用
- 界面布局
可看出自定义了一个TextView和EditText,添加的唯一功能就是根据显示表情的代码替换成相应的表情
<com.rockerhieu.emojicon.EmojiconTextView
android:id="@+id/txtEmojicon"
android:text="@string/i_love_emojicon"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.rockerhieu.emojicon.EmojiconEditText
android:id="@+id/editEmojicon"
android:hint="@string/hint"
android:text="@string/i_love_emojicon"
emojicon:emojiconSize="28sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<FrameLayout
android:id="@+id/emojicons"
android:layout_width="match_parent"
android:layout_height="fill_parent"/>
- activity实现
通过EmojiconsFragment实现表情界面,并实现两个回调函数,因为EmojiconsFragment要调用activity的这两个方法
public class MainActivity extends FragmentActivity implements EmojiconGridFragment.OnEmojiconClickedListener, EmojiconsFragment.OnEmojiconBackspaceClickedListener {
EmojiconEditText mEditEmojicon;
EmojiconTextView mTxtEmojicon;
CheckBox mCheckBox;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mEditEmojicon = (EmojiconEditText) findViewById(R.id.editEmojicon);
mTxtEmojicon = (EmojiconTextView) findViewById(R.id.txtEmojicon);
setEmojiconFragment(false);
}
//EmojiconsFragment表情显示的fragment
private void setEmojiconFragment(boolean useSystemDefault) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.emojicons, EmojiconsFragment.newInstance(useSystemDefault))
.commit();
}
//表情点击回调
@Override
public void onEmojiconClicked(Emojicon emojicon) {
EmojiconsFragment.input(mEditEmojicon, emojicon);
}
//删除表情点击回调
@Override
public void onEmojiconBackspaceClicked(View v) {
EmojiconsFragment.backspace(mEditEmojicon);
}
}
原理
打开emojicon,可看到如下代码结构
既然我调用的是EmojiconsFragment,那就来看看他是怎么实现的。
EmojiconsFragment的初始化
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.emojicons, container, false);
final ViewPager emojisPager = (ViewPager) view.findViewById(R.id.emojis_pager);
emojisPager.setOnPageChangeListener(this);
// we handle recents
EmojiconRecents recents = this;
//初始化viewpager adapter,一个历史记录和5个图片类别
mEmojisAdapter = new EmojisPagerAdapter(getFragmentManager(), Arrays.asList(
EmojiconRecentsGridFragment.newInstance(mUseSystemDefault),
EmojiconGridFragment.newInstance(People.DATA, recents, mUseSystemDefault),
EmojiconGridFragment.newInstance(Nature.DATA, recents, mUseSystemDefault),
EmojiconGridFragment.newInstance(Objects.DATA, recents, mUseSystemDefault),
EmojiconGridFragment.newInstance(Places.DATA, recents, mUseSystemDefault),
EmojiconGridFragment.newInstance(Symbols.DATA, recents, mUseSystemDefault)
));
emojisPager.setAdapter(mEmojisAdapter);
......
//设置删除按钮的触摸监听,重写监听类是为了实现能够长按时一直删除图片。
view.findViewById(R.id.emojis_backspace).setOnTouchListener(new RepeatListener(1000, 50, new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mOnEmojiconBackspaceClickedListener != null) {
mOnEmojiconBackspaceClickedListener.onEmojiconBackspaceClicked(v);
}
}
}));
// get last selected page
//获取最后退出时显示的是哪一页
mRecentsManager = EmojiconRecentsManager.getInstance(view.getContext());
int page = mRecentsManager.getRecentPage();
// last page was recents, check if there are recents to use
// if none was found, go to page 1
//缺省值为0,所以要判断一下
if (page == 0 && mRecentsManager.size() == 0) {
page = 1;
}
//跳到上次退出时候的那个tab
if (page == 0) {
onPageSelected(page);
}
else {
emojisPager.setCurrentItem(page, false);
}
return view;
}
public void onAttach(Activity activity) {
super.onAttach(activity);
//获取activity或者父fragment实现的OnEmojiconBackspaceClickedListener监听器
//由此可知道在activity没有实现的话会报异常
if (getActivity() instanceof OnEmojiconBackspaceClickedListener) {
mOnEmojiconBackspaceClickedListener = (OnEmojiconBackspaceClickedListener) getActivity();
} else if(getParentFragment() instanceof OnEmojiconBackspaceClickedListener) {
mOnEmojiconBackspaceClickedListener = (OnEmojiconBackspaceClickedListener) getParentFragment();
} else {
throw new IllegalArgumentException(activity + " must implement interface " + OnEmojiconBackspaceClickedListener.class.getSimpleName());
}
}
RepeatListener类实现了OnTouchListener接口,在里面定义了Handle类来实现每次间隔一定时间点击一次删除。
private Runnable handlerRunnable = new Runnable() {
@Override
public void run() {
if (downView == null) {
return;
}
//重复移除添加runnable,每次延迟normalInterval秒,实现按住删除
handler.removeCallbacksAndMessages(downView);
handler.postAtTime(this, downView, SystemClock.uptimeMillis() + normalInterval);
clickListener.onClick(downView);
}
};
//当按住删除时,使用handler发送个延时信息然后在handler中实现重复点击
case MotionEvent.ACTION_DOWN:
downView = view;
handler.removeCallbacks(handlerRunnable);
handler.postAtTime(handlerRunnable, downView, SystemClock.uptimeMillis() + initialInterval);
clickListener.onClick(view);
EmojiconGridFragment是如何显示图片的呢?他的每个item的布局是EmojiconTextView,一个继承TextView的类,是怎么显示的了图片的?
看一下EmojiAdapter的item的布局
public View getView(int position, View convertView, ViewGroup parent) {
Emojicon emoji = getItem(position);
ViewHolder holder = (ViewHolder) v.getTag();
holder.icon.setText(emoji.getEmoji());
return v;
}
class ViewHolder {
EmojiconTextView icon;
}
//emoji.getEmoji()的数据就是5个图片类里面的数据,
//
public static final Emojicon[] DATA = new Emojicon[]{
Emojicon.fromCodePoint(0x1f604),
Emojicon.fromCodePoint(0x1f603),
Emojicon.fromCodePoint(0x1f600),
Emojicon.fromCodePoint(0x1f60a),
Emojicon.fromChar((char) 0x263a),
.......
}
当使用这些十六进制数据的时候就算不使用图片替代还是会有默认的图片(原生系统)有些手机厂商可能会修改这些默认的东西。所以还是使用自己添加的图片来的好
由上面看出shettext一下String字符串时就有图片显示了,所以EmojiconTextView 的settext()方法一定被重写了,看一下怎么写的。
public void setText(CharSequence text, BufferType type) {
if (!TextUtils.isEmpty(text)) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
//把文本转化成spannable之后调用下面的方法
EmojiconHandler.addEmojis(getContext(), builder, mEmojiconSize, mEmojiconTextSize, mTextStart, mTextLength, mUseSystemDefault);
text = builder;
}
super.setText(text, type);
}
EmojiconHandler 类负责将图片代码转化成图片并且实现TextView和EditText的图文的混排。
首先在静态块里面将图片的16进制表示和图片资源以key-value存储起来。
static {
// People
sEmojisMap.put(0x1f604, R.drawable.emoji_1f604);
sEmojisMap.put(0x1f603, R.drawable.emoji_1f603);
sEmojisMap.put(0x1f600, R.drawable.emoji_1f600);
sEmojisMap.put(0x1f60a, R.drawable.emoji_1f60a);
sEmojisMap.put(0x263a, R.drawable.emoji_263a);
sEmojisMap.put(0x1f609, R.drawable.emoji_1f609);
......
}
addEmojis方法在两个自定义控件里面都有调用。
/**
* Convert emoji characters of the given Spannable to the according emojicon.
*
* @param context
* @param text
* @param emojiSize
* @param index
* @param length
* @param useSystemDefault
*/
public static void addEmojis(Context context, Spannable text, int emojiSize, int textSize, int index, int length, boolean useSystemDefault) {
if (useSystemDefault) {
return;
}
//省略的都是获取text所对应的资源id,赋值到icon中
......
//该方法定义图文混排,
if (icon > 0) {
text.setSpan(new EmojiconSpan(context, icon, emojiSize, textSize), i, i + skip, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
然后看一下EmojiconSpan的实现
EmojiconSpan 继承DynamicDrawableSpan 该类是用来设置复合文本的,具体可点击这里参考
class EmojiconSpan extends DynamicDrawableSpan {
......
//这里设置了要显示的图片
public Drawable getDrawable() {
if (mDrawable == null) {
try {
mDrawable = mContext.getResources().getDrawable(mResourceId);
mHeight = mSize;
mWidth = mHeight * mDrawable.getIntrinsicWidth() / mDrawable.getIntrinsicHeight();
mTop = (mTextSize - mHeight) / 2;
mDrawable.setBounds(0, mTop, mWidth, mTop + mHeight);
} catch (Exception e) {
// swallow
}
}
return mDrawable;
}
......
}
由此就大致实现了图片的显示。
总结一下,就是
- 通过静态块间图片代码和对应的资源通过key-value保存起来。
- 显示图片时,通过重写控件EditText和TextView,使用spannable将要显示的String文本转化成图片实现图文混排。