仿微信表情输入键盘(支持 Gif 表情图文混排 )

简介

自定义的表情输入键盘在很多应用中都会有用到,譬如微信、QQ 等社交聊天软件中更是不可缺少的部分。本文将解析一下个人的自定义表情输入控件库 PandaEmoView 的实现和使用。

该库具有以下特点:

  • 支持 emoji 表情图片
  • 支持 gif 动态表情输入显示
  • 支持单张贴图表情(与微信收藏表情一致)
  • 支持题图表情库的添加删除

效果图:

效果图
效果图

快速使用

引入库

compile 'com.pandaq:PandaEmoView:1.0.0'

表情资源及配置文件

  • 默认的 emoji 和 gif 表情以及他们的配置文件是放在开发包 assets 目录下的,若表情比较多比较大也可自行修改源码在 APP 启动时从服务器下载。
assets 目录图片
assets 目录图片
  • emoji 表情配置文件
config 截图
config 截图
  • 非自定义 sticker 配置文件(自定义 sticker 是没有配置文件的)
sticker config 截图
sticker config 截图

具体使用规则

与表情输入控件相关的 EditText 必须使用 PandaEditText
PandaEditText 只是重写了 onKeyPreIme() 获取按返回键的通知,继承自 EditText 的控件可继承 PandaEditText 自定义

1.应用 Application 中进行全局参数配置

    private void configPandaEmoView() {
        new PandaEmoManager.Builder()
                .with(getApplicationContext()) // 传递 Context
                .configFileName("emoji.xml")// 配置文件名称
                .emoticonDir("face") // asset 下存放表情的目录路径(asset——> configFileName 之间的路径,结尾不带斜杠)
                .sourceDir("images") // 存放 emoji 表情资源文件夹路径(emoticonDir 图片资源之间的路径,结尾不带斜杠)
                .showAddTab(true)//tab栏是否显示添加按钮
                .showStickers(true)//tab栏是否显示贴图切换按键
                .showSetTab(true)//tab栏是否显示设置按钮
                .defaultBounds(30)//emoji 表情显示出来的宽高
                .cacheSize(1024)//加载资源到内存时 LruCache 缓存大小
                .defaultTabIcon(R.drawable.ic_default)//emoji表情Tab栏图标
                .emojiColumn(7)//单页显示表情的列数
                .emojiRow(3)//单页显示表情的行数
                .stickerRow(2)//单页显示贴图表情的行数
                .stickerColumn(4)//单页显示贴图表情的列数
                .maxCustomStickers(30)//允许添加的收藏表情数
                .imageLoader(new IImageLoader() {
                    @Override
                    public void displayImage(String path, ImageView imageView) { // 加载贴图表情的图片加载接口
                        Picasso.with(getApplicationContext())
                                .load(path)
                                .fit()
                                .centerCrop()
                                .into(imageView);
                    }
                })
                .build(); //构建 PandaEmoManager 单利
    }

2.使用此控件的 Activity 在 manifest 文件中配置

// 这句是一定要加上的。
android:windowSoftInputMode="adjustResize"

3.使用此控件的界面 xml 文件规则
布局规则如下图,lockView 即是我们正常显示内容的 View 它与表情输入控件 PandaEmoView 属于同一层级,父布局必须为纵向线性布局,且设置 lockView 权重为 1 ,PandaEmoView 高度包裹内容即可

布局规则说明
布局规则说明

4.使用控件的 Activity Java 代码设置

//界面控件初始化后 .attachEditText()绑定输入控件

//初始化 KeyBoardManager,PandaEmoView.attachEditText() 必须在后调用

主要使用类及公有方法概览

PandaEmoEditText

  • 表情输入框继承自 EditText 只对 onKeyIme() 进行复写用于监听输入键盘或者软键盘的弹出与关闭

PandaEmoView

  • 表情输入控件 View 继承自 RelativeLayout
方法 返回值 参数 描述
attachEditText(PandaEmoEditText input) void 表情输入控件(如使用自定义 EditText 可直接继承重写) 绑定输入控件
getAttachEditText() void 获取当前表情控件绑定的输入框控件
reloadEmos void position 重载表情控件数据后默认选中 Tab 的 position 添加或者删除表情数据后重载刷新表情控件

PandaEmoManager

  • PandaEmoManager 为核心配置类,表情控件的各种参数都通过此类的构造器进行配置
属性 类型 描述 默认值
EMOT_DIR String 默认表情图在 assets 中的路径 (assets 目录到配置 xml 文件之间的部分的路径,demo 中的 face) face
SOURCE_DIR String EMOT_DIR 目录下存放图片资源的文件夹路径 (demo 中的 images) source_default
STICKER_PATH String Sticker贴图包的存放目录,该目录下的每一个文件目录都为一个贴图包 /data/data/< package name>/files/sticker
CACHE_MAX_SIZE int 加载表情 LruCache 缓存大小 1024
DEFAULT_EMO_BOUNDS_DP int 表情图显示大小(非贴图表情) 30dp
defaultIcon int 表情 Tab 资源文件名 R.drawable.ic_default
mContext Context 上下文 null
mConfigFile String EMOT_DIR 目录下的 emoji 配置文件名称 emoji_default.xml
mIImageLoader IImageLoader Sticker 图片加载器接口,加载方式外部传入 null
MAX_CUSTOM_STICKER int 最大添加的自定义贴图表情数 30
EMOJI_ROW int emoji 表情单页行数 3
EMOJI_COLUMN int emoji 表情单页列数 7
STICKER_ROW int sticker 表情单页行数 2
STICKER_COLUMN int sticker 表情单页列数 4
showAddButton boolean tab 栏是否显示添加按钮 ture
showSetButton boolean tab 栏是否显示设置按钮 ture
showStickers boolean tab 栏是否显示贴图按钮(所以 sticker) ture
方法 返回值 参数 描述
init() void 无参 初始化 拼接 Sticker 路径及创建自定义贴图 文件夹 (STICKER_PATH + "/selfSticker" )
makePattern() Pattern 无参 创建 emoji 正则匹配器

剩余方法都为属性值的 getter() setter() 不在赘述。

PandaEmoManager.Builder

  • PandaEmoManager 的构造器类,属性及方法都与 PandaEmoManager 一一对应;

KeyBoardManager

  • KeyBoardManager 为输入法软键盘与表情输入控件协调管理类
属性 类型 描述 默认值
SHARE_PREFERENCE_NAME String 用于存储键盘高度的 SP 的名字 "EmotionKeyBoard"
SHARE_PREFERENCE_SOFT_INPUT_HEIGHT String 用于存储键盘高度的 key "EmotionKeyBoard"
mActivity Activity 控件依附的 Activity 界面 null
mEmotionView PandaEmoView 当前管理的表情输入控件 null
interceptBackPress boolean 是否拦截返回键 false
lockView View 锁定高度的 View(即同一线性父布局中,表情控件之外的布局视图) null
mOnEmotionButtonOnClickListener OnEmotionButtonOnClickListener 表情显示控制按钮监听 null
mOnInputShowListener OnInputShowListener 监听输入栏的弹出与关闭 null
方法 返回值 参数 描述
with() KeyBoardManager Activity : 当前输入控件依附的 Activity 初始化 KeyBoardManager 创建单例
bindToLockContent() KeyBoardManager lockView:切换需要锁定高度的 View 赋值给内部属性 lockView
bindToEmotionButton() KeyBoardManager View...: 多个 View 参数,切换控制按钮 为输入控件绑定控制按钮
setEmotionView() KeyBoardManager PandaEmoView: 被管理的表情控件 绑定当前管理的输入控件(绑定的控件必须在此之前调用 PandaEmoView.attachEditText() 否则内部将会空指针)
interceptBackPress() boolean null 在 Activity 的 backPressd() 中检查是否需要拦截返回键关闭输入栏而不是退出界面
hideInputLayout() void null 供外部调用关闭输入栏(不能未初始化直接调用)
showInputLayout() void null 供外部调用显示输入栏(不能未初始化直接调用)

EmoticonManager

  • EmoticonManager 为 emoji 表情加载管理类,此类提供方法将资源文件根据配置文件加载进内存,方法大多数为私有方法,源码中可查看注释。

StickerManager

  • StickerManager 为 sticker 表情加载管理类,此类提供方法将资源文件根据配置文件加载进内存,与 EmoticonManager 类似

PandaEmoTranslator

  • PandaEmoTranslator 为 emoji 表情 [文字] 转表情的转换工具类
方法 返回值 参数 描述
getInstance() PandaEmoTranslator null 获取文本表情转换器单例
setMaxGifPerView() void maxGifPerView: 每个 TextView 控件最多显示动态表情的个数 设置每个 TextView 控件最多显示动态表情的个数,超过此数全部显示为静态表情
getMaxGifPerView() int null 获取每个 TextView 控件最多显示动态表情的个数
makeGifSpannable() SpannableString classTag:PandaEmoView 依附的 ActivityTag(推荐使用 Activity.getLocalClassName());value : 待替换的文本 ;gif 运行回调(回调中需要刷新 TextView 重绘) 整段图文混排,支持 gif 和静态 emoji
makeEmojiSpannable() SpannableString classTag:PandaEmoView 依附的 ActivityTag(推荐使用 Activity.getLocalClassName());value : 待替换的文本 ;gif 运行回调(回调中需要刷新 TextView 重绘) 整段图文混排,所以内容转换为静态 emoji
resumeGif() void activityTag : makeGifSpannable() 传入的 tag 名 开始 activityTag 对应的所有 gif 表情执行
pauseGif() void 暂停所有的 Gif 表情运行
clearGif() void activityTag : makeGifSpannable() 传入的 tag 名 停止 activityTag 对应的所有 gif 表情执行,并将期从任务栈中移除

关于内存优化

因为表情,gif 表情,自定义贴图,表情包贴图这些都涉及到图片资源加载到内存中。因此开发过程中不可避免的也遇到了许多的内存优化相关的问题。

工具

就地取材,直接使用 Android Studio 的 Monitors 工具可以直观的查看到应用运行过程中内存的变化过程

优化点1 —— Gif 播放类的优化

  • 问题:
    参考网上的 gif 图文混排项目,虽然实现了 gif 与文字的图文混排效果,但存在致命的缺陷。该项目中每一个 gif 动态表情图都有一个对应的 Runable 对象去执行 gif 图片的逐帧播放,当一个表情重复输入也会有新的 Runable 对象去执行这样的操作,这样做的后果就是当输入的表情数量增加时,所消耗的内存是持续增长的。这显然不能满足生产使用的需求。
  • 解决方案:
    考虑到此处内存增加的原因是让表情动起来的 Runable 泛滥引起的,因此减少 Runable 的数量就是解决此处内存问题的关键。我的方案做的比较彻底,整个应用 gif 表情这一块儿都交给一个 Runable 去处理,这个 Runable 在 PandaTranslator 中进行图文转化时会被初始化
 // PandaTranslator 的 103 - 107 行 
 103                   if (mGifRunnable == null) {
 104                      mGifRunnable = new GifRunnable(gifDrawable, mHandler);
 105                   } else {
 106                      mGifRunnable.addGifDrawable(gifDrawable);
 107                   }

因为 PandaTranslator 是一个单例实现,所以在他初始化后 mGifRunnable 也将保持唯一性。无论是新建初始化还是 addGifDrawable() 都是把 Gif 表情对象放入 GifRunnable 中的一个 Map 中。Map 的 key value 分别是表情控件依附的 Activity 的 LocalName 和 一个 AnimatedGifDrawable 的 List。在 GifRunnable 的 run 方法中会根据当前的 Activity 的 LocalName 去取出对应的 AnimatedGifDrawable 列表,遍历执行并按第一张 gif 表情的帧间隔去刷新 Drawable 并触发 TextView 刷新回调

@Override
    public void run() {
        isRunning = true;
        if (currentActivity != null) {
            List<AnimatedGifDrawable> runningDrawables = mGifDrawableMap.get(currentActivity);
            if (runningDrawables != null) {
                for (AnimatedGifDrawable gifDrawable : runningDrawables) {
                    AnimatedGifDrawable.RunGifCallBack listener = gifDrawable.getUpdateListener();
                    List<AnimatedGifDrawable.RunGifCallBack> runningListener = listenersMap.get(currentActivity);
                    if (runningListener != null) {
                    // 避免一个 TextView 多个表情时重复添加回调
                        if (!runningListener.contains(listener)) {
                            runningListener.add(listener);
                        }
                    } else {
                        // 为空时肯定不存在直接添加
                        runningListener = new ArrayList<>();
                        runningListener.add(listener);
                        listenersMap.put(currentActivity, runningListener);
                    }
                    gifDrawable.nextFrame();
                }
                for (AnimatedGifDrawable.RunGifCallBack callBack : listenersMap.get(currentActivity)) {
                    if (callBack != null) {
                        callBack.run();
                    }
                }
                frameDuration = runningDrawables.get(0).getFrameDuration();
            }
        }
        mHandler.postDelayed(this, frameDuration);
    }

这样就实现了全局使用一个 Runable 来执行 gif 动起来的任务,不同的界面也仅需要将该界面的 AnimatedGifDrawable 对象加入任务 Map 即可。

优化点2 —— 界面暂停或退出时 Gif 播放资源同步退出回收

上面说到的将 AnimatedGifDrawable 列表加入任务 Map,只进不出显然是不科学的也会持续增加内存的消耗。我们希望在 Activity 退出时能将将当前 Activity 的 AnimatedGifDrawable 列表销毁移除,在界面不可见但是可能会恢复时(pause 状态)暂停 Runable 的执行,减少资源消耗。于是 GifRunable 提供了如下三个方法给外部调用

/**
     * 使用了表情转换的界面退出时调用,停止动态图handler
     */
    public void clearHandler(String activityName) {
        currentActivity = null;
        //清除当前页的数据
        mGifDrawableMap.remove(activityName);
        // 当退出当前Activity后没表情显示时停止 Runable 清除所有动态表情数据
        listenersMap.remove(activityName);
        if (mGifDrawableMap.size() == 0) {
            clearAll();
        }
    }

    private void clearAll() {
        mHandler.removeCallbacks(this);
        mHandler.removeCallbacksAndMessages(null);
        mGifDrawableMap.clear();
        isRunning = false;
    }

    /**
     * 启动运行
     */
    public void startHandler(String activityName) {
        currentActivity = activityName;
        if (mGifDrawableMap != null && mGifDrawableMap.size() > 0 && !isRunning) {
            run();
        }
    }

它的调用入口都在 PandaTranslator 中,然后我们只需在使用到 PandaEmoView 或者直接在 BaseActivity 的 onResume(),onPause(),onDestory() 中分别调用以下三个方法:

PandaTranslator.getInstance().resumeGif(activityLocalName);

PandaTranslator.getInstance().pauseGif();

PandaTranslator.getInstance().clearGif(activityLocalName)

优化点3 —— 使用 LruCache 缓存 emoji 资源

根据 LRU 规则将表情 Gif 缓存,避免重复加载创建新对象。

最后

因为离职从南京回到成都还有工作的各种各样的原因,也是有四个多月没更博客了。这是重新开始写博客的第一篇,之后大概会以一个月 2-3 篇的样子更新,记录与分享,欢迎大家关注我的简书。

本库地址 PandaEmoView 欢迎 star 和提 issue

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,442评论 25 707
  • 原文链接:https://github.com/opendigg/awesome-github-android-u...
    IM魂影阅读 32,900评论 6 472
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,019评论 4 62
  • 无欲无求 甚至连个想念的人都没有 就像这颗心一样 是个空白 总感觉少了点什么东西 却又无以言状 丶 我也不知道自己...
    灵夕丶阅读 239评论 0 2
  • 回顾会议(retrospective meeting)是scrum中最有价值的会议之一,虽然这个会议很重要,但是在...
    鲁佳阅读 31,820评论 7 47