Android实现一个功能完善的聊天页面
前言:在APP中经常需要使用到聊天页面,尤其是一些涉及社交和社区类的APP。本次我对自己做过的聊天页面的一些模块进行抽取归纳和总结,也希望能够帮助他人快速完成聊天页面的开发,所以写了这篇博客。实现了表情页面、礼物页面和一键发送页面, 主要解决了键盘跟表情和礼物框切换时的跳闪、礼物的切换选择、动画等问题,因为UI是从网上找的,动画效果文件是直接使用的YY开源库里面Demo所提供的,大家将就下,关键是了解实现的思路和流程即可,好,废话不多说,具体效果如下所示:
1、解决键盘与表情和礼物框的切换跳闪问题
我们先来看一下activity_main.xml的布局效果图:
现在我们来分析下上图中的蓝色框中的布局,代码如下:
<LinearLayout
android:id="@+id/ll_chatControl"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
android:orientation="vertical"
android:paddingTop="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="bottom"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingRight="10dp">
<LinearLayout
android:id="@+id/ll_chatMsg"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/chat_input_box_shape"
android:gravity="center_vertical"
android:paddingLeft="9dp"
android:paddingTop="11dp"
android:paddingRight="9dp"
android:paddingBottom="11dp">
<com.lin.clay.emojikeyboard.utils.CustomPasteEditText
android:id="@+id/edit_chatMsg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@null"
android:gravity="center_vertical"
android:hint="请输入内容"
android:includeFontPadding="false"
android:maxLines="5"
android:textColor="@color/black"
android:textColorHint="@color/chat_input_box_hint"
android:textCursorDrawable="@drawable/cursor_shape"
android:textSize="14dp" />
</LinearLayout>
<ImageView
android:id="@+id/img_emoji"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_marginBottom="4dp"
android:src="@drawable/chat_page_emoji" />
<RelativeLayout
android:layout_width="45dp"
android:layout_height="match_parent"
android:gravity="bottom">
<TextView
android:id="@+id/tv_send"
android:layout_width="45dp"
android:layout_height="28dp"
android:layout_centerHorizontal="true"
android:layout_marginBottom="4dp"
android:background="@drawable/chat_send_button_bg"
android:gravity="center"
android:text="发送"
android:textColor="@color/white"
android:textSize="13dp" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_moreOperate"
android:layout_width="match_parent"
android:layout_height="49dp"
android:layout_gravity="bottom"
android:background="@color/white"
android:gravity="center_vertical"
android:paddingLeft="20dp"
android:paddingRight="20dp">
<RelativeLayout
android:id="@+id/rl_oneKeySend"
android:layout_width="65dp"
android:layout_height="wrap_content"
android:layout_weight="1">
<ImageView
android:id="@+id/img_oneKeySend"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerInParent="true"
android:src="@drawable/chat_one_key_send" />
</RelativeLayout>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_marginTop="9dp"
android:layout_marginBottom="9dp"
android:background="@color/app_main_color" />
<RelativeLayout
android:id="@+id/rl_giftCommon"
android:layout_width="65dp"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_weight="1">
<ImageView
android:id="@+id/img_giftCommon"
android:layout_width="65dp"
android:layout_height="65dp"
android:layout_centerInParent="true"
android:src="@drawable/chat_page_gifts_common" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_emotionGifts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="#e5e5e5" />
<com.lin.clay.emojikeyboard.utils.NoHorizontalScrollerViewPager
android:id="@+id/vp_emotionGifts"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
</LinearLayout>
我们可以看到最下面有个NoHorizontalScrollerViewPager布局,这个是设置了不能滑动的viewpager,用来加载表情fragment、礼物fragment和一键发送fragment的。
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;
}
}
我们再来看下在MainActivity中涉及键盘和表情、礼物切换的主要代码:
//表情面板
private EmotionKeyboard mEmotionKeyboard;
List<Fragment> emotionFragments = new ArrayList<>();
private GlobalOnItemClickManagerUtils GlobalOnItemClickManager; //表情和编辑框的绑定
private void initKEG() {
mEmotionKeyboard = EmotionKeyboard.with(this)
.setEmotionView(llEmotionGifts)//绑定表情面板
.setEmotionGiftsView(vpEmotionGifts)//绑定礼物表情选择
.bindToContent(rvMatchMsg)//绑定内容view
.bindToEditText(editChatMsg)//判断绑定那种EditView
.bindToEmotionButton(imgEmoji)//绑定表情按钮
.bindToGiftsButton(rlGiftCommon)//绑定礼物按钮
.bindToOneKeySendButton(rlOneKeySend)//绑定一键发送按钮
.bindToChatControl(llChatControl) //绑定整个底部表情礼物布局
.bindKeyboardListener() //绑定键盘弹出隐藏监听
.build();
//创建全局监听
GlobalOnItemClickManager = GlobalOnItemClickManagerUtils.getInstance(this);
GlobalOnItemClickManager.attachToEditText(editChatMsg);
emotionFragments.add(new EmotionFragment());
emotionFragments.add(new GiftsFragment());
emotionFragments.add(new OneKeySendFragment());
vpEmotionGifts.setAdapter(new EmotionGiftsFragmentAdapter(getSupportFragmentManager(), emotionFragments));
//设置缓存View的个数
vpEmotionGifts.setOffscreenPageLimit(emotionFragments.size() - 1);
}
其中EmotionKeyboard类就是我们解决切换过程中跳闪问题的关键类,代码也比较长,所以这边就不全部贴出来了,主要挑比较重要的来讲,具体代码大家可以前往最下面进入GitHub中查看。
状态栏:首先,因为在这里状态栏设置了白底黑字,因为手机系统默认时白色字体的,但是在开发中我所需要使用的沉浸式是白底黑字,所以需要修改状态栏的字体颜色,Andorra6.0及以上Android系统提供了API可以改变状态栏的字体颜色,6.0以下的只有小米和魅族手机官方提供了方法,大家可以查看这个//TODO 博客,设置了白底黑字后在底部编辑框不会被键盘顶起,以前只要设置了状态栏透明也会出现这个问题,这个是Android本身的问题,网上有一个类AndroidBug5497Workaround 可以解决这个问题,好像这个现在已经被Android官方给修复了。
获取软键盘的高度:我们可以使用绑定监听,获取软键盘的高度,然后用SharedPreferences保存下来,下次可直接使用
public EmotionKeyboard bindKeyboardListener() {
//检查,用来处理一些隐藏的导航栏导致表情布局弹出高度不一致,为了适配一些隐藏了虚拟按键的手机
//保证监听获取的键盘高度正确
checkHasHintNavigationBar();
virtualBarHeigh = Utils.getVirtualBarHeigh(mActivity);
// TODO: 2018/8/14 适应首页动画,改变了6.0以下的状态栏,解除限制
//if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
SoftKeyBoardListener.setListener(mActivity, new SoftKeyBoardListener.OnSoftKeyBoardChangeListener() {
@Override
public void keyBoardShow(int height) {
sp.edit().putInt(SHARE_PREFERENCE_SOFT_INPUT_HEIGHT, height).apply();
if (virtualBarHeigh != 0 && screenHeight != rootViewHeight) {
height = height - virtualBarHeigh;
}
setChatControlViewParams(height);
((MainActivity) mActivity).chatListScroolDelay(200);
}
@Override
public void keyBoardHide(int height) {
if (!mEmotionLayout.isShown()) {
setChatControlViewParams(0);
}
}
});
//}
return this;
}
RecyclerView:也就是在类中的mContentView,在操作时主要是操作mContentView的权重weight,通过改变权重获得需要的效果。
操作编辑框:即是弹出和隐藏键盘时,获取保存了软键盘的高度,通过监听键盘的弹起与收起,然后根据键盘的变化相应操作绑定整个底部表情礼物布局llChatControl的bottomMargin,使键盘弹出的时候位于键盘的上方,键盘退出的时候设置bottomMargin为0。
private void setChatControlViewParams(int marginButton) {
((LinearLayout.LayoutParams) mContentView.getLayoutParams()).weight = 1.0F;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) mChatControlView.getLayoutParams();
layoutParams.bottomMargin = marginButton;
mChatControlView.setLayoutParams(layoutParams);
}
操作表情按钮:有两种情况:
/**
* 绑定表情按钮
*
* @param emotionButton
* @return
*/
public EmotionKeyboard bindToEmotionButton(final View emotionButton) {
emojiIcon = (ImageView)emotionButton;
emotionButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
emojiIcon.setImageResource(R.drawable.chat_page_keyboard);
if (mEmotionLayout.isShown()) {
if (vpEmotion.getCurrentItem() == 1) {
vpEmotion.setCurrentItem(0);
return;
}
if (vpEmotion.getCurrentItem() == 2) {
vpEmotion.setCurrentItem(0);
return;
}
if(vpEmotion.getCurrentItem() == 0) {
emojiIcon.setImageResource(R.drawable.chat_page_emoji);
//TODO: 2018/5/23 表情已经弹出,再点击时不变化,不让其弹出软键盘
lockContentHeight();//显示软件盘时,锁定内容高度,防止跳闪。
hideEmotionLayout(true);//隐藏表情布局,显示软件盘
unlockContentHeightDelayed();//软件盘显示后,释放内容 高度
return;
}
} else {
vpEmotion.setCurrentItem(0);
if (isSoftInputShown()) {//同上
setChatControlViewParams(0);
lockContentHeight();
showEmotionLayout();
unlockContentHeightDelayed();
} else {
showEmotionLayout();//两者都没显示,直接显示表情布局
}
}
((MainActivity) mActivity).chatListScroolDelay(200);
}
});
return this;
}
一种是mEmotionLayout已经显示的情况,mEmotionLayout即是那包括了显示表情和礼物fragment的上面提到的那个viewpager。如果显示的不是表情框,那么则是vpEmotion中的fragment切换,如果已经显示的是表情fragment,那么则需要隐藏mEmotionLayout,弹出软键盘,首先需要锁定内容高度,也就是RecyclerView的高度,把权重设为0,然后隐藏mEmotionLayout,显示软键盘,然后需要加个延迟把RecyclerView的权重再设为1,至于llChatControl,因为上面有键盘的监听,所以会自动回调设置bottomMargin的。
/**
* 锁定内容高度,防止跳闪
*/
private void lockContentHeight() {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mContentView.getLayoutParams();
params.height = mContentView.getHeight();
params.weight = 0.0F;
}
/**
* 隐藏表情布局
*
* @param showSoftInput 是否显示软件盘
*/
private void hideEmotionLayout(boolean showSoftInput) {
if (mEmotionLayout.isShown()) {
mEmotionLayout.setVisibility(View.GONE);
if (showSoftInput) {
showSoftInput();
}
}
}
/**
* 释放被锁定的内容高度
*/
private void unlockContentHeightDelayed() {
mEditText.postDelayed(new Runnable() {
@Override
public void run() {
((LinearLayout.LayoutParams) mContentView.getLayoutParams()).weight = 1.0F;
}
}, 200L);
}
第二种是mEmotionLayouy没有显示的情况,如果键盘没有弹出,则直接显示表情布局;如果键盘已经弹出,则现将mChatControlView的bottomMargin设为0,然后锁定RecyclerView的高度,显示表情礼物mEmotionLayout,并设定mEmotionLayout高度等于键盘的高度,然后需要加个延迟把RecyclerView的权重再设为1。
private void setChatControlViewParams(int marginButton) {
((LinearLayout.LayoutParams) mContentView.getLayoutParams()).weight = 1.0F;
LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams) mChatControlView.getLayoutParams();
layoutParams.bottomMargin = marginButton;
mChatControlView.setLayoutParams(layoutParams);
}
/**
* 锁定内容高度,防止跳闪
*/
private void lockContentHeight() {
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mContentView.getLayoutParams();
params.height = mContentView.getHeight();
params.weight = 0.0F;
}
/**
* 显示表情礼物mEmotionLayout,并设定高度等于键盘的高度
*/
private void showEmotionLayout() {
softInputHeight = getKeyBoardHeight();
hideSoftInput();
mEmotionLayout.getLayoutParams().height = softInputHeight;
mEmotionLayout.setVisibility(View.VISIBLE);
}
/**
* 释放被锁定的内容高度
*/
private void unlockContentHeightDelayed() {
mEditText.postDelayed(new Runnable() {
@Override
public void run() {
((LinearLayout.LayoutParams) mContentView.getLayoutParams()).weight = 1.0F;
}
}, 200L);
}
操作礼物和一键发送按钮:实现和流程跟表情是一样的,无非是通过vpEmotion切换fragment和一些细节的不同,具体前往最下面进入查看源码。
参考:https://blog.csdn.net/javazejian/article/details/50542912
2、表情页面
在最上面中我们提到了NoHorizontalScrollerViewPager,这个是用来加载三个fragment的,表情页面在EmotionFragment中实现,在此EmotionFragment中使用了工厂模式,可以生产各种表情,根据需要对各种表情进行添加和扩展,
首先在EmotionFragment的布局如下:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager.widget.ViewPager
android:id="@+id/vp_emotion"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<View
android:layout_width="match_parent"
android:layout_height="1px"
android:background="@color/emotion_gifts_interval"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_emotion"
android:layout_width="match_parent"
android:layout_height="40dp"/>
</LinearLayout>
可以看到这里同样使用了ViewPager,这里是用来加载各种表情fragment的,比如一些经典表情和一些GIF动画表情等,这里只添加了经典表情,下面的RecyclerView是用来实现条目横向布局的,就是与各种表情fragment相对应的,从效果图也能看得出来。
下面这个方法是首先通过工厂类FragmentFactory根据传进去的参数生成各种表情的EmotionFactoryFragment。然后给ViewPager加载各种表情EmotionFactoryFragment。
private void setVPEmotion() {
//创建fragment的工厂类
FragmentFactory factory = FragmentFactory.getSingleFactoryInstance();
//经典表情
EmotionFactoryFragment classicEmotionFragment = (EmotionFactoryFragment) factory.getEmotiomFragment(EmotionManager.getInstance().EMOTION_CLASSIC_TYPE, Utils.Dp2Px(getActivity(), 25), 7, 0);
emotionFragments.add(classicEmotionFragment);
//经典表情2
// EmotionFactoryFragment otherEmotionFragment = (EmotionFactoryFragment) factory.getEmotiomFragment(EmotionUtils.EMOTION_OTHER_TYPE, Utils.Dp2Px(getActivity(), 60), 4, 1);
// emotionFragments.add(otherEmotionFragment);
EmotionGiftsFragmentAdapter adapter = new EmotionGiftsFragmentAdapter(getChildFragmentManager(), emotionFragments);
vpEmotion.setAdapter(adapter);
}
这个是工厂类,专门用来生成表情fragment和礼物fragment的
public class FragmentFactory {
public static final String EMOTION_MAP_TYPE = "EMOTION_MAP_TYPE";
public static final String ITEM_WIDTH_HEIGHT = "ITEM_WIDTH_HEIGHT";
public static final String EMOTION_COLUMNS = "EMOTION_COLUMNS";
public static final String EMOTION_TYPE = "EMOTION_TYPE";
private static FragmentFactory factory;
private FragmentFactory() {
}
/**
* 双重检查锁定,获取工厂单例对象
*
* @return
*/
public static FragmentFactory getSingleFactoryInstance() {
if (factory == null) {
synchronized (FragmentFactory.class) {
if (factory == null) {
factory = new FragmentFactory();
}
}
}
return factory;
}
/**
* 根据需求获取表情fragment的方法
*
* @param emotionType 表情类型,用于判断使用哪个map集合的表情
* @param itemSize 表情大小
* @param columns 表情列数
* @param type 表情类型,0:表示经典表情 1:表示其它表情
* @return
*/
public Fragment getEmotiomFragment(int emotionType, int itemSize, int columns, int type) {
Bundle bundle = new Bundle();
bundle.putInt(FragmentFactory.EMOTION_MAP_TYPE, emotionType);
bundle.putInt(FragmentFactory.ITEM_WIDTH_HEIGHT, itemSize);
bundle.putInt(FragmentFactory.EMOTION_COLUMNS, columns);
bundle.putInt(FragmentFactory.EMOTION_TYPE, type);
EmotionFactoryFragment emotionFactoryFragment = EmotionFactoryFragment.newInstance(EmotionFactoryFragment.class, bundle);
return emotionFactoryFragment;
}
/**
* 根据需求获取礼物fragment的方法
*
* @return
* @param data
*/
public Fragment getGiftsFragment(ArrayList<IMChatGiftsModel.GiftDetail> data) {
Bundle bundle = new Bundle();
bundle.putSerializable("imchatgifts",data);
Fragment giftsFactoryFragment = GiftsFactoryFragment.newInstance(GiftsFactoryFragment.class, bundle);
return giftsFactoryFragment;
}
}
至于EmotionFactoryFragment,这个就是真正的每个表情页了,布局就是一个RecyclerView,这里主要介绍一下表情页里面的这个方法
private void setEmotionDatas() {
List<String> emotionNames = new ArrayList<>();
Collections.addAll(emotionNames, EmotionManager.getInstance().EMOJI_TEXT_ARRAY);
EmotionFactoryAdapter emotionFactoryAdapter = new EmotionFactoryAdapter(getActivity(), emotionNames, emotion_map_type, item_width_height, emotion_columns, emotion_type);
rvEmotion.setLayoutManager(new GridLayoutManager(getActivity(), emotion_columns, RecyclerView.VERTICAL, false));
rvEmotion.setAdapter(emotionFactoryAdapter);
if (emotion_type == 0) {
emotionFactoryAdapter.setOnEmotionClickItemListener(GlobalOnItemClickManagerUtils.getInstance(getActivity()).getOnClassicEmotionClickItemListener(emotion_map_type));
} else if (emotion_type == 1) {
emotionFactoryAdapter.setOnEmotionClickItemListener(GlobalOnItemClickManagerUtils.getInstance(getActivity()).getOnOtherEmotionClickItemListener(emotion_map_type));
}
}
这里就是给页面设置adapter,重点关注方法最下面的点击事件,我们把这个是个点击事件交给了GlobalOnItemClickManagerUtils,这个类在最上面介绍的代码中也出现过,它是一个单例,主要就是绑定编辑框,然后在表情点击选择选择之后添加到标编辑框上,
public class GlobalOnItemClickManagerUtils {
private static GlobalOnItemClickManagerUtils instance;
private List<EditText> mEditTextList;
public static GlobalOnItemClickManagerUtils getInstance(Context context)
{
if(instance == null)
{
synchronized(GlobalOnItemClickManagerUtils.class)
{
if(instance == null)
{
instance = new GlobalOnItemClickManagerUtils();
instance.mEditTextList = new ArrayList<>();
}
}
}
return instance;
}
public void attachToEditText(EditText editText)
{
mEditTextList.add(editText);
}
/**
* 经典表情点击事件
*
* @param emotion_map_type
* @return
*/
public OnEmotionClickItemListener getOnClassicEmotionClickItemListener(final int emotion_map_type)
{
return new OnEmotionClickItemListener()
{
@Override
public void onItemClick(RecyclerView.Adapter adapter, View view, int position)
{
if(adapter instanceof EmotionFactoryAdapter)
{
if(mEditTextList == null || mEditTextList.size() == 0)
{
return;
}
EditText mEditText = mEditTextList.get(mEditTextList.size() - 1);
if(mEditText == null)
{
return;
}
Context mContext = mEditText.getContext();
if(mContext == null)
{
return;
}
EmotionFactoryAdapter emotionFactoryAdapter = (EmotionFactoryAdapter)adapter;
if(position == emotionFactoryAdapter.getItemCount() - 1)
{
// 如果点击了最后一个回退按钮,则调用删除键事件
mEditText.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));
}
else
{
// 如果点击了表情,则添加到输入框中
String emotionName = emotionFactoryAdapter.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, (int)(mEditText.getTextSize() * 1.5), sb.toString()));
// mEditText.setText(sb.toString());
// ClickSpanBuilder.getInstance().setIsEmoji(true).build(mEditText);
// 将光标设置到新增完表情的右侧
mEditText.setSelection(curPosition + emotionName.length());
}
}
}
@Override
public void onItemLongClick(RecyclerView.Adapter adapter, View view, int position)
{
}
};
}
/**
* 其它表情点击事件
*
* @param emotion_map_type
* @return
*/
public OnEmotionClickItemListener getOnOtherEmotionClickItemListener(int emotion_map_type)
{
return new OnEmotionClickItemListener()
{
@Override
public void onItemClick(RecyclerView.Adapter adapter, View view, int position)
{
if(adapter instanceof EmotionFactoryAdapter)
{
EmotionFactoryAdapter emotionFactoryAdapter = (EmotionFactoryAdapter)adapter;
}
}
@Override
public void onItemLongClick(RecyclerView.Adapter adapter, View view, int position)
{
}
};
}
public void onDestrouGlobalOnItemClickManager()
{
if(mEditTextList == null || mEditTextList.size() == 0)
{
return;
}
EditText editText = mEditTextList.get(mEditTextList.size() - 1);
mEditTextList.remove(mEditTextList.size() - 1);
editText = null;
}
}
我们再来看下这个类中的getOnClassicEmotionClickItemListener方法,里面给编辑框setText中使用了SpanStringUtils,这个类就是根据正则将相应的文本替换成emoji表情图片。
/**
* 根据文本替换成emoji表情图片
*
* @param text
* @return
*/
public static CharSequence getEmotionContent(int emotion_map_type, final Context mContext, int size, String text)
{
if(TextUtils.isEmpty(text))
{
return "";
}
SpannableStringBuilder builder = new SpannableStringBuilder(text);
Pattern mPattern2 = EmotionManager.getInstance().mPatternEmoji;
Matcher matcher = mPattern2.matcher(text);
while(matcher.find())
{
// 利用表情名字获取到对应的图片
int resId = EmotionManager.getInstance().getImgByName(emotion_map_type, matcher.group());
Drawable drawable = mContext.getResources().getDrawable(resId);
drawable.setBounds(0, 0, size, size);//这里设置图片的大小
MyImageSpan imageSpan = new MyImageSpan(drawable);
builder.setSpan(imageSpan, matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return builder;
}
这里面可以看到使用了EmotionManager,这个就是表情的管理类,我们可以往这个类里面添加各种表情集合,还有相应的正则表达式,然后在EmotionFactoryFragment根据传递过来的表情类型显示出相应的表情页。
public class EmotionManager {
/**
* 表情类型标志符
*/
public static final int EMOTION_CLASSIC_TYPE = 0x0001;//经典表情
public static final int EMOTION_OTHER_TYPE = 0x0002;//经典表情
/**
* key-表情文字;
* value-表情图片资源
*/
public ArrayMap<String, Integer> EMOTION_CLASSIC_MAP;
public ArrayMap<String, Integer> EMOTION_OTHER_MAP;
public ArrayMap<String, Integer> EMPTY_MAP;
public ArrayMap<String, Integer> mEmojiSmileyToRes;
public Pattern mPatternEmoji;
private static EmotionManager emotionManager;
public static EmotionManager getInstance() {
if (emotionManager == null) {
synchronized (EmotionManager.class) {
if (emotionManager == null) {
emotionManager = new EmotionManager();
}
}
}
return emotionManager;
}
private EmotionManager() {
mPatternEmoji = buildPatternEmoji();
EMOTION_CLASSIC_MAP = buildEmojiSmileyToRes();
//其它表情
EMOTION_OTHER_MAP = new ArrayMap<>();
// EMOTION_OTHER_MAP.put("[clay呵呵]", R.drawable.lt_001_s);
// EMOTION_OTHER_MAP.put("[clay嘻嘻]", R.drawable.lt_002_s);
// EMOTION_OTHER_MAP.put("[clay哈哈]", R.drawable.lt_003_s);
//空表情
EMPTY_MAP = new ArrayMap<>();
}
private final int[] EMOJI_SMILEY_RES_IDS = {
R.drawable.emoji_001, R.drawable.emoji_002, R.drawable.emoji_003, R.drawable.emoji_004,
R.drawable.emoji_005, R.drawable.emoji_006, R.drawable.emoji_007, R.drawable.emoji_008, R.drawable.emoji_009, R.drawable.emoji_010,
R.drawable.emoji_011, R.drawable.emoji_012, R.drawable.emoji_013, R.drawable.emoji_014, R.drawable.emoji_015, R.drawable.emoji_016,
R.drawable.emoji_017, R.drawable.emoji_018, R.drawable.emoji_019, R.drawable.emoji_020, R.drawable.emoji_021, R.drawable.emoji_022,
R.drawable.emoji_023, R.drawable.emoji_024, R.drawable.emoji_025, R.drawable.emoji_026, R.drawable.emoji_027, R.drawable.emoji_028,
R.drawable.emoji_029, R.drawable.emoji_030, R.drawable.emoji_031, R.drawable.emoji_032, R.drawable.emoji_033, R.drawable.emoji_034,
R.drawable.emoji_035, R.drawable.emoji_036, R.drawable.emoji_037, R.drawable.emoji_038, R.drawable.emoji_039, R.drawable.emoji_040,
R.drawable.emoji_041, R.drawable.emoji_042, R.drawable.emoji_043, R.drawable.emoji_044, R.drawable.emoji_045, R.drawable.emoji_046,
R.drawable.emoji_047, R.drawable.emoji_048, R.drawable.emoji_049, R.drawable.emoji_050, R.drawable.emoji_051, R.drawable.emoji_052,
R.drawable.emoji_053, R.drawable.emoji_054, R.drawable.emoji_055, R.drawable.emoji_056, R.drawable.emoji_057, R.drawable.emoji_058,
R.drawable.emoji_059, R.drawable.emoji_060, R.drawable.emoji_061, R.drawable.emoji_062, R.drawable.emoji_063, R.drawable.emoji_064,
R.drawable.emoji_065, R.drawable.emoji_066, R.drawable.emoji_067, R.drawable.emoji_068, R.drawable.emoji_069, R.drawable.emoji_070,
R.drawable.emoji_071, R.drawable.emoji_072, R.drawable.emoji_073, R.drawable.emoji_074, R.drawable.emoji_075, R.drawable.emoji_076,
R.drawable.emoji_077, R.drawable.emoji_078, R.drawable.emoji_079, R.drawable.emoji_080, R.drawable.emoji_081, R.drawable.emoji_082,
R.drawable.emoji_083, R.drawable.emoji_084, R.drawable.emoji_085, R.drawable.emoji_086, R.drawable.emoji_087, R.drawable.emoji_088,
R.drawable.emoji_089, R.drawable.emoji_090, R.drawable.emoji_091, R.drawable.emoji_092, R.drawable.emoji_093, R.drawable.emoji_094,
R.drawable.emoji_095, R.drawable.emoji_096, R.drawable.emoji_097, R.drawable.emoji_098, R.drawable.emoji_099, R.drawable.emoji_100,
R.drawable.emoji_101, R.drawable.emoji_102, R.drawable.emoji_103, R.drawable.emoji_104, R.drawable.emoji_105, R.drawable.emoji_106,
R.drawable.emoji_107, R.drawable.emoji_108, R.drawable.emoji_109, R.drawable.emoji_110, R.drawable.emoji_111, R.drawable.emoji_112,
R.drawable.emoji_113, R.drawable.emoji_114, R.drawable.emoji_115, R.drawable.emoji_116, R.drawable.emoji_117, R.drawable.emoji_118,
R.drawable.emoji_119, R.drawable.emoji_120, R.drawable.emoji_121, R.drawable.emoji_122, R.drawable.emoji_123, R.drawable.emoji_124,
};
public final String[] EMOJI_TEXT_ARRAY = new String[]{
"\uD83D\uDE0C", "\uD83D\uDE28", "\uD83D\uDE37", "\uD83D\uDE33", "\uD83D\uDE12", "\uD83D\uDE30", "\uD83D\uDE0A", "\uD83D\uDE03", "\uD83D\uDE1E",
"\uD83D\uDE20", "\uD83D\uDE1C", "\uD83D\uDE0D", "\uD83D\uDE31", "\uD83D\uDE13", "\uD83D\uDE25", "\uD83D\uDE0F", "\uD83D\uDE14", "\uD83D\uDE01",
"\uD83D\uDE09", "\uD83D\uDE23", "\uD83D\uDE16", "\uD83D\uDE2A", "\uD83D\uDE1D", "\uD83D\uDE32", "\uD83D\uDE2D", "\uD83D\uDE02", "\uD83D\uDE22",
"☺", "\uD83D\uDE04", "\uD83D\uDE21", "\uD83D\uDE1A", "\uD83D\uDE18", "\uD83D\uDC4F", "\uD83D\uDC4D", "\uD83D\uDC4C", "\uD83D\uDC4E", "\uD83D\uDCAA",
"\uD83D\uDC4A", "\uD83D\uDC46", "✌", "✋", "\uD83D\uDCA1", "\uD83C\uDF39", "\uD83C\uDF84", "\uD83D\uDEA4", "\uD83D\uDC8A", "\uD83D\uDEC1", "⭕",
"❌", "❓", "❗", "\uD83D\uDEB9", "\uD83D\uDEBA", "\uD83D\uDC8B", "❤", "\uD83D\uDC94", "\uD83D\uDC98", "\uD83C\uDF81", "\uD83C\uDF89", "\uD83D\uDCA4",
"\uD83D\uDCA8", "\uD83D\uDD25", "\uD83D\uDCA6", "⭐", "\uD83C\uDFC0", "⚽", "\uD83C\uDFBE", "\uD83C\uDF74", "\uD83C\uDF5A", "\uD83C\uDF5C", "\uD83C\uDF70",
"\uD83C\uDF54", "\uD83C\uDF82", "\uD83C\uDF59", "☕", "\uD83C\uDF7B", "\uD83C\uDF49", "\uD83C\uDF4E", "\uD83C\uDF4A", "\uD83C\uDF53", "☀", "☔", "\uD83C\uDF19",
"⚡", "⛄", "☁", "\uD83C\uDFC3", "\uD83D\uDEB2", "\uD83D\uDE8C", "\uD83D\uDE85", "\uD83D\uDE95", "\uD83D\uDE99", "✈", "\uD83D\uDC78", "\uD83D\uDD31",
"\uD83D\uDC51", "\uD83D\uDC8D", "\uD83D\uDC8E", "\uD83D\uDC84", "\uD83D\uDC85", "\uD83D\uDC60", "\uD83D\uDC62", "\uD83D\uDC52", "\uD83D\uDC57", "\uD83C\uDF80",
"\uD83D\uDC5C", "\uD83C\uDF40", "\uD83D\uDC9D", "\uD83D\uDC36", "\uD83D\uDC2E", "\uD83D\uDC35", "\uD83D\uDC2F", "\uD83D\uDC3B", "\uD83D\uDC37", "\uD83D\uDC30",
"\uD83D\uDC24", "\uD83D\uDC2C", "\uD83D\uDC33", "\uD83C\uDFB5", "\uD83D\uDCF7", "\uD83C\uDFA5", "\uD83D\uDCBB", "\uD83D\uDCF1", "\uD83D\uDD52"
};
//聊天emoji表情
private ArrayMap<String, Integer> buildEmojiSmileyToRes() {
if (EMOJI_SMILEY_RES_IDS.length != EMOJI_TEXT_ARRAY.length) {
//表情的数量需要和数组定义的长度一致!
throw new IllegalStateException("Smiley resource ID/text mismatch");
}
ArrayMap<String, Integer> smileyToRes = new ArrayMap<String, Integer>(EMOJI_TEXT_ARRAY.length);
for (int i = 0; i < EMOJI_TEXT_ARRAY.length; i++) {
smileyToRes.put(EMOJI_TEXT_ARRAY[i], EMOJI_SMILEY_RES_IDS[i]);
}
return smileyToRes;
}
//构建Emoji正则表达式
private Pattern buildPatternEmoji() {
StringBuilder patternString = new StringBuilder(EMOJI_TEXT_ARRAY.length * 3);
patternString.append('(');
for (String s : EMOJI_TEXT_ARRAY) {
patternString.append(Pattern.quote(s));
patternString.append('|');
}
patternString.replace(patternString.length() - 1, patternString.length(), ")");
return Pattern.compile(patternString.toString());
}
/**
* 根据文本替换成emoji表情图片
*
* @param text
* @param size
* @return
*/
public CharSequence replaceEmoji(Context mContext, CharSequence text, int size) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
Matcher matcher = mPatternEmoji.matcher(text);
while (matcher.find()) {
int resId = mEmojiSmileyToRes.get(matcher.group());
Drawable drawable = mContext.getResources().getDrawable(resId);
drawable.setBounds(0, 0, Utils.getRealPixel(size), Utils.getRealPixel(size));//这里设置图片的大小
MyImageSpan imageSpan = new MyImageSpan(drawable);
builder.setSpan(imageSpan, matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
return builder;
}
/**
* 根据名称获取当前表情图标R值
*
* @param EmotionType 表情类型标志符
* @param imgName 名称
* @return
*/
public int getImgByName(int EmotionType, String imgName) {
Integer integer = null;
switch (EmotionType) {
case EMOTION_CLASSIC_TYPE:
integer = EMOTION_CLASSIC_MAP.get(imgName);
break;
case EMOTION_OTHER_TYPE:
integer = EMOTION_OTHER_MAP.get(imgName);
break;
default:
LogUtils.e("the emojiMap is null!! Handle Yourself ");
break;
}
return integer == null ? -1 : integer;
}
/**
* 根据类型获取表情数据
*
* @param EmotionType
* @return
*/
public ArrayMap<String, Integer> getEmojiMap(int EmotionType) {
ArrayMap EmojiMap = null;
switch (EmotionType) {
case EMOTION_CLASSIC_TYPE:
EmojiMap = EMOTION_CLASSIC_MAP;
break;
case EMOTION_OTHER_TYPE:
EmojiMap = EMOTION_OTHER_MAP;
break;
default:
EmojiMap = EMPTY_MAP;
break;
}
return EmojiMap;
}
}
3、礼物页面
礼物页面的实现跟表情页其实差不多,搞清楚了表情页面的实现相信很快能搞定礼物的实现流程,礼物页面的根fragment是GiftsFragment,它的布局如下,可以看出跟表情差不多,同样是一个viewpager和一个RecyclerView,只是多了一个点击赠送。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.viewpager.widget.ViewPager
android:id="@+id/vp_gifts"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
<View
android:layout_width="match_parent"
android:layout_height="0.5dp"
android:background="@color/emotion_gifts_interval"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="40dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_gifts"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginRight="73dp"/>
<RelativeLayout
android:id="@+id/rl_gradualGifts"
android:layout_width="84dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:background="@drawable/chat_gifts_gradual_button">
<RelativeLayout android:layout_width="73dp"
android:layout_height="match_parent"
android:layout_alignParentRight="true"
android:background="@color/white">
<TextView
android:layout_width="53dp"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="@drawable/chat_gradual_gift_shape"
android:gravity="center"
android:includeFontPadding="false"
android:padding="6dp"
android:text="赠送"
android:textColor="@color/white"
android:textSize="13dp"/>
</RelativeLayout>
</RelativeLayout>
</RelativeLayout>
</LinearLayout>
我们再来看下这个方法,同样是使用工厂模式通过FragmentFactory创建礼物GiftsFactoryFragment,然后使用viewpager进行加载。
private void setVPGifts(ArrayList<IMChatGiftsModel> data)
{
//创建fragment的工厂类
FragmentFactory factory = FragmentFactory.getSingleFactoryInstance();
for(int i = 0; i < data.size(); i++)
{
IMChatGiftsModel imChatGiftsModel = data.get(i);
ArrayList<IMChatGiftsModel.GiftDetail> items = imChatGiftsModel.items;
if(items != null && items.size() > 0)
{
GiftsFactoryFragment giftsFactoryFragment = (GiftsFactoryFragment)factory.getGiftsFragment(items);
giftsFragments.add(giftsFactoryFragment);
}
}
EmotionGiftsFragmentAdapter adapter = new EmotionGiftsFragmentAdapter(getChildFragmentManager(), giftsFragments);
vpGifts.setAdapter(adapter);
vpGifts.setOffscreenPageLimit(giftsFragments.size());
}
下面这个方法是点击赠送按钮后调用的,通过giftsFactoryFragment.isChackedGift()判断页面中礼物是否选中,选中则获取礼物的信息然后调用disposeSelectedGift(checkGifts)将其传给MainActivity进行发送。
/**
* 不默认选择礼物--赠送礼物
*/
private void sendGifts()
{
List<Fragment> fragments = getChildFragmentManager().getFragments();
if(fragments != null)
{
for(int i = 0; i < fragments.size(); i++)
{
GiftsFactoryFragment giftsFactoryFragment = (GiftsFactoryFragment)fragments.get(i);
boolean chackedGift = giftsFactoryFragment.isChackedGift();
if(chackedGift)
{
IMChatGiftsModel.GiftDetail checkGifts = giftsFactoryFragment.getCheckGifts();
if(checkGifts == null)
{
return;
}
disposeSelectedGift(checkGifts);
return;
}
}
}
}
点击选中礼物之后最好需要一些效果显示给用户,所以我在GiftsFactoryFragment布局中的RecyclerView的GiftsFactoryAdapter给选中的条目增加了一些属性动画。
private ScaleAnimation getmGiftGlobalScaleAnimation() {
if (mGlobalGiftScaleAnimation == null) {
mGlobalGiftScaleAnimation = new ScaleAnimation(1, 0.9f, 1, 0.9f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
mGlobalGiftScaleAnimation.setDuration(250);
mGlobalGiftScaleAnimation.setRepeatCount(1);
mGlobalGiftScaleAnimation.setRepeatMode(Animation.REVERSE);
}
return mGlobalGiftScaleAnimation;
}
private AnimatorSet addAnimatorSet(int position, View view) {
if (!animatorSetMap.containsKey(new Integer(position))) {
AnimatorSet set = new AnimatorSet();
final ObjectAnimator oa3 = ObjectAnimator.ofFloat(view, "scaleX", 1, 0.7f);
oa3.setDuration(800);
oa3.setRepeatCount(-1);
oa3.setRepeatMode(ValueAnimator.REVERSE);
final ObjectAnimator oa4 = ObjectAnimator.ofFloat(view, "scaleY", 1, 0.7f);
oa4.setDuration(800);
oa4.setRepeatCount(-1);
oa4.setRepeatMode(ValueAnimator.REVERSE);
//设置一起飞
set.playTogether(oa3, oa4);
set.setStartDelay(300);
animatorSetMap.put(new Integer(position), set);
return set;
} else {
return animatorSetMap.get(new Integer(position));
}
}
接下来就是关键的礼物的播放了,在Android中,如果想要实现酷炫的动画效果,那么实现主要有这几种方式:
- 帧动画:需要把图片带到安装包中,加大了安装包的大小,如果动画很多,这种方案明显不是那么现实,不过可以从网上下载到sd卡,然后播放的时候去加载,这样虽然可以,但下载量也有点大,太麻烦,不是好的选择。
- 属性动画:属性动画实现的效果有限,而且开发代价太大。
- GIF:GIF的性能不是最优选择,而且动画酷炫点GIF也会相应的变大。
- Lottie:Lottie是Airbnb开源的一个面向 iOS、Android、React Native 的动画库,可实现非常复杂的动画,使用也及其简单,极大释放人力,值得一试。目前QQ礼物就是使用了这个。Lottie
- SVGA:这是YY开源的一个动画框架,占用资源少,动画文件大小也比较小,集成很方便,目前虎牙直播等YY系在线上使用。SVGA
经过上面的分析我们可以在礼物中可以使用的方案有两种,要么使用Airbnb的Lottie,要么使用YY的SVGA,这个看个人需求。我不会制作这些动画文件,所以我在这里选择使用的是YY的SVGA,因为YY开源库中提供了很多的动画效果文件,我直接拿来用了。
好,回归主题,监听长按礼物的条目或者直接点击赠送礼物,即可使礼物播放,最终会调用到MainActivity的下面的这个方法进行播放。
/**
* 长按播放礼物
*
* @param giftId
* @param title
* @param strSvga
*/
public void playFrameGift(String giftId, String title, String strSvga) {
loadAnimation(strSvga);
}
private void loadAnimation(String strSvga) {
SVGAParser parser = new SVGAParser(this);
parser.parse(strSvga, new SVGAParser.ParseCompletion() {
@Override
public void onComplete(@NotNull SVGAVideoEntity videoItem) {
svgaGift.setVisibility(View.VISIBLE);
svgaGift.setVideoItem(videoItem);
svgaGift.setLoops(1);
svgaGift.startAnimation();
}
@Override
public void onError() {
}
});
}
svgaGift在MainActivity上的布局如下:
<com.opensource.svgaplayer.SVGAImageView
android:id="@+id/svga_gift"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="match_parent" />
4、一键发送
发送开场白就简单多了,就是一个OneKeySendFragment,点击发送开场白直接把设置的开场白发送出去。
public class OneKeySendFragment extends Fragment {
private RelativeLayout rlOnekeyMsg;
private TextView tvOnekeySendMsg;
private TextView tvOnekeySend;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.chat_onekey_send_fragment, container, false);
rlOnekeyMsg = view.findViewById(R.id.rl_onekeyMsg);
tvOnekeySendMsg = view.findViewById(R.id.tv_onekeySendMsg);
tvOnekeySend = view.findViewById(R.id.tv_onekeySend);
return view;
}
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
tvOnekeySend.setOnTouchListener(Utils.getTouchBackListener(0.9f));
tvOnekeySend.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
((MainActivity) getActivity()).sendOneKeyMsg(tvOnekeySendMsg.getText().toString());
}
});
tvOnekeySendMsg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
promptAnimator();
}
});
rlOnekeyMsg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
promptAnimator();
}
});
}
private void promptAnimator() {
int offset = Utils.Dp2Px(getActivity(), 5);
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(tvOnekeySend, "translationX", offset, -offset, offset, -offset, offset, -offset, 0);
objectAnimator.setDuration(800);
objectAnimator.start();
ToastUtils.showToast(getActivity(), "请点击发送按钮", Toast.LENGTH_SHORT, ToastUtils.DEFEAT);
}
}
5、结束
好了,这篇博客到此为止了,内容也比较多,可能有些没讲好的,或者没讲到的,欢迎指出。如果有需要,最好下载代码跟着博客介绍一起查看比较传送门