Android高仿微信表情输入与键盘输入详解
2016-09-16 18:32 阅读(269)

本文由泽坚投稿。

泽坚的博客地址:

http://blog.csdn.net/javazejian


文章以问题+解决的方式进行描述,可以更好的对细节进行接收。




1
概述
   

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




 

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

本篇主要分析的核心类EmotionKeyboard.java,EmotionComplateFragment.java,EmotionMainFragment.java,FragmentFactory.java,还有一个是工具类里的EmotionUtils.java和GlobalOnItemClickManagerUtils.java 这几个类我会重点分析一下,其他的大家自行看源码哈。


下面就开始咯,先来看看本篇主要内容以及大概思路: 


2

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

   

2.1跳闪问题概述


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


 


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


 


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


2.2 解决跳闪问题的思路:


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


2.3 解决跳闪问题的套路:


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

  

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



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


 
这下就清晰了吧,右边是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

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


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



其中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(),也就达到改变控件的高度的目的。到此两个主要问题都解决了


3
如何扩展
   

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




4

单个表情面板的实现思路

   

4.1 表情图片的本质与显示


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




ArrayMap





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



接下来的问题就是表情如何显示呢?其实这里主要用到了SpannableString拓展性字符串相关知识点,SpannableString可以让一段字符串在显示的时候,将其中某小段文字附着上其他内容或替换成其他内容,拓展内容可以是图片或者是文字格式,比如加粗,显示特殊颜色等。对于SpannableString不熟悉,可先看看这篇文章 (http://blog.csdn.net/lan410812571/article/details/9083023),下面我只对本篇需要用到的SpannableString作简要介绍: 



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


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


这里我们利用正则表达式找出特殊字符,根据我们自己的需求编写特定的正则表达式,如下: 


String regex = "\\[[\u4e00-\u9fa5\\w]+\\]"; 


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



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



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


4.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判断,如果是最后一个就进行特殊的显示(删除按钮)和点击处理。


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


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



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

code为按钮事件码,删除对应的就是KEYCODE_DEL。


源码下载: 
https://github.com/shinezejian/emotionkeyboard