图片 46

Android高仿微信表情输入与键盘输入详解

最近公司在项目上要使用到表情与键盘的切换输入,自己实现了一个,还是存在些缺陷,比如说键盘与表情切换时出现跳闪问题,这个相当困扰我,不过所幸在Github(其中一个不错的开源项目,其代码整体结构很不错)并且在论坛上找些解决方案,再加上我也是研究了好多个开源项目的代码,最后才苦逼地整合出比较不错的实现效果,可以说跟微信基本一样(嘿嘿,只能说目前还没发现大Bug,若发现大家一起日后慢慢完善,这里我也只是给出了实现方案,拓展其他表情我并没有实现哈,不过代码中我实现了一个可拓展的fragment模板以便大家实现自己的表情包),我只是实现了一页表情,代码我也进行另外的封装与拓展,大家需要多表情的话只需要实现自己的表情fragment界面,然后根据工厂类获取即可,废话不多说先上图看效果:

一、 说明

在之前的学习中我们接触到了一些简单常用的控件和监听器,但这些在实际开发中是远远不够的,接下来一起来认识更高级的控件和更丰富的监听器,以及学会如何使用适配器搭建起数据源和视图界面的桥梁。高级控件篇第的一部分将围绕适配器讲解它是如何在某些高级控件发挥重要作用。本篇控件清单:

  • ListView 列表
  • Spinner 下拉列表
  • GridView 网格视图
  • ViewPager 视图滑动切换工具

图片 1

说到聊天,就离不开文字、表情和图片,表情和图片增加了聊天的趣味性,让原本无聊的文字瞬间用表情动了起来,今天给大家带来的是表情键盘,教你一步一步实现,先来看下效果图. 

那什么是数据适配器Adapter呢?如开头所说,它的作用是把复杂的数据填充在指定视图界面上。常用两种Adapter:ArrayAdapter(用于绑定单一的数据,数据源是数组或集合),SimpleAdapter(用于绑定格式复杂的数据,数据源是特定的泛型集合)。接下来将以ListView为例,看看这两种适配器的使用方法和效果。

1.ListView 列表

ListView是最为常用的控件之一,它以列表的形式展示具体内容,并且能够根据数据的长度自适应显示。下图是一个每项数据只有一行文本的ListView效果图,对于这类单一数据,用ArrayAdapter加载数据再合适不过,接下来一起学习一下。

图片 2

布局界面只需要一个ListView,设置好宽高和id就够了。另外,还常用属性android:divider设置列表分割线的颜色,如透明色#00000000.

图片 3

在MainActivity用id找到布局中的ListView之后,就是加载适配器的过程了:

图片 4

可以看到使用过程无非三个步骤:数据源准备->适配器加载数据源->控件加载适配器,在关键的第二步对ArrayAdapter初始化中,提供的三个参数完成了在哪里显示、每一项数据如何显示(这里直接使用安卓提供好一个布局)、显示哪些数据及有多少项这些任务,再set到ListView上,就实现了一开始看到的界面效果。所以ListView只负责加载和管理视图,其他显示内容都是交给Adapter去做的。

当然ListView的每一项Item都是可以被监听的,监听器是OnItemClickListener,其中返回的参数position表示被点击的某项在整个List中的位置,从0起算,这样就能用ListView的getItemAtPosition()方法获取到被点击项的内容:

图片 5

当点击第一项“wifi”时效果如下:

图片 6

接下来再看一个页面效果:

图片 7

在这个ListView能看到每个Item不再是简单的一行,有文字也有图片,这种格式复杂的数据就要用到SimpleAdapter了,还是在main.xml里准备好ListView控件,再回到MainActivity来学习如何用之前学会的三步骤来加载SimpleAdapter吧!

第一步准备数据源,可以看到数据源dataList是一个特定的泛型集合,这里String代表文字,Object代表图片,然后调用getData()初始化dataList。

图片 8

每一个Map对应一项Item,为了方便用for循环让每个Item里图标都一样,文字内容递增就可以,然后添加到dataList,这样就完成一个有20项Item的List。这里注意Map键值对里的键名,后面会需要。

图片 9

第二步适配器加载数据源,在此之前,需要给列表每一项做个布局item.xml,这个不难理解,因为在ArrayAdpter例子里我们直接使用系统提供的布局而已。注意要给出TextView和ImageView的id,马上就会用到。

图片 10

现在又到了关键一步,SimpleAdapter初始化比较复杂,需要用到五个参数,前三个容易理解,后两个就是之前需要留心的两个要点。这一步实现了控件与数据的一一绑定。最后一步加载适配器就大功告成了!

图片 11

现在再介绍ListView上常用的监听器OnScrollListener,用于监听滚动变化,当用户拉到列表最底下的时候可帮助视图在滚动中加载数据。现在为列表设置监听器listView.setOnScrollListener(this),并实现onScrollStateChanged
()、onScroll()方法。

这里重写第一个方法,能看到事件会返回一个scrollState,它有三个状态值,下图打印出详细描述。因为需要在视图一直滑动到底端给出新的Item,为dataList增添新的map之后,要用到adpter非常关键的方法notifyDataSetChanged()通知适配器数据发生了变化要重新加载数据,这再次印证之前所说数据的显示是适配器的工作而不是列表。

图片 12

效果如下,可以看到当用户看完20项继续向下拖时就会有源源不断的新内容更新上来。

图片 13

学完这两个常用适配器使用和适用情况之后,对比可看出ArrayAdapter使用起来明显简单许多,思考一个问题,ArrayAdapter的第二个参数如果不用系统提供的列表项布局而是自定义布局,是否也能做到图文并存的效果呢?答案是肯定的,只不过需要自定义一个适配器继承ArrayAdapter并重写一些方法了。下面就来学习如何定制一个ListView界面吧!

这次做一个更好看的界面,准备好小动物的图片就可以开始大展身手了!

图片 14

回忆一下实例化一个ArrayAdapter时需要的三个参数,其中列表项布局以及适配器的适配类型都是要重新考虑的。那么先就从这开始准备吧!

每个Item都是由左边一张图片和右边一行文本组成的,下面代码中需要解释的是使用tools:的属性在我们预览能看到效果但不会出现在运行后的布局,方便我们提前看效果又不至于影响后续工作。

图片 15

接着需要准备一个实体类Animal作为适配器的适配类型,这个类里提供动物图片和名称两个属性、用来初始化属性的构造方法以及对应的get方法即可。

图片 16

然后到了关键一步,创建一个自定义的适配器且继承ArrayAdapter,重写父类一组含三个参数的构造函数,并将列表项子布局的id保存下来。接着重写getView()方法,先用getItem(position)得到当前Item项的Animal实例,再用LayoutInflater系列方法把子布局传入当前布局得到一个View,接着调用这个View的findViewById()找到ImageView和TextView实例,这样就可以把从当前项对象get的内容设置到这两个控件里去显示图片和文字了。

图片 17

一切准备就绪之后,后面的步骤基本信手拈来了,相信下面这段代码你一定没问题了。

图片 18

图片 19

点击某个Item也会有响应:

图片 20

这里对getView()多提几句,如果我们只是用上面几行代码来运行ListView的话效率会非常低,因为每次为了要显示每个子项去调用getView()方法后都会将布局重新加载一遍,如果能将显示过的Item
View缓存起来,以后出现直接复用就能达到提升ListView运行效率的效果了。优化后代码如下:

图片 21

以下是源代码:

 public View getView(int position, View convertView, ViewGroup parent) {
        Animal animal=getItem(position);
        View view;
        ViewHolder viewHolder;
        if(convertView==null){
            view=LayoutInflater.from(getContext()).inflate(resourceId, null);
            viewHolder=new ViewHolder();
            viewHolder.imageView= (ImageView) view.findViewById(R.id.animal_image);
            viewHolder.textView= (TextView) view.findViewById(R.id.animal_name);
            view.setTag(viewHolder);
        }else{
            view=convertView;
            viewHolder = (ViewHolder) view.getTag();
        }

        viewHolder.textView.setText(animal.getAnimalName());
        viewHolder.imageView.setImageResource(animal.getImageId());
        return view;
    }

    class ViewHolder{

        ImageView imageView;
        TextView textView;
    }
}

到此学了这么多,相信你对适配器可以熟练使用了吧!只要三步就搞定。想必在其他控件上应用适配器也很容易了,下面快来再认识两个高级控件。

2.Spinner 下拉列表

与ListView类似的,每个下拉列表项对应一个Item,列表项内容一般是文字,用ArrayAdapter就能做到,触类旁通,相信做一个下图所示的下拉列表已经难不倒你了!

图片 22

图片 23

图片 24

选择系统提供的一个布局作为Spinnner的菜单样式,注意是设置在适配器上,这里给Spinner安装监听器是OnItemSelectListener,用适配器和列表都可以定位到某Item,完成后效果如下:

图片 25

3.GridView 网格视图

从名字中能看出来GridView的特点,它使得每个Item以网格的形式展现,除此之外使用方式和ListView非常相似。下面准备用SimpleAdapter做一个这样的Demo:

图片 26

GirdView本身还有些常用的属性:
android:verticalSpacing(两列之间的间距),android:horizontalSpacing(两行之间的间距),
android:numColumns(每行显示多少列,选值为auto_fit表示自动适应展示几列)。

图片 27

接下来就是GridView绑定SimpleAdapter的过程了,不再细说,需要强调这里把图标和文字分别放在两个数组中且一一对应以便能通过循环得到数据源dataList。监听器是OnItemClickListener

图片 28

图片 29

最后为了界面美观,在注册该活动时候设置theme是Black且NoTitleBar,注意被设置成android:theme=”@android:style/Theme.Black.NoTitleBar.Fullscreen”的活动一定继承的是android.app包下Activity,如果是V7兼容包下的AppCompatActivity会导致程序崩溃无法打开。点击运行来看看是不是达到上面的效果了呢?

图片 30

其实除了这两个常用Adatper,还有一些Adapter也实用,下面通过ViewPager控件再来认识一个Adapter。

4.ViewPager 视图滑动切换工具

ViewPager是android扩展包v4包中的类,这个类可以让用户左右切换当前的视图(View、Fragment都可以),很多APP都用到这个功能,可见其重要程度,因此想用这点篇幅详解ViewPager是完全不够的,这里就仅仅给大家介绍用来帮助ViewPager管理View数据源的适配器PagerAdapter,感受一下风格各样的适配器。

首先在布局里导入v4包两个控件,其中PagerTabStrip是ViewPager子标签,包含在ViewPager里,这里用它作标题。

图片 31

由于PagerAdapter是抽象类,使用时需要自定义子类。初始化时让这个适配器获取到两个数据源List:页卡List和标题List,之后重写几个方法更好的完善这个适配器的功能。

图片 32

图片 33

接着三步骤,在主活动准备好两个List,这里用View.inflate
()方法将布局转化成View对象,数据加载到自定义适配器上,adapter加载到ViewPager即可,又给ViewPager设置监听器OnPageChangeListener监听页卡是否发生变化。另外,我们还获取到控件PagerTabStrip去给标题做些美化工作。

图片 34

图片 35

图片 36

图片 37

最后效果如图,手指左右滑动就可以实现页面切换了。

图片 38

其实所有这些Adapter都是从父类BaseAdapter扩展而来的,也就是说我们也可以根据自己的需要自定义一个Adapter继承BaseAdapter,然后具体实现下面4个方法:

图片 39

由于adapter中含有要显示的数据集合,数据集合中元素个数即可被展示的View个数,每个数据的获取、每个Item
View的样式都由adapter控制,每个position位置上数据都绑定到Item
View上,这样数据和视图也就结合在一起了。由于篇幅原因不在这里接着具体展开,后续再深入探究。

本篇先到这里,下一篇还有更多有趣的高级控件等我们学习~

图片 40

图片 41

效果还不错吧,哈哈。下面开始介绍:

二、功能

本篇主要分析的核心类EmotionKeyboard.Java,EmotionComplateFragment.java,EmotionMainFragment.java,FragmentFactory.java,还有一个是工具类里的EmotionUtils.java和GlobalOnItemClickManagerUtils.java
这几个类我会重点分析一下,其他的大家自行看源码哈。下面就开始咯,先来看看本篇主要内容以及大概思路:

1、如何控制表情键盘与输入法的切换 2、如何解析表情 3、如何处理表情与非表情的删除

图片 42

三、实现

1.解决表情与键盘切换跳闪问题

明确了各个要解决的问题,下面我们逐个来实现

1.1跳闪问题概述

为了让大家对这个问题有一定了解,我先来个简单案例,用红色面板代表表情面板,效果如下:

图片 43
图(1-1)

我们先来看图(1-1),即上图,通过上图我们可以看出,当表情显示时,我们点击表情按钮,隐藏表情显示软件盘时,内容Bar有一个明显的先向下后恢复的跳闪现象,这样用户体验相当的差,我们希望的是下图(1-2)的效果,无论怎么切换都不会有跳闪现象,这就是我所有说的键盘与表情切换的跳闪问题。

图片 44
图(1-2)

到这里,我们对这个问题有了大概了解后,再来深入分析如何实现图(1-2)的不跳闪效果。这里我们做个约定,我们把含有表情那个bar统称为内容Bar。

表情键盘与输入法切换 博主查了一下相关资料,有如下方案

1.2 解决跳闪问题的思路:

Android系统在弹出软键盘时,会把我们的内容 Bar
顶上去,因此只有表情面板的高度与软键盘弹出时高度一致时,才有可能然切换时高度过渡更自然,所以我们必须计算出软键盘的高度并设置给表情面板。仅仅有这一步跳闪问题还是依旧存在,因此这时我们必须想其他办法固定内容Bar,因为所有的跳闪都是表情面板隐藏,而软键盘往上托出瞬间,Activity高度变高(为什么会变高后面会说明),内容Bar往下滑后,又被软键盘顶回原来位置造成的。因此只要固定了内容Bar的位置,闪跳问题就迎刃而解了。那么如何固定内容Bar的位置呢?我们知道在一个布局中一个控件的位置其实是由它上面所有控件的高度决定的,如果其上面其他控件的高度不变,那么当前控件的高度自然也不会变化,即使到时Activity的高度发生了变化也也不会影响该控件的位置(整个界面的显示是挂载在window窗体上的,而非Activity,不了解的可以先研究一下窗体的创建过程),因此我们只要在软键盘弹出前固定内容Bar上面所有控件高度,从而达到固定内容Bar位置(高度)的目的。好了,有思路了,我们接下来一步步按上面思路解决问题。

方案一:动态改变SoftInputMode 软键盘显示时将SoftInputMode设置为「stateVisible|adjustResize」,表情键盘显示时调整为「adjustPan」

1.3 解决跳闪问题的套路:

1.3.1 先获取键盘高度,并设置表情面板的高度为软键盘的高度

Android系统在界面上弹出软键盘时会将整个Activity的高度压缩,此时windowSoftInputMode属性设置为adjustResize(对windowSoftInputMode不清楚的话,请自行查阅相关资料哈),这个属性表示Activity的主窗口总是会被调整大小,从而保证软键盘显示空间。在这种情况下我们可以通过以下方法计算软键盘的高度:

Rect r = new Rect();
/*
* decorView是window中的最顶层view,可以从window中通过getDecorView获取到decorView。
* 通过decorView获取到程序显示的区域,包括标题栏,但不包括状态栏。
*/
mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r);
//获取屏幕的高度
int screenHeight = mActivity.getWindow().getDecorView().getRootView().getHeight();
//计算软件盘的高度
int softInputHeight = screenHeight - r.bottom;

这里我们队对r.bottom和mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r)进行简单解释,直接上图吧:

图片 45

这下就清晰了吧,右边是Rect参数解析图,辅助我们对rect的理解。

Rect r = new Rect();
mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r)

这两句其实将左图中蓝色边框(
其实也就是actvity的大小)的size大小参数封装到Rect中,以便我们后续使用。虽然计算出来的区域大小不包含状态栏,但是r.bottom(红色箭头长度)的大小是从屏幕顶部开始计算的所以包含了状态栏的高度。需要注意的是,区域大小是这样计算出来的:
区域的高:r.bottom-r.top 区域的宽:r.right-r.left
当然这个跟计算软键盘高度没关系,只是顺带提一下。因此我们可以通过即可获取到软以下方式获取键盘高度:
键盘高度=屏幕高度-r.bottom

1.3.2 固定内容Bar的高度,解决闪跳问题

软键盘高度解决后,现在剩下的问题关键就在于控制内容Bar的高度了,那么如何做呢?我们先来看一个布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ListView
        android:id="@+id/listview"
        android:layout_weight="1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        />
    <FrameLayout
        android:id="@+id/fl_emotionview_main"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

其中ListView的layout_height为0dp、layout_weight为1,这样这个ListView就会自动充满整个布局,这里ListView可以替换成任意控件,FrameLayout则为表情布局(也可认为就是我们前面所说的内容Bar,只不过这里最终会被替换成整个表情布局),我们的目的就是在弹出软键盘时固定FrameLayout的高度,以便去除跳闪问题。根据我们前面的思路,FrameLayout的高度是由其上面的控件决定的也就是由ListView决定的,也就是说我们只要在软键盘弹出前固定ListView的内容高度即可。因此我们可以通过下面的方法来锁定ListView的高度,(mContentView就是我们所指的ListView,这些方法都封装在EmotionKeyboard.java类中)

/**
  * 锁定内容高度,防止跳闪
  */
 private void lockContentHeight(){
    LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mContentView.getLayoutParams();
    params.height = mContentView.getHeight();
    params.weight = 0.0F;
  }

将weight置0,然后将height设置为当前的height,在父控件(LinearLayout)的高度变化时它的高度也不再会变化。释放ListView的高度:

private void unlockContentHeightDelayed() {
    mEditText.postDelayed(new Runnable() {
        @Override
        public void run() {
            ((LinearLayout.LayoutParams) mContentView.getLayoutParams()).weight = 1.0F;
        }
    }, 200L);
}

其中的LinearLayout.LayoutParams.weight =
1.0F;,在代码里动态更改LayoutParam的weight,会导致父控件重新onLayout(),也就达到改变控件的高度的目的。到此两个主要问题都解决了,我们直接上核心类代码,该类来自github上的开源项目我在使用中直接从该项目中抽取了该类,
并做了细微修改,也添加了代码注释。

package com.zejian.emotionkeyboard.emotionkeyboardview;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.os.Build;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import com.zejian.emotionkeyboard.utils.LogUtils;
/**
 * author : zejian
 * time : 2016年1月5日 上午11:14:27
 * email : shinezejian@163.com
 * description :源码来自开源项目https://github.com/dss886/Android-EmotionInputDetector
 *              本人仅做细微修改以及代码解析
 */
public class EmotionKeyboard {

        private static final String SHARE_PREFERENCE_NAME = "EmotionKeyboard";
        private static final String SHARE_PREFERENCE_SOFT_INPUT_HEIGHT = "soft_input_height";
        private Activity mActivity;
        private InputMethodManager mInputManager;//软键盘管理类
        private SharedPreferences sp;
        private View mEmotionLayout;//表情布局
        private EditText mEditText;//
        private View mContentView;//内容布局view,即除了表情布局或者软键盘布局以外的布局,用于固定bar的高度,防止跳闪
        private EmotionKeyboard(){
        }

        /**
         * 外部静态调用
         * @param activity
         * @return
         */
        public static EmotionKeyboard with(Activity activity) {
            EmotionKeyboard emotionInputDetector = new EmotionKeyboard();
            emotionInputDetector.mActivity = activity;
            emotionInputDetector.mInputManager = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
            emotionInputDetector.sp = activity.getSharedPreferences(SHARE_PREFERENCE_NAME, Context.MODE_PRIVATE);
            return emotionInputDetector;
        }

        /**
         * 绑定内容view,此view用于固定bar的高度,防止跳闪
         * @param contentView
         * @return
         */
        public EmotionKeyboard bindToContent(View contentView) {
            mContentView = contentView;
            return this;
        }

        /**
         * 绑定编辑框
         * @param editText
         * @return
         */
        public EmotionKeyboard bindToEditText(EditText editText) {
            mEditText = editText;
            mEditText.requestFocus();
            mEditText.setOnTouchListener(new View.OnTouchListener() {
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    if (event.getAction() == MotionEvent.ACTION_UP && mEmotionLayout.isShown()) {
                        lockContentHeight();//显示软件盘时,锁定内容高度,防止跳闪。
                        hideEmotionLayout(true);//隐藏表情布局,显示软件盘
                        //软件盘显示后,释放内容高度
                        mEditText.postDelayed(new Runnable() {
                            @Override
                            public void run() {
                                unlockContentHeightDelayed();
                            }
                        }, 200L);
                    }
                    return false;
                }
            });
            return this;
        }

        /**
         * 绑定表情按钮
         * @param emotionButton
         * @return
         */
        public EmotionKeyboard bindToEmotionButton(View emotionButton) {
            emotionButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    if (mEmotionLayout.isShown()) {
                        lockContentHeight();//显示软件盘时,锁定内容高度,防止跳闪。
                        hideEmotionLayout(true);//隐藏表情布局,显示软件盘
                        unlockContentHeightDelayed();//软件盘显示后,释放内容高度
                    } else {
                        if (isSoftInputShown()) {//同上
                            lockContentHeight();
                            showEmotionLayout();
                            unlockContentHeightDelayed();
                        } else {
                            showEmotionLayout();//两者都没显示,直接显示表情布局
                        }
                    }
                }
            });
            return this;
        }
        /**
         * 设置表情内容布局
         * @param emotionView
         * @return
         */
        public EmotionKeyboard setEmotionView(View emotionView) {
            mEmotionLayout = emotionView;
            return this;
        }
        public EmotionKeyboard build(){
//设置软件盘的模式:SOFT_INPUT_ADJUST_RESIZE  这个属性表示Activity的主窗口总是会被调整大小,从而保证软键盘显示空间。
       //从而方便我们计算软件盘的高度
       mActivity.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN |
                    WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
            //隐藏软件盘
            hideSoftInput();
            return this;
        }
        /**
         * 点击返回键时先隐藏表情布局
         * @return
         */
        public boolean interceptBackPress() {
            if (mEmotionLayout.isShown()) {
                hideEmotionLayout(false);
                return true;
            }
            return false;
        }
        private void showEmotionLayout() {
            int softInputHeight = getSupportSoftInputHeight();
            if (softInputHeight == 0) {
                softInputHeight = sp.getInt(SHARE_PREFERENCE_SOFT_INPUT_HEIGHT, 400);
            }
            hideSoftInput();
            mEmotionLayout.getLayoutParams().height = softInputHeight;
            mEmotionLayout.setVisibility(View.VISIBLE);
        }
        /**
         * 隐藏表情布局
         * @param showSoftInput 是否显示软件盘
         */
        private void hideEmotionLayout(boolean showSoftInput) {
            if (mEmotionLayout.isShown()) {
                mEmotionLayout.setVisibility(View.GONE);
                if (showSoftInput) {
                    showSoftInput();
                }
            }
        }
        /**
         * 锁定内容高度,防止跳闪
         */
        private void lockContentHeight() {
            LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mContentView.getLayoutParams();
            params.height = mContentView.getHeight();
            params.weight = 0.0F;
        }
        /**
         * 释放被锁定的内容高度
         */
        private void unlockContentHeightDelayed() {
            mEditText.postDelayed(new Runnable() {
                @Override
                public void run() {
                    ((LinearLayout.LayoutParams) mContentView.getLayoutParams()).weight = 1.0F;
                }
            }, 200L);
        }
        /**
         * 编辑框获取焦点,并显示软件盘
         */
        private void showSoftInput() {
            mEditText.requestFocus();
            mEditText.post(new Runnable() {
                @Override
                public void run() {
                    mInputManager.showSoftInput(mEditText, 0);
                }
            });
        }
        /**
         * 隐藏软件盘
         */
        private void hideSoftInput() {
            mInputManager.hideSoftInputFromWindow(mEditText.getWindowToken(), 0);
        }
        /**
         * 是否显示软件盘
         * @return
         */
        private boolean isSoftInputShown() {
            return getSupportSoftInputHeight() != 0;
        }
        /**
         * 获取软件盘的高度
         * @return
         */
        private int getSupportSoftInputHeight() {
            Rect r = new Rect();
            /**
             * decorView是window中的最顶层view,可以从window中通过getDecorView获取到decorView。
             * 通过decorView获取到程序显示的区域,包括标题栏,但不包括状态栏。
             */
            mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r);
            //获取屏幕的高度
            int screenHeight = mActivity.getWindow().getDecorView().getRootView().getHeight();
            //计算软件盘的高度
            int softInputHeight = screenHeight - r.bottom;
            /**
             * 某些Android版本下,没有显示软键盘时减出来的高度总是144,而不是零,
             * 这是因为高度是包括了虚拟按键栏的(例如华为系列),所以在API Level高于20时,
             * 我们需要减去底部虚拟按键栏的高度(如果有的话)
             */
            if (Build.VERSION.SDK_INT >= 20) {
                // When SDK Level >= 20 (Android L), the softInputHeight will contain the height of softButtonsBar (if has)
                softInputHeight = softInputHeight - getSoftButtonsBarHeight();
            }
            if (softInputHeight < 0) {
                LogUtils.w("EmotionKeyboard--Warning: value of softInputHeight is below zero!");
            }
            //存一份到本地
            if (softInputHeight > 0) {
                sp.edit().putInt(SHARE_PREFERENCE_SOFT_INPUT_HEIGHT, softInputHeight).apply();
            }
            return softInputHeight;
        }
        /**
         * 底部虚拟按键栏的高度
         * @return
         */
        @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
        private int getSoftButtonsBarHeight() {
            DisplayMetrics metrics = new DisplayMetrics();
            //这个方法获取可能不是真实屏幕的高度
            mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
            int usableHeight = metrics.heightPixels;
            //获取当前屏幕的真实高度
            mActivity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);
            int realHeight = metrics.heightPixels;
            if (realHeight > usableHeight) {
                return realHeight - usableHeight;
            } else {
                return 0;
            }
        }
    /**
     * 获取软键盘高度
     * @return
     */
    public int getKeyBoardHeight(){
        return sp.getInt(SHARE_PREFERENCE_SOFT_INPUT_HEIGHT, 400);
    }
}

EmotionKeyboard类使用的是设计模式中的builder模式来创建对象。其中mEmotionLayout是表情布局,mContentView是内容布局view,即除了表情布局或者软键盘布局以外的布局,用于固定bar的高度,防止跳闪,当然mContentView可以是任意布局。

/**
    * 绑定表情按钮
    * @param emotionButton
    * @return
    */
    public EmotionKeyboard bindToEmotionButton(View emotionButton) {
        emotionButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mEmotionLayout.isShown()) {
                    lockContentHeight();//显示软件盘时,锁定内容高度,防止跳闪。
                    hideEmotionLayout(true);//隐藏表情布局,显示软件盘
                    unlockContentHeightDelayed();//软件盘显示后,释放内容高度
                } else {
                    if (isSoftInputShown()) {//同上
                        lockContentHeight();
                        showEmotionLayout();
                        unlockContentHeightDelayed();
                    } else {
                        showEmotionLayout();//两者都没显示,直接显示表情布局
                    }
                }
            }
        });
        return this;
    }

这里我们主要重点说明一下点击表情按钮时,显示或者隐藏表情布局以及软键盘的逻辑。首先我们通过mEmotionLayout.isShown()去判断表情是否已经显示,如果返回true,这时肯定要去切换成软键盘,因此必须先通过lockContentHeight()方法锁定mContentView内容高度,然后通过hideEmotionLayout(true)方法因此表情布局并显示软键盘,这里传入true表示显示软键盘,如果传入false则表示不显示软键盘,软键盘显示后通过unlockContentHeightDelayed()方法去解锁mContentView内容高度。但如果mEmotionLayout.isShown()返回了false,这有两种情况,第1种是如果此时软键盘已经显示,则需先锁定mContentView内容高度,再去隐藏软键盘,然后显示表情布局,最后再解锁mContentView内容高度。第2种情况是软键盘和表情都没显示,这下就简单了,直接显示表情布局即可。好,这个类解析到这,其他直接看源码哈,注释杠杠的哈。最后我们来看看在外部使用该类的例子代码如下:

mEmotionKeyboard = EmotionKeyboard.with(getActivity())
                .setEmotionView(rootView.findViewById(R.id.ll_emotion_layout))//绑定表情面板
                .bindToContent(contentView)//绑定内容view
                .bindToEditText(!isBindToBarEditText ? ((EditText) contentView) : ((EditText) rootView.findViewById(R.id.bar_edit_text)))//判断绑定那种EditView
                .bindToEmotionButton(rootView.findViewById(R.id.emotion_button))//绑定表情按钮
                .build();

方案二:Dialog 直接在软键盘上显示一个Dialog,可避开大部分切换逻辑,但是在打开当前页面后存在软键盘和Dialog冲突问题

2.实现表情表情面板切换的思路

这里我们主要采用NoHorizontalScrollerViewPager+RecyclerView+Fragment实现,思路是这样的,我们以NoHorizontalScrollerViewPager作为载体,fragment作为展示界面,RecyclerView作为底部滚动条,每当点击RecyclerView的item时,我们使用viewPager.setCurrentItem(position,false)方法来切换fragment界面即可(这里传入false是表示不需要viewPager的切换动画)。这样我们就可以实现不同类表情的切换了。(提示一下这里所指的fragment其实是就工程目录中的EmotiomComplateFragment.java类)这个比较简单,就不多啰嗦了。实现代码稍后会一起提供。下面是不可横向滑动的ViewPager的实现代码,非常简单,不拦截子类事件即可。

package com.zejian.emotionkeyboard.emotionkeyboardview;
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
/**
 * Created by zejian
 * Time  16/1/7 上午11:12
 * Email shinezejian@163.com
 * Description:不可横向滑动的ViewPager
 */
public class NoHorizontalScrollerViewPager extends ViewPager{
    public NoHorizontalScrollerViewPager(Context context) {
        super(context);
    }
    public NoHorizontalScrollerViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    /**
     * 重写拦截事件,返回值设置为false,这时便不会横向滑动了。
     * @param ev
     * @return
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
    /**
     * 重写拦截事件,返回值设置为false,这时便不会横向滑动了。
     * @param ev
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        return false;
    }
}

博主在观察QQ、微信、微博、陌陌后发现,他们的表情键盘和软键盘切换,并不会导致聊天内容(ListView、RecyclerView)的跳动,基本就可以推测SoftInputMode就是adjustsPan(SoftInputMode含义) 明确了adjustPan那就好办了,既然聊天内容(ListView、RecyclerView)不会跳动,那么在软键盘切换至表情键盘的时候,底部肯定有一个和软键盘高度一致的View,只需在点击表情的时候将软键盘隐藏,显示表情键盘,在点击EditText的时候显示软键盘,隐藏表情键盘。

3.单个表情面板的实现思路

来梳理一下知识点:

3.1 表情图片的本质与显示

表情的显示从直观上看确实是一个图片,但实际只是一种特殊的文本(ImageSpan),比如微博里表情就是”[表情名字]”的接口,可爱的表情就是[可爱]…因此这里我们也打算利用”[表情名字]”作为key,图片的R值作为内容进行存取,EmotionUtils类如下

package com.zejian.emotionkeyboard.utils;
import android.support.v4.util.ArrayMap;
import com.zejian.emotionkeyboard.R;
/**
 * @author : zejian
 * @time : 2016年1月5日 上午11:32:33
 * @email : shinezejian@163.com
 * @description :表情加载类,可自己添加多种表情,分别建立不同的map存放和不同的标志符即可
 */
public class EmotionUtils {
    /**
     * 表情类型标志符
     */
    public static final int EMOTION_CLASSIC_TYPE=0x0001;//经典表情
    /**
     * key-表情文字;
     * value-表情图片资源
     */
    public static ArrayMap<String, Integer> EMPTY_MAP;
    public static ArrayMap<String, Integer> EMOTION_CLASSIC_MAP;
    static {
        EMPTY_MAP = new ArrayMap<>();
        EMOTION_CLASSIC_MAP = new ArrayMap<>();
        EMOTION_CLASSIC_MAP.put("[呵呵]", R.drawable.d_hehe);
        EMOTION_CLASSIC_MAP.put("[嘻嘻]", R.drawable.d_xixi);
        EMOTION_CLASSIC_MAP.put("[哈哈]", R.drawable.d_haha);
        EMOTION_CLASSIC_MAP.put("[爱你]", R.drawable.d_aini);
        EMOTION_CLASSIC_MAP.put("[挖鼻屎]", R.drawable.d_wabishi);
        EMOTION_CLASSIC_MAP.put("[吃惊]", R.drawable.d_chijing);
        EMOTION_CLASSIC_MAP.put("[晕]", R.drawable.d_yun);
        EMOTION_CLASSIC_MAP.put("[泪]", R.drawable.d_lei);
        EMOTION_CLASSIC_MAP.put("[馋嘴]", R.drawable.d_chanzui);
        EMOTION_CLASSIC_MAP.put("[抓狂]", R.drawable.d_zhuakuang);
        EMOTION_CLASSIC_MAP.put("[哼]", R.drawable.d_heng);
        EMOTION_CLASSIC_MAP.put("[可爱]", R.drawable.d_keai);
        EMOTION_CLASSIC_MAP.put("[怒]", R.drawable.d_nu);
        EMOTION_CLASSIC_MAP.put("[汗]", R.drawable.d_han);
        EMOTION_CLASSIC_MAP.put("[害羞]", R.drawable.d_haixiu);
        EMOTION_CLASSIC_MAP.put("[睡觉]", R.drawable.d_shuijiao);
        EMOTION_CLASSIC_MAP.put("[钱]", R.drawable.d_qian);
        EMOTION_CLASSIC_MAP.put("[偷笑]", R.drawable.d_touxiao);
        EMOTION_CLASSIC_MAP.put("[笑cry]", R.drawable.d_xiaoku);
        EMOTION_CLASSIC_MAP.put("[doge]", R.drawable.d_doge);
        EMOTION_CLASSIC_MAP.put("[喵喵]", R.drawable.d_miao);
        EMOTION_CLASSIC_MAP.put("[酷]", R.drawable.d_ku);
        EMOTION_CLASSIC_MAP.put("[衰]", R.drawable.d_shuai);
        EMOTION_CLASSIC_MAP.put("[闭嘴]", R.drawable.d_bizui);
        EMOTION_CLASSIC_MAP.put("[鄙视]", R.drawable.d_bishi);
        EMOTION_CLASSIC_MAP.put("[花心]", R.drawable.d_huaxin);
        EMOTION_CLASSIC_MAP.put("[鼓掌]", R.drawable.d_guzhang);
        EMOTION_CLASSIC_MAP.put("[悲伤]", R.drawable.d_beishang);
        EMOTION_CLASSIC_MAP.put("[思考]", R.drawable.d_sikao);
        EMOTION_CLASSIC_MAP.put("[生病]", R.drawable.d_shengbing);
        EMOTION_CLASSIC_MAP.put("[亲亲]", R.drawable.d_qinqin);
        EMOTION_CLASSIC_MAP.put("[怒骂]", R.drawable.d_numa);
        EMOTION_CLASSIC_MAP.put("[太开心]", R.drawable.d_taikaixin);
        EMOTION_CLASSIC_MAP.put("[懒得理你]", R.drawable.d_landelini);
        EMOTION_CLASSIC_MAP.put("[右哼哼]", R.drawable.d_youhengheng);
        EMOTION_CLASSIC_MAP.put("[左哼哼]", R.drawable.d_zuohengheng);
        EMOTION_CLASSIC_MAP.put("[嘘]", R.drawable.d_xu);
        EMOTION_CLASSIC_MAP.put("[委屈]", R.drawable.d_weiqu);
        EMOTION_CLASSIC_MAP.put("[吐]", R.drawable.d_tu);
        EMOTION_CLASSIC_MAP.put("[可怜]", R.drawable.d_kelian);
        EMOTION_CLASSIC_MAP.put("[打哈气]", R.drawable.d_dahaqi);
        EMOTION_CLASSIC_MAP.put("[挤眼]", R.drawable.d_jiyan);
        EMOTION_CLASSIC_MAP.put("[失望]", R.drawable.d_shiwang);
        EMOTION_CLASSIC_MAP.put("[顶]", R.drawable.d_ding);
        EMOTION_CLASSIC_MAP.put("[疑问]", R.drawable.d_yiwen);
        EMOTION_CLASSIC_MAP.put("[困]", R.drawable.d_kun);
        EMOTION_CLASSIC_MAP.put("[感冒]", R.drawable.d_ganmao);
        EMOTION_CLASSIC_MAP.put("[拜拜]", R.drawable.d_baibai);
        EMOTION_CLASSIC_MAP.put("[黑线]", R.drawable.d_heixian);
        EMOTION_CLASSIC_MAP.put("[阴险]", R.drawable.d_yinxian);
        EMOTION_CLASSIC_MAP.put("[打脸]", R.drawable.d_dalian);
        EMOTION_CLASSIC_MAP.put("[傻眼]", R.drawable.d_shayan);
        EMOTION_CLASSIC_MAP.put("[猪头]", R.drawable.d_zhutou);
        EMOTION_CLASSIC_MAP.put("[熊猫]", R.drawable.d_xiongmao);
        EMOTION_CLASSIC_MAP.put("[兔子]", R.drawable.d_tuzi);
    }
    /**
     * 根据名称获取当前表情图标R值
     * @param EmotionType 表情类型标志符
     * @param imgName 名称
     * @return
     */
    public static int getImgByName(int EmotionType,String imgName) {
        Integer integer=null;
        switch (EmotionType){
            case EMOTION_CLASSIC_TYPE:
                integer = EMOTION_CLASSIC_MAP.get(imgName);
                break;
            default:
                LogUtils.e("the emojiMap is null!!");
                break;
        }
        return integer == null ? -1 : integer;
    }
    /**
     * 根据类型获取表情数据
     * @param EmotionType
     * @return
     */
    public static ArrayMap<String, Integer> getEmojiMap(int EmotionType){
        ArrayMap EmojiMap=null;
        switch (EmotionType){
            case EMOTION_CLASSIC_TYPE:
                EmojiMap=EMOTION_CLASSIC_MAP;
                break;
            default:
                EmojiMap=EMPTY_MAP;
                break;
        }
        return EmojiMap;
    }
}

ArrayMap

/**
* 获取fragment的方法
* @param emotionType 表情类型,用于判断使用哪个map集合的表情
*/
public Fragment getFragment(int emotionType){
   Bundle bundle = new Bundle();
   bundle.putInt(FragmentFactory.EMOTION_MAP_TYPE,emotionType);
   EmotiomComplateFragment fragment= EmotiomComplateFragment.newInstance(EmotiomComplateFragment.class,bundle);
   return fragment;
}

调用时,如下:

//创建fragment的工厂类
 FragmentFactory factory=FragmentFactory.getSingleFactoryInstance();
 //创建修改实例
 EmotiomComplateFragment f1= (EmotiomComplateFragment) factory.getFragment(EmotionUtils.EMOTION_CLASSIC_TYPE);

这里我们通过工厂类getFragment(int
emotionType)方法的创建出模版表情类EmotiomComplateFragment,为什么说是模版呢,因为只要我们创建时传递集合标志不同,例如经典表情传递的就是EmotionUtils.EMOTION_CLASSIC_TYPE,这时EmotiomComplateFragment类内部就会根据传递的集合类型去EmotionUtils类中获取相对应的集合,这样也就会创建出我们所需要的表情面板。这里小结一下:通过上术分析我们可以知道如果我们要添加自己的其他类型表情,只需以下步骤:

  • 步骤1.在EmotionUtils类创建一个表情集合,并赋予这个集合唯一标志
  • 步骤2.在EmotionUtils类中的两个获取方法中完善相应的代码。
  • 步骤3.在创建新的EmotiomComplateFragment模板类时,传递相应的集合标志符即可创建相应的表情面板。接下来的问题就是表情如何显示呢?其实这里主要用到了SpannableString拓展性字符串相关知识点,SpannableString可以让一段字符串在显示的时候,将其中某小段文字附着上其他内容或替换成其他内容,拓展内容可以是图片或者是文字格式,比如加粗,显示特殊颜色等。

下面我只对本篇需要用到的SpannableString作简要介绍:

ImageSpan,这个是可以将指定的特殊字符替换成我们所需要的图片。也就是我们可以使用”[表情名字]”这个key作为指定的特殊字符,然后在文本中替换成该key所对应的特殊表情即可。

简单实例如下:

SpannableString spannableString = new SpannableString(source);
int size = (int) tv.getTextSize()*13/10;
Bitmap bitmap = BitmapFactory.decodeResource(res, imgRes);
Bitmap scaleBitmap = Bitmap.createScaledBitmap(bitmap, size, size, true);
ImageSpan span = new ImageSpan(context, scaleBitmap);
spannableString.setSpan(span, start, start + key.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

首先将我们要替换的字符串转换成SpannableString再创建一个ImageSpan并把我们的表情图片包含在内,最后利用SpannableString的setSpan方法,将span对象设置在对应位置,这样就完成了特殊字符与文字的转换。参数解析如下,

  • start 是需要附着的内容的开始位置
  • end 是需要附着的内容的开始位置
  • flag 标志位,这里是最常用的EXCLUSIVE_EXCLUSIVE的表示span拓展文本不包含前后(这个参数还有其他类型,这里不过多介绍)

1、如何获取软键盘高度 2、如何手动控制软键盘的显示与隐藏 3、如何避免在别的页面切到当前界面因软键盘的状态变化而冲突

3.2 利用正则表达式找出特殊字符便于转换成表情

这里我们利用正则表达式找出特殊字符,根据我们自己的需求编写特定的正则表达式,如下:
String regex = "\[[u4e00-u9fa5\w]+\]";
其中[]是我们特殊需要的字符,因此必须使用“//”进行转义,u4e00-u9fa5表示中文,w表示下划线的任意单词字符,+
代表一个或者多个。因此这段正则就代表,匹配方括号内有一或多个文字和单词字符的文本。有了正则表达式,剩下就是找匹配的问题了,这里我们可以先用matcher.find()获取到匹配的开始位置,作为setSpan的start值,再使用matcher.group()方法获取到匹配规则的具体表情文字。对于matcher.find()和matcher.group()这里简单介绍一下:

  • matcher.find(),代表部分匹配,从当前位置开始匹配,找到一个匹配的子串,将移动下次匹配的位置。因此我们可以通过这个方法获取到匹配的开始位置,作为setSpan的start值(如果字符串中有多个表情就会执行多次匹配)。
  • matcher.group(),获取匹配到的具体字符。

下面直接上SpanStringUtils.java类对代码:

package com.zejian.emotionkeyboard.utils;
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 android.widget.TextView;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
 * @author : zejian
 * @time : 2016年1月5日 上午11:30:39
 * @email : shinezejian@163.com
 * @description :文本中的emojb字符处理为表情图片
 */
public class SpanStringUtils {

    public static SpannableString getEmotionContent(int emotion_map_type,final Context context, final TextView tv, String source) {
        SpannableString spannableString = new SpannableString(source);
        Resources res = context.getResources();
        String regexEmotion = "\[([u4e00-u9fa5\w])+\]";
        Pattern patternEmotion = Pattern.compile(regexEmotion);
        Matcher matcherEmotion = patternEmotion.matcher(spannableString);
        while (matcherEmotion.find()) {
            // 获取匹配到的具体字符
            String key = matcherEmotion.group();
            // 匹配字符串的开始位置
            int start = matcherEmotion.start();
            // 利用表情名字获取到对应的图片
            Integer imgRes = EmotionUtils.getImgByName(emotion_map_type,key);
            if (imgRes != null) {
                // 压缩表情图片
                int size = (int) tv.getTextSize()*13/10;
                Bitmap bitmap = BitmapFactory.decodeResource(res, imgRes);
                Bitmap scaleBitmap = Bitmap.createScaledBitmap(bitmap, size, size, true);
ImageSpan span = new ImageSpan(context, scaleBitmap);
                spannableString.setSpan(span, start, start + key.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            }
        }
        return spannableString;
    }
}

代码相对比较简单,这里就不啰嗦啦。

获取软键盘高度

3.3 表情面板的实现(ViewPager+GridView)

这里的自然就是使用到ViewPager和GridView相结合实现多界面滑动的效果,参考了微信的实现,每页都是一个GridView显示20个表情,末尾还有一个删除按钮。实现思路入下:

利用ViewPager作为滑动控件,同时结合GridView来布局每个表情,GridView会显示3行7列,共21个Item,即每页都是一个GridView显示20个表情,末尾还有一个删除按钮。为了让Item能大小合适,我们在这里利用动态计算的方式设置宽高,因为屏幕宽度各有不同。每个item宽度的计算方式,由(屏幕的宽度-左右边距大小(如果有的话就减去)-每个item间隙距离)/7,最终便得到item的宽度。至于表情面板的高度=(item宽度*3+间隙*6),即可获取中高度,为什么间隙*6?这里并没有什么计算原理,纯粹是我在调试的过程中试出来的值,这个值相对比较合理,也比较美观,当然大家也可根据自己需要调整。最后就是有多少页的问题了,这里可以通过for循环表情集合的所有元素,把每次循环获取的元素添加到一个集合中,每次判断集合是否满20个元素,每满20个集合就利用该集合去创建一个GridView的表情面板View,同时再新建一个集合存放新获取到的元素,以次循环。最后把所有表情生成的一个个GridView放到一个总view集合中,利用ViewPager显示即可。要注意的是在GridView的适配器和点击事件中,都利用position判断,如果是最后一个就进行特殊的显示(删除按钮)和点击处理。

package com.zejian.emotionkeyboard.fragment;
import android.os.Bundle;
import android.support.v4.view.ViewPager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridView;
import android.widget.LinearLayout;
import com.zejian.emotionkeyboard.R;
import com.zejian.emotionkeyboard.adapter.EmotionGridViewAdapter;
import com.zejian.emotionkeyboard.adapter.EmotionPagerAdapter;
import com.zejian.emotionkeyboard.emotionkeyboardview.EmojiIndicatorView;
import com.zejian.emotionkeyboard.utils.DisplayUtils;
import com.zejian.emotionkeyboard.utils.EmotionUtils;
import com.zejian.emotionkeyboard.utils.GlobalOnItemClickManagerUtils;
import java.util.ArrayList;
import java.util.List;
/**
 * Created by zejian
 * Time  16/1/5 下午4:32
 * Email shinezejian@163.com
 * Description:可替换的模板表情,gridview实现
 */
public class EmotiomComplateFragment extends BaseFragment {
    private EmotionPagerAdapter emotionPagerGvAdapter;
    private ViewPager vp_complate_emotion_layout;
    private EmojiIndicatorView ll_point_group;//表情面板对应的点列表
    private int emotion_map_type;
    /**
     * 创建与Fragment对象关联的View视图时调用
     * @param inflater
     * @param container
     * @param savedInstanceState
     * @return
     */
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View rootView = inflater.inflate(R.layout.fragment_complate_emotion, container, false);
        initView(rootView);
        initListener();
        return rootView;
    }
    /**
     * 初始化view控件
     */
    protected void initView(View rootView){
        vp_complate_emotion_layout = (ViewPager) rootView.findViewById(R.id.vp_complate_emotion_layout);
        ll_point_group= (EmojiIndicatorView) rootView.findViewById(R.id.ll_point_group);
        //获取map的类型
        emotion_map_type=args.getInt(FragmentFactory.EMOTION_MAP_TYPE);
        initEmotion();
    }
    /**
     * 初始化监听器
     */
    protected void initListener(){
        vp_complate_emotion_layout.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
            int oldPagerPos=0;
            @Override
            public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
            }
            @Override
            public void onPageSelected(int position) {
                ll_point_group.playByStartPointToNext(oldPagerPos,position);
                oldPagerPos=position;
            }
            @Override
            public void onPageScrollStateChanged(int state) {
            }
        });
    }
    /**
     * 初始化表情面板
     * 思路:获取表情的总数,按每行存放7个表情,动态计算出每个表情所占的宽度大小(包含间距),
     *      而每个表情的高与宽应该是相等的,这里我们约定只存放3行
     *      每个面板最多存放7*3=21个表情,再减去一个删除键,即每个面板包含20个表情
     *      根据表情总数,循环创建多个容量为20的List,存放表情,对于大小不满20进行特殊
     *      处理即可。
     */
    private void initEmotion() {
        // 获取屏幕宽度
        int screenWidth = DisplayUtils.getScreenWidthPixels(getActivity());
        // item的间距
        int spacing = DisplayUtils.dp2px(getActivity(), 12);
        // 动态计算item的宽度和高度
        int itemWidth = (screenWidth - spacing * 8) / 7;
        //动态计算gridview的总高度
        int gvHeight = itemWidth * 3 + spacing * 6;
        List<GridView> emotionViews = new ArrayList<>();
        List<String> emotionNames = new ArrayList<>();
        // 遍历所有的表情的key
        for (String emojiName : EmotionUtils.getEmojiMap(emotion_map_type).keySet()) {
            emotionNames.add(emojiName);
            // 每20个表情作为一组,同时添加到ViewPager对应的view集合中
            if (emotionNames.size() == 20) {
                GridView gv = createEmotionGridView(emotionNames, screenWidth, spacing, itemWidth, gvHeight);
                emotionViews.add(gv);
                // 添加完一组表情,重新创建一个表情名字集合
                emotionNames = new ArrayList<>();
            }
        }
        // 判断最后是否有不足20个表情的剩余情况
        if (emotionNames.size() > 0) {
            GridView gv = createEmotionGridView(emotionNames, screenWidth, spacing, itemWidth, gvHeight);
            emotionViews.add(gv);
        }
        //初始化指示器
        ll_point_group.initIndicator(emotionViews.size());
        // 将多个GridView添加显示到ViewPager中
        emotionPagerGvAdapter = new EmotionPagerAdapter(emotionViews);
        vp_complate_emotion_layout.setAdapter(emotionPagerGvAdapter);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(screenWidth, gvHeight);
        vp_complate_emotion_layout.setLayoutParams(params);
    }
    /**
     * 创建显示表情的GridView
     */
    private GridView createEmotionGridView(List<String> emotionNames, int gvWidth, int padding, int itemWidth, int gvHeight) {
        // 创建GridView
        GridView gv = new GridView(getActivity());
        //设置点击背景透明
        gv.setSelector(android.R.color.transparent);
        //设置7列
        gv.setNumColumns(7);
        gv.setPadding(padding, padding, padding, padding);
        gv.setHorizontalSpacing(padding);
        gv.setVerticalSpacing(padding * 2);
        //设置GridView的宽高
        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(gvWidth, gvHeight);
        gv.setLayoutParams(params);
        // 给GridView设置表情图片
        EmotionGridViewAdapter adapter = new EmotionGridViewAdapter(getActivity(), emotionNames, itemWidth,emotion_map_type);
        gv.setAdapter(adapter);
        //设置全局点击事件
        gv.setOnItemClickListener(GlobalOnItemClickManagerUtils.getInstance(getActivity()).getOnItemClickListener(emotion_map_type));
        return gv;
    }
}

注释非常清晰哈。我就不啰嗦了。但这有个要注意的是在for循环时是通过EmotionUtils的getEmojiMap(emotion_map_type).keySet()获取集合,这也印证前面我们所说的EmotiomComplateFragment内部是通过集合标志判断集合类型,最终获取到所需的集合数据,也就生成了不同表情类型的面板。

private int getSupportSoftInputHeight() {

3.4 表情的输入框插入和删除

思路:在表情框输入一个表情实际上是在当前光标位置插入一个表情,添加完表情后再把当前光标移动到表情之后,所以我们首先要获取到光标到首位置,这个可以利用EditText.setSelectionStart()方法,添加完表情后要设置光标的位置到表情之后,这个可以使用EditText.setSelection(position)方法。当然如果点击的是删除按钮,那么直接调用系统的
Delete 按钮事件即可。下面直接上代码:

// 点击的是表情
EmotionGridViewAdapter emotionGvAdapter = (EmotionGridViewAdapter) itemAdapter;
if (position == emotionGvAdapter.getCount() - 1) {
    // 如果点击了最后一个回退按钮,则调用删除键事件
    mEditText.dispatchKeyEvent(new KeyEvent(
            KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
} else {
    // 如果点击了表情,则添加到输入框中
    String emotionName = emotionGvAdapter.getItem(position);
    // 获取当前光标位置,在指定位置上添加表情图片文本
    int curPosition = mEditText.getSelectionStart();
    StringBuilder sb = new StringBuilder(mEditText.getText().toString());
    sb.insert(curPosition, emotionName);
    // 特殊文字处理,将表情等转换一下
    mEditText.setText(SpanStringUtils.getEmotionContent(emotion_map_type,
            mContext, mEditText, sb.toString()));
    // 将光标设置到新增完表情的右侧
    mEditText.setSelection(curPosition + emotionName.length());
}

这里要理解一点就是让控件调用系统事件的方法为EditText.displatchKeyEvent(new
KeyEvent(action,
code));其中action就是动作,用ACTION_DOWN按下动作就可以了而code为按钮事件码,删除对应的就是KEYCODE_DEL。

        Rect r = new Rect();

4.表情点击事件全局监听的实现

上面弄明白了表情的输入与删除操作后,我们就要考虑一个问题了,那就是在哪里设置监听?直接在创建GridView时,这个确实行得通,不过我们还要再考虑一个问题,那就是如果我们存在多个GridView呢?多复制几遍咯。但我们是高级工程师对吧,这样重复代码显然是不可出现在我们眼前的,因此这里我们决定使用全局监听来设置点击事件,当然这个并非我想到的,这个是在github开源项目我在阅读源码时,发现的,这种方式挺不错,我就拿来用咯。直接上代码:

package com.zejian.emotionkeyboard.utils;
import android.content.Context;
import android.view.KeyEvent;
import android.view.View;
import android.widget.AdapterView;
import android.widget.EditText;
import com.zejian.emotionkeyboard.adapter.EmotionGridViewAdapter;
/**
 * Created by zejian
 * Time  16/1/8 下午5:05
 * Email shinezejian@163.com
 * Description:点击表情的全局监听管理类
 */
public class GlobalOnItemClickManagerUtils {
    private static GlobalOnItemClickManagerUtils instance;
    private EditText mEditText;//输入框
    private static Context mContext;
    public static GlobalOnItemClickManagerUtils getInstance(Context context) {
        mContext=context;
        if (instance == null) {
            synchronized (GlobalOnItemClickManagerUtils.class) {
                if(instance == null) {
                    instance = new GlobalOnItemClickManagerUtils();
                }
            }
        }
        return instance;
    }
    public void attachToEditText(EditText editText) {
        mEditText = editText;
    }
    public AdapterView.OnItemClickListener getOnItemClickListener(final int emotion_map_type) {
        return new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                Object itemAdapter = parent.getAdapter();
                if (itemAdapter instanceof EmotionGridViewAdapter) {
                    // 点击的是表情
                    EmotionGridViewAdapter emotionGvAdapter = (EmotionGridViewAdapter) itemAdapter;
                    if (position == emotionGvAdapter.getCount() - 1) {
                        // 如果点击了最后一个回退按钮,则调用删除键事件
                        mEditText.dispatchKeyEvent(new KeyEvent(
                                KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
                    } else {
                        // 如果点击了表情,则添加到输入框中
                        String emotionName = emotionGvAdapter.getItem(position);
                        // 获取当前光标位置,在指定位置上添加表情图片文本
                        int curPosition = mEditText.getSelectionStart();
                        StringBuilder sb = new StringBuilder(mEditText.getText().toString());
                        sb.insert(curPosition, emotionName);
                        // 特殊文字处理,将表情等转换一下
                        mEditText.setText(SpanStringUtils.getEmotionContent(emotion_map_type,
                                mContext, mEditText, sb.toString()));
                        // 将光标设置到新增完表情的右侧
                        mEditText.setSelection(curPosition + emotionName.length());
                    }
                }
            }
        };
    }
}

代码相当简单,就是创建一个AdapterView.OnItemClickListener的全局监听器,然后在里面实现表情的输入与删除操作即可。那么怎么使用呢?我们在EmotionMainFragment类中使用创建GlobalOnItemClickManagerUtils,并绑定编辑框,部分代码如下:

//创建全局监听
GlobalOnItemClickManagerUtils globalOnItemClickManager= GlobalOnItemClickManagerUtils.getInstance(getActivity());
if(isBindToBarEditText){
  //绑定当前Bar的编辑框
  globalOnItemClickManager.attachToEditText(bar_edit_text);
}else{
  // false,则表示绑定contentView, 此时外部提供的contentView必定也是EditText
  globalOnItemClickManager.attachToEditText((EditText) contentView);
  mEmotionKeyboard.bindToEditText((EditText)contentView);
}

绑定的编辑框可能有两种情况,可能是Bar上的编辑框,但也可能是contentView,此时外部提供的contentView是EditText(可以直接理解为是把之前所说的listview替换成了edittext)。最后别忘记在EmotiomComplateFragment类种创建GridView时注册该监听器,

//设置全局点击事件
gv.setOnItemClickListener(GlobalOnItemClickManagerUtils.getInstance(getActivity()).getOnItemClickListener(emotion_map_type));

好了,到此本篇也完结了,下面给出源码下载方式:
Github项目源码下载地址
CSDN源码地址下载

        mActivity.getWindow().getDecorView().getWindowVisibleDisplayFrame(r);

        int screenHeight =
mActivity.getWindow().getDecorView().getRootView().getHeight();

        int softInputHeight = screenHeight – r.bottom;

        if (Build.VERSION.SDK_INT >= 20) {

            // When SDK Level >= 20 (Android L),

            // the softInputHeight will contain the height of
softButtonsBar (if has)

            softInputHeight = softInputHeight –
getSoftButtonsBarHeight();

        }

        if (softInputHeight < 0) {

            Log.w(“EmotionInputDetector”, “Warning: value of
softInputHeight is below zero!”);

        }

        if (softInputHeight > 0) {

            sp.edit().putInt(SHARE_PREFERENCE_TAG,
softInputHeight).apply();

        }

        return softInputHeight;

}

这里的原理是通过当前Activity获取RootView的高度减去Activity自身的高度,就得到了软键盘的高度,但是发现在有虚拟按键的手机上在没有显示软键盘时减出来的高度总是144,后来查了下资料,发现在API>18时有软键盘的手机需要减去底部虚拟按键的高度。

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)

private int getSoftButtonsBarHeight() {

        DisplayMetrics metrics = new DisplayMetrics();

        mActivity.getWindowManager().getDefaultDisplay().getMetrics(metrics);

        int usableHeight = metrics.heightPixels;

        mActivity.getWindowManager().getDefaultDisplay().getRealMetrics(metrics);

        int realHeight = metrics.heightPixels;

        if (realHeight > usableHeight) {

            return realHeight – usableHeight;

        } else {

            return 0;

        }

}

把获取到的高度设置给表情键盘

private void showEmotionLayout() {

     int softInputHeight = getSupportSoftInputHeight();

     if (softInputHeight == 0) {

         softInputHeight = sp.getInt(SHARE_PREFERENCE_TAG, 400);

     }

     hideSoftInput();

     mEmotionLayout.getLayoutParams().height = softInputHeight;

     mEmotionLayout.setVisibility(View.VISIBLE);

}

控制表情的显示与隐藏

private void showSoftInput() {

     mEditText.requestFocus();

     mEditText.post(new Runnable() {

         @Override

         public void run() {

            mInputManager.showSoftInput(mEditText, 0);

          }

     });

}

private void hideSoftInput() {

    mInputManager.hideSoftInputFromWindow(mEditText.getWindowToken(),
0);

}

博主在测试后发现一个问题,点击表情按钮,输入框会抖动,分析下这个过程,点击表情按钮,关闭软键盘,此时Activity的高度发生变化,高度变高,输入框回到底部,再打开表情键盘,此时输入框又被顶上来,输入框看起来上下抖动,经多次测试发现无论是先隐藏软键盘还是先显示表情键盘都存在这个问题,思考过后,既然输入框会上下抖动,那么固定它的位置不就行了,那么问题来了,如何固定它的位置呢?举个栗子,假如在一个LinearLayout里面有若干个控件,如果里面的控件的位置大小都不变,那么即使在软键盘显示和隐藏(Activity的高度发生变化),也不会隐藏输入框的位置,自然也就不会发生跳动问题。

锁定解锁内容高度(ListView、RecyclerView)

private void lockContentHeight() {

        LinearLayout.LayoutParams params = (LinearLayout.LayoutParams)
mContentView.getLayoutParams();

        params.height = mContentView.getHeight();

        params.weight = 0.0F;

}

private void unlockContentHeightDelayed() {

     mEditText.postDelayed(new Runnable() {

         @Override

         public void run() {

             ((LinearLayout.LayoutParams)
mContentView.getLayoutParams()).weight = 1.0F;

         }

     }, 200L);

}

表情面板控制

public EmotionInputDetector bindToEmotionButton(final CheckBox
emotionButton) {

        mEmojiView = emotionButton;

        emotionButton.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

                if (mEmotionLayout.isShown()) {

                    lockContentHeight();

                    hideEmotionLayout(true);

                    mEmojiView.setChecked(false);

                    unlockContentHeightDelayed();

                } else {

                    if (isSoftInputShown()) {

                        lockContentHeight();

                        showEmotionLayout();

                        mEmojiView.setChecked(true);

                        unlockContentHeightDelayed();

                    } else {

                        showEmotionLayout();

                    }

                }

            }

        });

    return this;

}

表情解析

问题分析: 1、如何将表情码和表情建立联系 2、如何给表情分页 3、如何将表情码转换成表情

将表情码和表情以键值对的形式建立联系

1ArrayMap emoJiMap = new ArrayMap();

key(表情码)value(表情地址)

emoJiMap.put(“[emoji_1]”,R.drawable.emoji_1);

        emoJiMap.put(“[emoji_2]”,R.drawable.emoji_2);

        emoJiMap.put(“[emoji_3]”,R.drawable.emoji_3);

        emoJiMap.put(“[emoji_4]”,R.drawable.emoji_4);

        emoJiMap.put(“[emoji_5]”,R.drawable.emoji_5);

        emoJiMap.put(“[emoji_6]”,R.drawable.emoji_6);

        emoJiMap.put(“[emoji_7]”,R.drawable.emoji_7);

        emoJiMap.put(“[emoji_8]”,R.drawable.emoji_8);

        emoJiMap.put(“[emoji_9]”,R.drawable.emoji_9);

        emoJiMap.put(“[emoji_10]”,R.drawable.emoji_10);

        emoJiMap.put(“[emoji_11]”,R.drawable.emoji_11);

        emoJiMap.put(“[emoji_12]”,R.drawable.emoji_12);

        emoJiMap.put(“[emoji_13]”,R.drawable.emoji_13);

        emoJiMap.put(“[emoji_14]”,R.drawable.emoji_14);

        emoJiMap.put(“[emoji_15]”,R.drawable.emoji_15);

        emoJiMap.put(“[emoji_16]”,R.drawable.emoji_16);

        emoJiMap.put(“[emoji_17]”,R.drawable.emoji_17);

        emoJiMap.put(“[emoji_18]”,R.drawable.emoji_18);

        emoJiMap.put(“[emoji_19]”,R.drawable.emoji_19);

        emoJiMap.put(“[emoji_20]”,R.drawable.emoji_20);

将表情面板的表情码用List进行保存

1

2

3

4

5

6

7

8

9

public List getPagers() {

        List pageViewList = new ArrayList<>();

        //每一页表情的view

        mPageNum = (int) Math.ceil(mEmoJiResList.size() * 1.0f /
EMOJI_PAGE_COUNT);

        for (int position = 1; position <= mPageNum; position++) {

            pageViewList.add(getGridView(position));

        }

        return pageViewList;

}

表情分页

public View getGridView(int position) {

        List mEmoJiList = new ArrayList<>();

        View containerView = View.inflate(mContext,
R.layout.container_gridview, null);

        ExpandGridView eg_gridView = (ExpandGridView)
containerView.findViewById(R.id.eg_gridView);

        eg_gridView.setGravity(Gravity.CENTER_VERTICAL);

        List emojiPageList = null;

        if (position == mPageNum)//最后一页

            emojiPageList = mEmoJiResList.subList((position – 1) *
EMOJI_PAGE_COUNT, mEmoJiResList.size());

        else

            emojiPageList = mEmoJiResList.subList((position – 1) *
EMOJI_PAGE_COUNT, EMOJI_PAGE_COUNT * position);

        mEmoJiList.addAll(emojiPageList);

        //添加删除表情

        mEmoJiList.add(“[删除]”);

        final EmoJiAdapter mEmoJiAdapter = new EmoJiAdapter(mContext,
position, mEmoJiList);

        eg_gridView.setAdapter(mEmoJiAdapter);

        eg_gridView.setOnItemClickListener(new
AdapterView.OnItemClickListener() {

            @Override

            public void onItemClick(AdapterView parent, View view, int
positionIndex, long id) {

                String fileName = mEmoJiAdapter.getItem(positionIndex);

                if (fileName != “[删除]”) { // 不是删除键,显示表情

                    showEmoJi(fileName);

                } else { // 删除文字或者表情

                    deleteContent();

                }

            }

        });

        return containerView;

}

将表情面板的表情码转解析成表情

@Override

public View getView(int position, View convertView, ViewGroup parent) {

        if (convertView == null) {

            convertView = View.inflate(getContext(),
R.layout.item_row_emoji, null);

        }

        ImageView imageView = (ImageView)
convertView.findViewById(R.id.iv_emoji);

        String fileName = getItem(position);

        Integer resId = EmoJiUtils.getEmoJiMap().get(fileName);

        if (resId != null) {

            Drawable drawable =
getContext().getResources().getDrawable(resId);

            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight());

            imageView.setImageResource(resId);

        }

        return convertView;

}

输入框表情码转换成表情

public static SpannableString parseEmoJi(Context context, String
content) {

        SpannableString spannable = new SpannableString(content);

        String reg =
“\[[a-zA-Z0-9_\u4e00-\u9fa5]+\]”;//校验表情正则

        Pattern pattern = Pattern.compile(reg);

        Matcher matcher = pattern.matcher(content);

        while (matcher.find()) {

            String regEmoJi = matcher.group();//获取匹配到的emoji字符串

            int start = matcher.start();//匹配到字符串的开始位置

            int end = matcher.end();//匹配到字符串的结束位置

            Integer resId =
emoJiMap.get(regEmoJi);//通过emoji名获取对应的表情id

            if (resId != null) {

                Drawable drawable =
context.getResources().getDrawable(resId);

                drawable.setBounds(0, 0, drawable.getIntrinsicWidth(),
drawable.getIntrinsicHeight());

                ImageSpan imageSpan = new ImageSpan(drawable, content);

                spannable.setSpan(imageSpan, start, end,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

            }

        }

        return spannable;

}

private void showEmoJi(String fileName) {

        int selectionStart = mInputContainer.getSelectionStart();

        String body = mInputContainer.getText().toString();

        StringBuilder stringBuilder = new StringBuilder(body);

        stringBuilder.insert(selectionStart, fileName);

        mInputContainer.setText(EmoJiUtils.parseEmoJi(mContext,
stringBuilder.toString()));

        mInputContainer.setSelection(selectionStart +
fileName.length());

}

表情删除

private void deleteContent() {

        if (!TextUtils.isEmpty(mInputContainer.getText())) {

            int selectionStart =
mInputContainer.getSelectionStart();//获取光标位置

            if (selectionStart > 0) {

                String body = mInputContainer.getText().toString();

                String lastStr = body.substring(selectionStart – 1,
selectionStart);//获取最后一个字符

                if (lastStr.equals(“]”)) {//表情

                    if (selectionStart < body.length())
{//从中间开始删除

                        body = body.substring(0, selectionStart);

                    }

                    int i = body.lastIndexOf(“[“);

                    if (i != -1) {

                        String tempStr = body.substring(i,
selectionStart);//截取表情码

                        if
(EmoJiUtils.getEmoJiMap().containsKey(tempStr)) {//校验是否是表情

                            mInputContainer.getEditableText().delete(i,
selectionStart);//删除表情

                        } else {

                            mInputContainer.getEditableText().delete(selectionStart

  • 1, selectionStart);//删除一个字符

                        }

                    } else {

                        mInputContainer.getEditableText().delete(selectionStart

  • 1, selectionStart);

                    }

                } else {//非表情

                    mInputContainer.getEditableText().delete(selectionStart

  • 1, selectionStart);

                }

            }

        }

}

四、成果

图片 46

EasyEmoji

发表评论

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