学问Chat UI(3)
前言
- 上文学问Chat UI(2)分析了消息适配器的实现;
- 本文主要学习下插件功能如何实现的.并以图片插件功能作为例子详细说明,分析从具体代码入手;
概要
- 分析策略说明
- “+”功能UI布局如何实现?分析整体思路与所用的哪些控件;
- 分析DefaultExtensionModule与PluginAdapter两个类
- 图片插件如何实现?
分析策略
- 1.从融云提供完整的demo,操作“+”按钮,选择图片发送图片消息;
- 2.根据1的操作,寻找对应的控件与事件,理清逻辑;
- 3.从整体把握,看如何实现插件功能;
“+”功能UI布局如何实现
- 从UI看是两个部分:“+”按钮与扩展面板,点击会触发事件,判断扩展面板状态,未显示则显示扩展面板,显示状态则隐藏扩展面板;
- 代码上mPluginToggle对象就是那个"+"按钮,它是ImageView的实例,其中点击会触发
RongExtension.this.setPluginBoard()方法;
this.mPluginToggle.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if(RongExtension.this.mExtensionClickListener != null) {
RongExtension.this.mExtensionClickListener.onPluginToggleClick(v, RongExtension.this);
}
RongExtension.this.setPluginBoard();
}
});
- 前面的是小菜,下面好好品下正菜,说明下后面所有代码中出现的8与0,分别代表Gone(消失)与Visible(可见);
- mPluginAdapter初始化状态与mPluginAdapter中显示状态作为主要判断条件;
private void setPluginBoard() {
if(this.mPluginAdapter.isInitialized()) {
if(this.mPluginAdapter.getVisibility() == 0) {
//省略部分代码
} else {
//省略部分代码
}
}
}
- 看到这里,我们会有疑问,mPluginAdapter是用来干什么的?在解答这个疑问之前,先来看下DefaultExtensionModule类。
DefaultExtensionModule干啥的
- 英文翻译下的意思默认的扩展功能模块,实现了图片,文件,地理位置3个基本插件;
- DefaultExtensionModule实现了IExtensionModule接口,其中要重点讲下onAttachedToExtension,onDetachedFromExtension,getPluginModules方法;
- 1.简单点说,onAttachedToExtension与onDetachedFromExtension负责管理其在RongExtension的生命周期,但是这里有个问题会出现内存泄露;*
public void onAttachedToExtension(RongExtension extension) {
this.mEditText = extension.getInputEditText();
Context context = extension.getContext();
RLog.i(TAG, "attach " + this.stack.size());
//mEditText编辑框存放到stack栈对象中
this.stack.push(this.mEditText);
Resources resources = context.getResources();
try {
this.types = resources.getStringArray(resources.getIdentifier("rc_realtime_support_conversation_types", "array", context.getPackageName()));
} catch (NotFoundException var5) {
;
}
}
//判断栈大小,如果大于0出栈,并返回mEditText
public void onDetachedFromExtension() {
RLog.i(TAG, "detach " + this.stack.size());
if(this.stack.size() > 0) {
this.stack.pop();
this.mEditText = this.stack.size() > 0?(EditText)this.stack.peek():null;
}
}
2.`getPluginModules方法`主要把功能插件对象--实现IPluginModule接口存放到ArrayList中,提供给外部使用;*
- 需要说明的是地理位置在单聊的时候与群聊功能略有不同,单聊多了位置共享的功能,那么怎么区别呢?通过ConversationType参数判断;
- 地理位置功能默认集成的是高德SDK,确定
AMapNetworkLocationClient存在后才会把地理位置插件加到ArrayList;
public List<IPluginModule> getPluginModules(ConversationType conversationType) {
ArrayList pluginModuleList = new ArrayList();
ImagePlugin image = new ImagePlugin();
FilePlugin file = new FilePlugin();
pluginModuleList.add(image);
String e;
Class cls;
try {
//判断高德定位服务类是否存在,存在的话根据ConversationType类型把位置共享插件与我的位置插件添加ArrayList中;
e = "com.amap.api.netlocation.AMapNetworkLocationClient";
cls = Class.forName(e);
if(cls != null) {
CombineLocationPlugin constructor = new CombineLocationPlugin();
DefaultLocationPlugin recognizer = new DefaultLocationPlugin();
boolean typesDefined = false;
if(this.types != null && this.types.length > 0) {
String[] arr$ = this.types;
int len$ = arr$.length;
for(int i$ = 0; i$ < len$; ++i$) {
String type = arr$[i$];
if(conversationType.getName().equals(type)) {
typesDefined = true;
break;
}
}
}
if(typesDefined) {
pluginModuleList.add(constructor);
} else if(this.types == null && conversationType.equals(ConversationType.PRIVATE)) {
pluginModuleList.add(constructor);
} else {
pluginModuleList.add(recognizer);
}
}
} catch (Exception var15) {
RLog.i(TAG, "Not include AMap");
var15.printStackTrace();
}
if(conversationType.equals(ConversationType.GROUP) || conversationType.equals(ConversationType.DISCUSSION) || conversationType.equals(ConversationType.PRIVATE)) {
pluginModuleList.addAll(InternalModuleManager.getInstance().getExternalPlugins(conversationType));
}
pluginModuleList.add(file);
//判断科大讯飞sdk是否存在,存在的话通过反射实例化语音识别插件并加入到ArraryList中
try {
e = "com.iflytek.cloud.SpeechUtility";
cls = Class.forName(e);
if(cls != null) {
cls = Class.forName("io.rong.recognizer.RecognizePlugin");
Constructor var16 = cls.getConstructor(new Class[0]);
IPluginModule var17 = (IPluginModule)var16.newInstance(new Object[0]);
pluginModuleList.add(var17);
}
} catch (Exception var14) {
RLog.i(TAG, "Not include Recognizer");
var14.printStackTrace();
}
return pluginModuleList;
}
关于PluginAdapter
- 继续上面提到的关于PluginAdapter的疑问,首先看下PluginAdapter这个类,代码如下:
- 这里暂时不去关注网格效果实现方式,关注
mInitialized布尔类型值与addPlugins方法 - mInitialized值在bindView被写入为true,说明被初始化了,而addPlugins方法把DefaultExtensionModule的插件集合加到mPluginModules中,并在initView使用到;
public class PluginAdapter {
private static final String TAG = "PluginAdapter";
private List<IPluginModule> mPluginModules = new ArrayList();
private boolean mInitialized;
public PluginAdapter() {
}
public boolean isInitialized() {
return this.mInitialized;
}
//省略部分方法
public void addPlugins(List<IPluginModule> plugins) {
for(int i = 0; plugins != null && i < plugins.size(); ++i) {
this.mPluginModules.add(plugins.get(i));
}
}
//省略部分方法
public void bindView(ViewGroup viewGroup) {
this.mInitialized = true;
this.initView(viewGroup.getContext(), viewGroup);
}
private void initView(Context context, ViewGroup viewGroup) {
//省略部分方法
}
public int getVisibility() {
return this.mPluginPager != null?this.mPluginPager.getVisibility():8;
}
//省略部分代码
}
- 下面从RongExtension看PluginAdapter如何被使用?
1.PluginAdapter在RongExtension的构造函数中被实例化,然后initPlugins方法把插件加到PluginAdapter对象中;
2.接下来,重点分析是上面提到的setPluginBoard方法;长话多说,如果mPluginAdapter(插件适配器)未初始化,先进行初始化;
否则,根据扩展面板是否显示,显示则隐藏键盘与扩展面板,隐藏的话显示扩展面板并隐藏表面面板与键盘;最后要做的是,把语音输入隐藏,mEditTextLayout布局显示;
private void setPluginBoard() {
if(this.mPluginAdapter.isInitialized()) {
if(this.mPluginAdapter.getVisibility() == 0) {
View pager = this.mPluginAdapter.getPager();
if(pager != null) {
pager.setVisibility(pager.getVisibility() == 8?0:8);
} else {
this.mPluginAdapter.setVisibility(8);
this.mContainerLayout.setSelected(true);
this.showInputKeyBoard();
}
} else {
this.mEmoticonToggle.setImageResource(drawable.rc_emotion_toggle_selector);
if(this.isKeyBoardActive()) {
this.getHandler().postDelayed(new Runnable() {
public void run() {
RongExtension.this.mPluginAdapter.setVisibility(0);
}
}, 200L);
} else {
this.mPluginAdapter.setVisibility(0);
}
this.hideInputKeyBoard();
this.hideEmoticonBoard();
this.mContainerLayout.setSelected(false);
}
} else {
this.mEmoticonToggle.setImageResource(drawable.rc_emotion_toggle_selector);
this.mPluginAdapter.bindView(this);
this.mPluginAdapter.setVisibility(0);
this.mContainerLayout.setSelected(false);
this.hideInputKeyBoard();
this.hideEmoticonBoard();
}
this.hideVoiceInputToggle();
this.mEditTextLayout.setVisibility(0);
}
图片插件如何实现?
- 前面的内容为后面理解图片插件的实现提供了铺垫,上面的
getPluginModules方法提到的ImagePlugin类是讲解的重点; - 在看ImagePlugin之前先来看下DefaultExtensionModule中的插件如何与PluginAdapter关联起来的?
DefaultExtensionModule中的插件如何与PluginAdapter关联
1 1.点击“+”的时候插件功能已经可以使用了,那么说明在聊天界面渲染之前插件已经被建立起来,很容易,想到初始化聊天IM服务是最好的时机;
//调用RongIM的public静态init方法,参数呢是实例化的DefaultExtensionModule
RongExtensionManager.getInstance().registerExtensionModule(new DefaultExtensionModule());
- RongExtensionManager中有一个List对象,简单点说,调用registerExtensionModule就是把对象加入到List中;
public void registerExtensionModule(IExtensionModule extensionModule) {
if(mExtModules == null) {
RLog.e("RongExtensionManager", "Not init in the main process.");
} else if(extensionModule != null && !mExtModules.contains(extensionModule)) {
RLog.i("RongExtensionManager", "registerExtensionModule " + extensionModule.getClass().getSimpleName());
if(mExtModules.size() <= 0 || !((IExtensionModule)mExtModules.get(0)).getClass().getCanonicalName().equals("com.jrmf360.rylib.modules.JrmfExtensionModule") && !((IExtensionModule)mExtModules.get(0)).getClass().getCanonicalName().equals("com.melink.bqmmplugin.rc.BQMMExtensionModule")) {
mExtModules.add(extensionModule);
} else {
mExtModules.add(0, extensionModule);
}
extensionModule.onInit(mAppKey);
} else {
RLog.e("RongExtensionManager", "Illegal extensionModule.");
}
}
2 2.再看RongExtension的initData方法,把RongExtensionManager中的List对象赋值给mExtensionModuleList,并实例化了PluginAdapter--插件适配器类
private void initData() {
this.mExtensionModuleList = RongExtensionManager.getInstance().getExtensionModules();
this.mPluginAdapter = new PluginAdapter();//省略若干代码
}
3 3.再看RongExtension的setConversation方法调用this.initPlugins(),当当当的,调用了实例化插件对象的addPlugins把插件加入到其中,从而形成关联;
private void initPlugins() {
Iterator i$ = this.mExtensionModuleList.iterator();
while(i$.hasNext()) {
IExtensionModule module = (IExtensionModule)i$.next();
List pluginModules = module.getPluginModules(this.mConversationType);
if(pluginModules != null && this.mPluginAdapter != null) {
this.mPluginAdapter.addPlugins(pluginModules);
}
}
}
ImagePlugin
- ImagePlugin实现了IPluginModule接口,总共四个方法,代码如下:
public interface IPluginModule {
Drawable obtainDrawable(Context var1);
String obtainTitle(Context var1);
void onClick(Fragment var1, EditExtension var2);
void onActivityResult(int var1, int var2, Intent var3);
}
- 看到这里你可能对四个方法是干什么产生疑问?别着急,欲知此事,请往下阅读;
- 贴上ImagePlugin的具体代码,这里看具体实现的代码,请看代码中注释;
public class ImagePlugin implements IPluginModule {
ConversationType conversationType;
String targetId;
public ImagePlugin() {
}
//item的背景图片
public Drawable obtainDrawable(Context context) {
return ContextCompat.getDrawable(context, R.drawable.rc_ext_plugin_image_selector);
}
//item的插件标题
public String obtainTitle(Context context) {
return context.getString(R.string.rc_plugin_image);
}
//item点击事件
public void onClick(Fragment currentFragment, EditExtension extension) {
String[] permissions = new String[]{"android.permission.READ_EXTERNAL_STORAGE"};
//这里考虑android6.0权限变更,不仅需要声明权限,而且敏感权限需要允许时申请
if(PermissionCheckUtil.requestPermissions(currentFragment, permissions)) {
this.conversationType = extension.getConversationType();
this.targetId = extension.getTargetId();
Intent intent = new Intent(currentFragment.getActivity(), PictureSelectorActivity.class);
//回调Fragment 中的onActivityResult
extension.startActivityForPluginResult(intent, 23, this);
}
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
}
- 下面看下ConversationFragment选完图片以后回调如何进行?
- 首先对requestCode做了判断,如果不是102则回调了mRongExtension对象的onActivityPluginResult方法,然后根据请求代码分析是哪个插件回调回来的,在调用IExtensionClickListener接口对应的方法;
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if(requestCode == 102) {
this.getActivity().finish();
} else {
this.mRongExtension.onActivityPluginResult(requestCode, resultCode, data);
}
}
- 这里有几个问题?onActivityResult可不可以直接处理?requestCode 的如何作用?
1 第一个问题,可以进行直接数据处理,但是需要约定好requestCode,如果通过融云回调的话不需要约定;
2 第二个问题,单独一个int类型的值容纳得信息有限,做过处理的就与众不同了,融云的方法是把后8位作为requestCode,前24位作为postion,为何要+1不是很懂,有知道请再评论中指出;
this.mFragment.startActivityForResult(intent, (position + 1 << 8) + (requestCode & 255));
上述代码完成以后可以通过回调ConversationFragment实现的`this.mExtensionClickListener.onImageResult(list, lat1);`方法发送图片消息了,代码就不贴了;
总结
- 插件实现通过接口方式,耦合度降低,扩展性好;
- 添加插件时,无需大改RongExtension代码只要实现IPluginModule接口并注册到实现IExtensionModule的插件模块中,并在初始化RongIM时注册插件模块;
- 考虑功能的时候需要考虑到兼容性,扩展性;
学问Chat UI(3)的更多相关文章
- 学问Chat UI(1)
前言 由于项目需要,最近开始借鉴学习下开源的Android即时通信聊天UI框架,为此结合市面上加上本项目需求列了ChatUI要实现的基本功能与扩展功能. 融云聊天UI-Android SDK 2.8. ...
- 学问Chat UI(2)
前言 上文讲了下要去做哪些事,重点分析了融云Sdk中RongExtension这个扩展控件,本文来学习下同样是融云Sdk中的AutoRefreshListView如何适配多种消息的实现方式,写的有不足 ...
- 学问Chat UI(4)
前言 写这个组件是在几个月前,那时候是因为老大讲RN项目APP的通讯聊天部分后面有可能自己实现,让我那时候尝试着搞下Android通讯聊天UI实现的部分,在这期间,找了不少的Android原生项目:蘑 ...
- 77.Android之代码混淆
转载:http://www.jianshu.com/p/7436a1a32891 简介 作为Android开发者,如果你不想开源你的应用,那么在应用发布前,就需要对代码进行混淆处理,从而让我们代码即使 ...
- 【SignalR学习系列】5. SignalR WPF程序
首先创建 WPF Server 端,新建一个 WPF 项目 安装 Nuget 包 替换 MainWindows 的Xaml代码 <Window x:Class="WPFServer.M ...
- 如何用ABP框架快速完成项目(8) - 用ABP一个人快速完成项目(4) - 能自动化就不要手动 - 使用自动化测试(BDD/TDD)
做为一个程序员, 深深知道计算机自动化的速度是比人手动的速度快的, 所以”快速”完成项目的一个重要武器就是: 能自动化就不要手动. BDD/TDD有很多优势, 其中之一就是自动化, 我们这节文章先 ...
- Android: apk反编译 及 AS代码混淆防反编译
一.工具下载: 1.apktool(资源文件获取,如提取出图片文件和布局文件) 反编译apk:apktool d file.apk –o path 回编译apk:apktool b path –o f ...
- 带你彻底明白 Android Studio 打包混淆
前言 在使用Android Studio混淆打包时,该IDE自身集成了Java语言的ProGuard作为压缩,优化和混淆工具,配合Gradle构建工具使用很简单.只需要在工程应用目录的gradle文件 ...
- “四核”驱动的“三维”导航 -- 淘宝新UI(需求分析篇)
前言 孔子说:"软件是对客观世界的抽象". 首先声明,这里的"三维导航"和地图没一毛钱关系,"四核驱动"和硬件也没关系,而是为了复杂的应用而 ...
随机推荐
- neo4j 数据库导入导出
工作中需要将 A 图数据库的数据完全导出,并插入到 B 图数据库中.查找资料,好多都是通过导入,导出 CSV 文件来实现.然而,经过仔细研究发现,导出的节点/关系 都带有 id 属性 ,因为 A B ...
- Java之面向对象概述,类,构造方法,static,主方法,对象
一.面向对象概述 面向过程 "面向过程"(Procedure Oriented)是一种以过程为中心的编程思想.这些都是以什么正在发生为主要目标进行编程,不同于面向对象的是谁在受影响 ...
- 通过添加filter过滤器 彻底解决ajax 跨域问题
1.在web.xml添加filter <filter> <filter-name>contextfilter</filter-name> <filter-cl ...
- 【转载】CANoe 入门 Step by step系列(三)简单例子的剖析
来源:http://www.cnblogs.com/dongdonghuihui/archive/2012/09/26/2704623.html 最好的学习方式是什么?模仿.有人会问,那不是山寨么?但 ...
- [补] windows C socket编程——大物实验预约
注 : 心血来潮,想着把这两年没能记录下来的经历,写一波回忆杀.诚然,有些经历十分复杂繁琐,希望能耐下性子,写出好文章来,可惜一时不能全想起来这两年来的种种,就想起来什么便写什么吧. 时间估摸着是大一 ...
- python爬虫从入门到放弃(二)之爬虫的原理
在上文中我们说了:爬虫就是请求网站并提取数据的自动化程序.其中请求,提取,自动化是爬虫的关键!下面我们分析爬虫的基本流程 爬虫的基本流程 发起请求通过HTTP库向目标站点发起请求,也就是发送一个Req ...
- (转)java 多线程 CountDownLatch用法
CountDownLatch,一个同步辅助类,在完成一组正在其他线程中执行的操作之前,它允许一个或多个线程一直等待. 主要方法 public CountDownLatch(int count); pu ...
- (转)CentOS6.5下Redis安装与配置
场景:项目开发中需要用到redis,之前自己对于缓存这块一直不是很理解,所以一直有从头做起的想法. 本文详细介绍redis单机单实例安装与配置,服务及开机自启动.如有不对的地方,欢迎大家拍砖o(∩_∩ ...
- ubuntu下处理mysql无法启动故障一例
故障现象: mysql无法启动 1: dmesg |grep mysql [101353.820000] init: mysql post-start process (9077) terminate ...
- iOS源码博文集锦2
iOS精选源码 快速集成观看直播和开播 一款类携程商旅的城市选择界面 一个类似于QQ电话的动画效果 高德地图定位,导航,轨迹,GPS纠偏 真实逻辑滚动数字DPScrollNumberL ...