今日科技快讯

哈啰顺风车近日在上海、广州、杭州、成都、合肥、东莞6座城市上线试运营。上述城市用户通过更新后的“哈啰出行”APP,可使用市内或跨城顺风车服务。猎云网在其中三座城市体验了下哈啰顺风车,结果——打车5次,失败4次。

作者简介

大家早上好,春节将至,很多朋友现在可能都已经放假回家了,还在上班的朋友也要记得守好最后一班岗。

本篇来自 hard_Rui 的投稿文章,分享了自己自定义的聊天界面,希望对大家有所帮助!

https://blog.csdn.net/qq_28931623

前言

最近因为项目中用到了IM聊天的功能,由于项目中并不准备集成第三方的sdk ,所以就自己写了一个ui界面来实现消息发送接收。大家如果需要的话直接移到自己的项目中就行,先展示一下实现的效果,然后再简单介绍一下怎么实现的:

整体布局

布局分三部分,聊天列表 ,输入框所在布局,底部表情和其他消息选择所在的布局

1. 聊天列表

这里是SwipeRefreshLayout和RecyclerView,我在这里用谷歌官方的下拉刷新控件SwipeRefreshLayout来实现下拉刷新获取历史消息,如果希望其他的样式更换下拉刷新控件就行。

2. 输入框和表情和更多选择所在布局

这里主要有几个部分,语音按钮,表情按钮,加号按钮,输入框,发送按钮,语音按住按钮,表情布局,更多布局,键盘,我们在代码里控制好这些控件的显示和隐藏即可。我这里就以点击加号按钮为例:

    //绑定底部加号按钮    public ChatUiHelper bindToAddButton(View addButton) {        mAddButton = addButton;        addButton.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                 mEditText.clearFocus();                 //隐藏长按发送语音按钮                 hideAudioButton();                  //表情和更多所在布局显示                 if (mBottomLayout.isShown()){                 //更多所在布局显示                  if (mAddLayout.isShown()){                     //显示软件盘时,锁定内容高度,防止跳闪。                      lockContentHeight();                      //隐藏更多布局,显示软件盘                      hideBottomLayout(true);                      //软件盘显示后,释放内容高度                      unlockContentHeightDelayed();                  }else{                      //显示更多布局                      showMoreLayout();                      //隐藏表情布局                      hideEmotionLayout();                  }              }else{                 //更多所在布局没显示,键盘显示                  if (isSoftInputShown()) {                      hideEmotionLayout();                      showMoreLayout();                      lockContentHeight();                      showBottomLayout();                      unlockContentHeightDelayed();                  } else {                      //表情,更多所在布局,键盘没显示,直接显示更多布局                       showMoreLayout();                       hideEmotionLayout();                       showBottomLayout();                  }              }            }        });        return this;    }

3. recycleview不同item所显示的布局:

我写的聊天界面主要有 文本消息,语音消息,图片消息,视频消息,文件消息这几种布局。改动的话直接到对应的item布局中修改即可。对于多布局的显示我用的BaseRecyclerViewAdapterHelper,一种类型对应一个布局。在发送后根据不同的type判断显示即可, 在发送完网络请求后,更新单个item即可。

private static final int SEND_TEXT = R.layout.item_text_send;private static final int RECEIVE_TEXT = R.layout.item_text_receive;private static final int SEND_IMAGE = R.layout.item_image_send;private static final int RECEIVE_IMAGE = R.layout.item_image_receive;private static final int SEND_VIDEO = R.layout.item_video_send;private static final int RECEIVE_VIDEO = R.layout.item_video_receive;private static final int SEND_FILE = R.layout.item_file_send;private static final int RECEIVE_FILE = R.layout.item_file_receive;private static final int RECEIVE_AUDIO = R.layout.item_audio_receive;private static final int SEND_AUDIO = R.layout.item_audio_send;

表情功能实现

对于表情,看上去是一个图片,但传输的时候其实就是一个文本。对于不同的应用像微博里表情就是"[表情名字]"的接口,比如可爱的表情就是[可爱],QQ表情就是一个 "/表情字母"的结构,比如害羞的表情就是/hx。

像这些文本显示成图片,我们可以用SpannableString,SpannableString就是显示字符串的时候,根据字符串中包含的拓展字段显示相应的效果,像字体,颜色之类的。如果显示成图片,大概原理就是 先通过正则,由字符串得到对应匹配的表情图片,然后使用ImageSpan,把Bitmap设置到SpannableString中,这样显示的时候显示SpannableString但展示出来的就是一个图片。如果自己对应不同的规则android端与ios端需要保持一致,如果android匹配到不同的图片显示出来而ios没有就尴尬了。

这里我使用的是Unicode码, Unicode码就是对于每个字符规定一个数字用来表示该字符,每个Emoji 都有自己对应的Unicode码点,当我们把对应的Unicode码点转化为字符时,它会被渲染为图片显示.例如一个笑脸:new String(Character.toChars(128512)) 这样我们可以把Unicode码点转化为字符,然后渲染为笑脸的表示。在这个网站上http://unicode.org/emoji/charts/full-emoji-list.html#1f600我们可以找到不同的表情对应的不同的Unicode码,我这里的表情实现则是先建立数据库,在数据库中存储不同表情的Unicode码,然后再用Textview显示出来就展现的是现在的效果。

说一下代码具体的实现,对于表情键盘的实现:1.表情键盘实际上就是viewpager加一个指示器,在viewpager的每一页我们添加一个recycleview,然后通过GridLayoutManager显示出表情, 对于每一页的recycleview的adapter的数据源则是n*pagesize到(n+1)*pagesize 2.对于表情的删除键:我先从数据库中取出所有的表情对象,我设置一页显示21个表情字符,根据所有表情数目和每页显示数目算出总页数,这就是需要显示的删除键的个数。然后在list的21,42,63的位置添加一个新的emoji对象,在adapter中判断如果是新的emoji对象,我们就显示删除键。这样每一页最后一位就是删除键了,下面是具体的代码:

ChatUiHelper bindEmojiData(){    //获取到所有表情     mListEmoji = EmojiDao.getInstance().getEmojiBean();     //表情底部滑动的viewpager和指示器所在布局     LinearLayout  homeEmoji = (LinearLayout)mActivity.findViewById(R.id.home_emoji);     //表情底部滑动的viewpager     ViewPager vpEmoji = (ViewPager) mActivity.findViewById(R.id.vp_emoji);     //指示器     final IndicatorView indEmoji = (IndicatorView) mActivity.findViewById(R.id.ind_emoji);     LayoutInflater inflater = LayoutInflater.from(mActivity);     //每一页显示的数量     int pageSize = EVERY_PAGE_SIZE;     //创建删除表情    EmojiBean mEmojiBean=new EmojiBean();    mEmojiBean.setId(0);    mEmojiBean.setUnicodeInt(000);    //删除键的数量    int deleteCount= (int) Math.ceil(mListEmoji.size() * 1.0/EVERY_PAGE_SIZE);//要显示的删除键的数量    LogUtil.d(""+deleteCount);    //添加删除键    for (int i=1;i<deleteCount+1;i++){        if(i==deleteCount){            mListEmoji.add( mListEmoji.size(),mEmojiBean);        }else{            mListEmoji.add(i*EVERY_PAGE_SIZE-1,mEmojiBean);        }        LogUtil.d("添加次数"+i);    }    //计算出总页数    int pageCount = (int) Math.ceil((mListEmoji.size()) * 1.0 / pageSize);//一共的页数    LogUtil.d("总共的页数:"+pageCount);    //每个页面创建一个recycleview    List<View> viewList = new ArrayList<View>();    for (int index = 0; index < pageCount; index++) {        RecyclerView recyclerView = (RecyclerView) inflater.inflate(R.layout.item_emoji_vprecy, vpEmoji, false);        recyclerView.setLayoutManager( new GridLayoutManager(mActivity, 7));        EmojiAdapter entranceAdapter;       if (index==pageCount-1){           //如果最后一页传入adapter的数据           List<EmojiBean> lastPageList=mListEmoji.subList(index*EVERY_PAGE_SIZE,mListEmoji.size());           entranceAdapter = new EmojiAdapter( lastPageList  , index, EVERY_PAGE_SIZE);       } else {           //其他页数页传入adapter的数据            entranceAdapter = new EmojiAdapter( mListEmoji.subList(index*EVERY_PAGE_SIZE, (index+1)*EVERY_PAGE_SIZE), index, EVERY_PAGE_SIZE);        }        //表情的点击事件         entranceAdapter.setOnItemClickListener(new BaseQuickAdapter.OnItemClickListener() {            @Override            public void onItemClick(BaseQuickAdapter adapter, View view, int position) {                 EmojiBean mEmojiBean=(EmojiBean)adapter.getData().get(position);                if (mEmojiBean.getId()==0){                    //如果是删除键                    mEditText.dispatchKeyEvent(new KeyEvent(                            KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL));                }else{                   mEditText.append(((EmojiBean)adapter.getData().get(position)).getUnicodeInt());                }          }        });        //为每个recycleview添加数据源        recyclerView.setAdapter(entranceAdapter);        viewList.add(recyclerView);    }    //设置viewpager和指示器    EmojiVpAdapter adapter = new EmojiVpAdapter(viewList);    vpEmoji.setAdapter(adapter);    indEmoji.setIndicatorCount(vpEmoji.getAdapter().getCount());    indEmoji.setCurrentIndicator(vpEmoji.getCurrentItem());    vpEmoji.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {        @Override        public void onPageSelected(int position) {            indEmoji.setCurrentIndicator(position);        }    });    return this;}

录音功能

通过一个RecordButton,在这个button里执行一些显示触摸的逻辑。主要分为以下几部分:

1. 触摸事件:

主要对button的onTouchEvent事件进行处理,在监测到MotionEvent.ACTION_DOWN时弹出录音dialog 在ACTION_UP和ACTION_CANCEL时则停止录音

public boolean onTouchEvent(MotionEvent event) {    int action = event.getAction();    y = event.getY();    if(mStateTV!=null && mStateIV!=null &&y<0){        mStateTV.setText("松开手指,取消发送");        mStateIV.setImageDrawable(getResources().getDrawable(R.drawable.ic_volume_cancel));    }else if(mStateTV != null){        mStateTV.setText("手指上滑,取消发送");    }    switch (action) {        case MotionEvent.ACTION_DOWN:            setText("松开发送");            initDialogAndStartRecord();            break;        case MotionEvent.ACTION_UP:        case MotionEvent.ACTION_CANCEL:            this.setText("按住录音");             if(y>=0 && (System.currentTimeMillis() - startTime <= MAX_INTERVAL_TIME)){                 finishRecord();            }else if(y<0){                  cancelRecord();            }            break;    }

    return true;}

2. dialog的显示

这部分主要涉及动画的显示和不同状态图片的替换。初始化Dialog的时候,则是为ImageViews设置ImageDrawable来开启动画,然后根据不同的操作,dialog中间显示不同的图片,这部分没什么难度

 mStateIV.setImageDrawable(getResources().getDrawable(R.drawable.anim_mic)); anim = (AnimationDrawable) mStateIV.getDrawable(); anim.start(); ----------------------------------- mStateIV.setImageDrawable(getResources().getDrawable(R.drawable.ic_volume_wraning)); mStateTV.setText("录音时间太短"); anim.stop();

3. 录音功能

录音通过MediaRecorder这个类,在Android中我们可以通过MediaRecorder来录制音频,分为下面几步:

1、创建MediaRecorder实例对象。

2、setAudioSource(int source)方法设置声音,里面的参数source设置为MediaRecorder.AudioSource.MIC,这个参数指定录音来源为主麦克风。

3、调用 setOutputFormat(int output_format) 设置在录制过程中产生的输出文件的格式

4、设置编码格式MediaRecord.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);//设置音频编码为amr_nb。

5、设置置录制音频文件的保存位置,通过调用MediaRecorder对象的setOutputFile(String path)方法,path传输出路径即可。

6、调用MediaRecorder的prepare()方法准备录制。

7、调用MediaRecorder对象的start()方法开始录制。

8、录制完成,调用MediaRecorder对象的stop()方法停止录制,并调用release()方法释放资源。

录完的文件我保存在data/data/包名/files目录下。

private void startRecording() {    if (mRecorder != null) {        mRecorder.reset();    } else {        mRecorder = new MediaRecorder();    }    mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);    mRecorder.setOutputFormat(MediaRecorder.OutputFormat.DEFAULT);    mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);    File file = new File(mFile);    LogUtil.d("创建文件的路径:"+mFile);     mRecorder.setOutputFile(mFile);     try {        mRecorder.prepare();        mRecorder.start();    }catch (Exception e){        LogUtil.d("preparestart异常,重新开始录音:"+e.toString());         e.printStackTrace();        mRecorder.release();        mRecorder = null ;        startRecording();    }}

在recycleview中点击播放,我们通过MediaPlayer,同时并播放item的动画即可。当同时播放多个的时候根据在点击的时候通过判断变量是否为空来取消其他的播放。对于播放音频MediaPlayer的初始化与MediaRecorder的类似,具体的下面的代码展示了出来:

//item点击播放mAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {    @Override    public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {        if (ivAudio != null) {            ivAudio.setBackgroundResource(R.mipmap.audio_animation_list_right_3);            ivAudio = null;            MediaManager.reset();        }else{             ivAudio = view.findViewById(R.id.ivAudio);              MediaManager.reset();              ivAudio.setBackgroundResource(R.drawable.audio_animation_right_list);             AnimationDrawable  drawable = (AnimationDrawable) ivAudio.getBackground();            drawable.start();             MediaManager.playSound(ChatActivity.this,((AudioMsgBody)mAdapter.getData().get(position).getBody()).getLocalPath(), new MediaPlayer.OnCompletionListener() {                @Override                public void onCompletion(MediaPlayer mp) {                    LogUtil.d("开始播放结束");                    ivAudio.setBackgroundResource(R.mipmap.audio_animation_list_right_3);                    MediaManager.release();                 }            });        }    }});----------------------------------------------------------------//播放音频public static void playSound(Context context ,String filePathString,                             OnCompletionListener onCompletionListener) {    try {        filepathstrings = filePathString ;        mPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);        mPlayer.setOnCompletionListener(onCompletionListener);        mPlayer.setDataSource(filePathString);        mPlayer.setVolume(90,90);        mPlayer.setLooping(false);        mPlayer.prepare();        mPlayer.start();        isStart = true;    } catch (IllegalArgumentException e) {        e.printStackTrace();    } catch (SecurityException e) {        e.printStackTrace();    } catch (IllegalStateException e) {        e.printStackTrace();    } catch (IOException e) {        e.printStackTrace();    }}

更多布局功能

这里比较简单,就是一个线性布局,然后对于底下布局不同模块的点击事件分别处理, 图片视频文件选择,我使用PictureSelector和MaterialFilePicker,然后在onactivity中回调后再发送消息,然后在recycleview根据不同消息的不同类型来显示出对用的布局即可。

代码里面的部分ui切图是从融云的sealtalk中移植过来的:

https://github.com/sealtalk/sealtalk-android

图片和视频选择我用的PictureSelector:

https://github.com/LuckSiege/PictureSelector

文件的选择我用的MaterialFilePicker:

https://github.com/nbsp-team/MaterialFilePicker

多布局显示的BaseRecyclerViewAdapterHelper

https://github.com/CymChad/BaseRecyclerViewAdapterHelper

最后附带上github地址,需要的话可以直接去下载:

https://github.com/huangruiLearn/HRLChatUi

Android自定义IM聊天界面相关推荐

  1. android 仿微信聊天界面 以及语音录制功能,Android仿微信录制语音功能

    本文实例为大家分享了Android仿微信录制语音的具体代码,供大家参考,具体内容如下 前言 我把录音分成了两部分 1.UI界面,弹窗读秒 2.一个类(包含开始.停止.创建文件名功能) 第一部分 由于6 ...

  2. Android仿微信聊天界面

    今天说说android的仿微信聊天界面,我只想说两个字:坑爹 项目已经传到了github: https://github.com/hebiao6446/Hantu-android- 还好我写过iOS仿 ...

  3. android模拟微信聊天功能,android仿微信聊天界面 语音录制功能

    本例为模仿微信聊天界面UI设计,文字发送以及语言录制UI. 1先看效果图: 第一:chat.xml设计 android:layout_width="fill_parent" and ...

  4. android高仿微信下拉有页面,Android——(仿微信聊天界面布局实例)

    今天看郭霖<第一行代码>书上写了一个聊天窗体的小例子,自己就练习学了一下.加上一些自己的理解整理了一下. 1.第一步首先是制作9.patch图片,这个在android  sdk 目录下to ...

  5. Android仿微信聊天界面布局

         在Android开发中,很多时候都需要接入即时通信功能,那么就需要一个聊天的布局界面,下面就来给大家介绍一下,怎么来布局聊天界面. 1.第一步首先是制作9.patch图片,这个在Androi ...

  6. Android 仿qq聊天界面之一

    一.登录界面 本来是只想仿一个qq的聊天界面的,顺便做了一个登录界面,熟悉下SharedPreferences(解释一下:SharedPreferences由于非常适合记录一些零散的简单的数据,因此登 ...

  7. Android 自定义拨打电话界面

    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- ...

  8. android环信聊天界面上面显示昵称,【环信征文】在android中5分钟实现环信昵称头像的显示...

    老司机带你们5分钟实现昵称头像的显示,车要开了,话不多说,快快上车~ 一.将简版demo里的cache包(5个java文件)复制到自己项目里. 下载环信android简版Demo: 环信Android ...

  9. android写qq聊天界面,30分钟手动实现QQ聊天界面

    先上效果图: giphy.gif 界面布局设计: image.png activity_main.xml布局: xmlns:android="http://schemas.android.c ...

最新文章

  1. oracle基础-基本的查询,以及pl/sql登录
  2. 高等数学同济第七版课后答案下册
  3. 在JavaScript中使用json.js:访问JSON编码的某个值
  4. PowerDesigner如何将物理模型转为对象模型,将对象模型转生成Java类
  5. OpenCV运行ReID网络的实例(附完整代码)
  6. 用好这几个工具,能大幅提升你的 Git/GitHub 操作效率!
  7. [转] MemCached 的 stats 命令
  8. 自己总结的sql基本操作
  9. 物联网系统开发如何选择时序数据库
  10. javascript的发展(周边插件的由来)
  11. 2017 前端大事件和趋势回顾,2018 何去何从?
  12. python正则_python中正则匹配
  13. 土豆服务器延期修复,DNF更新推迟, 土豆服务器发霉了?
  14. Android Fragment应用实战,使用碎片向ActivityGroup说再见
  15. greensock下载_使用GreenSock构建可拖动的画布外菜单
  16. win10好看的锁屏壁纸如何保存
  17. 【数据处理】——利用Excel VBA批量将详细地址转换成省市区三级行政区划
  18. opengl 库函数 glew glfw glad glut gl glu freeglut
  19. PMP培训第一次听课笔记(第1-3章)
  20. idea自动删除尾行空格(空行中空格)

热门文章

  1. Python深度学习之处理文本数据
  2. 单个正态总体参数的区间估计、两个正态总体参数的区间估计 Matlab实现
  3. liferay的控制条docbar消失解决方法
  4. 京东到家程序员离职当天删库跑路
  5. Pytesseract图片识别结果箭头符号去除
  6. VLC-Android音频播放不完整问题踏坑
  7. 解决VUE [WDS] DISCONNECTED 错误
  8. 电脑重装系统,微信备份与恢复聊天记录,保存的文件。微信聊天记录迁移
  9. 人工智能-数据分析-鸢尾花
  10. 基于 locust/boomer 为核心的简单 http 接口分布式性能测试工具