上一篇讲了主界面,里面包含了个人动态、联系人列表、会话列表,没有阅读的话建议看看FanChat学习笔记(四)——主页面。本文接着说一说添加好友与聊天界面的逻辑及一些重要的知识点。
- 添加好友——AddFriendActivity用到的知识点
- 添加好友——AddFriendActivity界面逻辑
- 聊天——ChatActivity用到的知识点
- 聊天——ChatActivity界面逻辑
1、添加好友——AddFriendActivity用到的知识点:
setHasFixedSize(boolean hasFixedSize)
RecycleView的这个方法的目的是RecycleView的ItemView宽高保持固定,不管Item增加或者减少,ItemView的宽高都不会变化。如果没有设置为false的话,RecyclerView会需要额外计算每个item的尺寸,而调用onMesure方法。
EventBus 3.0的使用
其实这个才是今天的重点,甚至是整个项目的重点,因为我研究这个项目就是为了学习EventBus 3.0在项目中的实际运用。在研究该项目之前,也曾翻过弘神的博客Android EventBus源码解析 带你深入理解EventBus。因为我记得当初我面试现在这份工作的时候,老板说:你们只知道那些东西怎么用,但是不只是为什么这么用?知其然不知其所以然!
所以,我们先来看看这个EventBus 3.0如何使用,以及其原理是怎么回事?
其实Github上面已经介绍了一遍,那么我尽量介绍得和上面不一样吧!
首先依赖,这个是没有办法不一样的!(因为我已经开始学习AndroidStudio了,所以Eclipse就不讨论了!)
compile 'org.greenrobot:eventbus:3.0.0'
接下来我们就可以注册了,注册的作用在后面再解释!
EventBus.getDefault().register(this);
注册了是不是就可以注销了?这就好像李白清的段子,英语刚刚说了一句“Hello”马上来一句“Goodbye”一样。所以我们不能闹这样的笑话!
那么我们不结束还能说什么啊?英文是一句都不会啊!!既然不会,我们就去自定义一个事件,就是我们要消息数据,这个就是我们要表达的内容!!
package com.itheima.leon.qqdemo.event;
/**
* 创建者: Leon
* 创建时间: 2016/10/19 20:26
* 描述: EventBus事件
*/
public class AddFriendEvent {
public static final String TAG = "AddFriendEvent";
private String mFriendName;
private String mReason;
public AddFriendEvent(String friendName, String reason) {
this.mFriendName = friendName;
this.mReason = reason;
}
public String getFriendName() {
return mFriendName;
}
public void setFriendName(String friendName) {
this.mFriendName = friendName;
}
public String getReason() {
return mReason;
}
public void setReason(String reason) {
this.mReason = reason;
}
}
这个类需要讲一下,EventBus的事件类型是Object,也就是说我们可以传递任意数据。这一点我们在下一个方法后来证实。因此,我们这个类可以只有属性没有get/set方法,类里面的数据类型以及相应的方法根据自己的需要构造。比如AddFriendEvent是按照标准实体来构建的,所有的属性都是私有的,然后提供公共方法来设置相应属性,而下面我们需要到这些数据的时候就可以通过方法来调用。
OK,我们知道说什么话了,那么我们开始对话吧?
@Subscribe(threadMode = ThreadMode.MainThread, sticky = true, priority = 100)
public void deleteFriend(AddFriendEvent event){
Log.d(TAG, "deleteFriend: "+event.getFriendName());
}
这里相当于收到了一句话,但是这句话就像是一个成语,简单一个方法却囊括了很多内容,我们一点一点来剖析。方法大家都明白是什么意思?不明白也没有关系,只要知道我们通过EventBus发布的消息会通过这个方法来实现,这个方法类似于回调。就是我们在其它地方发布消息,然后这个方法就会执行。接下来我们就说一下注解吧!
threadMode
这个参数是表示这个方法应该在什么线程运行?属于必选参数,可选线程有四种,如下:
ThreadMode.PostThread
在发布消息所在的线程执行ThreadMode.MainThread
在主线程(UI线程)执行BackgroundThread
在子线程(后台线程)执行ThreadMode.Async
必须在子线程执行(强制在后台线程)执行
sticky
这个参数默认为false,意思是deleteFriend方法需要先注册(register), 再发布消息,才能接受到事件; 如果使用post(Object event)方法发布消息的话该参数完全没有意义。它的意义在于使用postSticky(Object event)发布消息时, 参数如果为true,那么可以不需要先注册, 也能接受到事件.如果参数为false的话,不管什么方法发布消息都得先注册才能接受到事件。
priority
这个参数代表优先级,属于可选参数,默认值是0。概念和有序广播的接受顺序类似,即优先级越高,会优先被调用。也仅仅如此而已。比如我们还有一个同类型参数的方法,如下,
@Subscribe(threadMode = ThreadMode.BACKGROUND,sticky = true,priority = 90)
public void addFriend(AddFriendEvent event) {
Log.d(TAG, "addFriend: ");
}
那么我们也仅仅是先执行当前这个deleteFriend方法,再执行addFriend方法,而并不能像有序广播那样我在优先的接受者(订阅者)这里可以通过abortBroadcast()方法终止传播,这个在EventBus里面是做不到的。另外,如果我这个接受者只有一个,但是我把级别调至最高(经测试,该参数类型为Int类型,所以最大值为2147483647,当然使用Int的最小值-2147483648也不会报错)也并不会有任何异常?最后提一点,这个参数不受发布消息的方法影响,两种方法优先级都是有效的!
接下来可以来研究实现的方法了,埋上面说的坑——“EventBus的事件类型是Object”。这里我们可以来研究一下发布消息的两种方式:
void postSticky(Object event)
void post(Object event)
看见上面的方法就知道了,不管我们使用何种方式发布消息,都可以传输Object数据给接收方(订阅者)。其实看postSticky(Object event)的源码就知道,event其实是保存了两次:
/**
* Posts the given event to the event bus and holds on to the event (because it is sticky). The most recent sticky
* event of an event's type is kept in memory for future access by subscribers using {@link Subscribe#sticky()}.
*/
public void postSticky(Object event) {
synchronized (stickyEvents) {
stickyEvents.put(event.getClass(), event);
}
// Should be posted after it is putted, in case the subscriber wants to remove immediately
post(event);
}
代码中前者在没有注册的时候就可以通知到接收方,也就是说接收方(订阅者)sticky参数为true的时候就可以在EventBus未注册的时候执行了。而后者就不行,它需要在注册的时候告诉EventBus当前类里面有几个接收方(订阅者)在等待接收消息,注册实现的原理是将当前的类class作为key,将接收方法的参数作为value,然后以键对值的形式用Map来保存的。当用post方法发布消息时就遍历Map,根据参数类型通知到对应的订阅者。所以这里需要注意两个问题,如果两个订阅者的参数类型相同的话,那么它会通知到每一个订阅者,也就是说每一个订阅者方法都会执行。其次,如果在不同的类需要订阅(接收消息)的话,那么就需要在不同的类注册EventBus。
因此,postSticky方法的原理应该是遍历所有类,然后找到对应的订阅方法并判断其sticky参数是否为true,根据其值决定是否通知订阅者。当然,这个只是我的猜想。
在上面我们通过注册来说“Hello”,然后通过post(Object event)方法(或者是postSticky(Object event))方法来说话(发布消息),通过注解的方式来倾听人家的对话(接收人家的消息),交谈完毕之后就应该说“Goodbye”了。那么我们应该通过什么方式来说呢?
EventBus.getDefault().unregister(this);
上面我们说过EventBus注册的原理,那么我们在当前类关闭的时候就应该将保存的class给清除掉,避免内存泄漏。所以我们应该在注册的当前类的时候,考虑什么时候应该注销掉,假如我们需要在Activity里使用EventBus订阅消息的话,那么我们可以在onCreate方法里面注册,在onDestory方法里面注销。说到内存泄漏我想到了内存泄漏和内存溢出的区别,如下:
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
OK,上面讲完了使用EventBus来收发数据沟通,如果说得还是觉得肤浅的话,可以下载项目看看项目里具体是怎么使用的。
2. 添加好友——AddFriendActivity界面逻辑:
在聊逻辑之前,先看看具体界面,否则会感觉很抽象,所以先上图:
首先,当我们输入搜索的关键字以后,然后点击图片中的任意一个放大镜都可以实现搜索好友,搜索好友的主要实现在代码的业务逻辑实现类AddFriendPresenterImpl,所以先看看这个方法:
@Override
public void searchFriend(final String keyword) {
mAddFriendView.onStartSearch();
//注:模糊查询只对付费用户开放,付费后可直接使用。
BmobQuery<User> query = new BmobQuery<User>();
query.addWhereContains("username", keyword).addWhereNotEqualTo("username", EMClient.getInstance().getCurrentUser());
query.findObjects(new FindListener<User>() {
@Override
public void done(List<User> list, BmobException e) {
processResult(list, e);
}
});
}
通过代码可以发现,就是通过Bmob的API实现查询,然后将查询结果返回给processResult方法了,那么我们接下来可以看看这个方法具体做了什么事务?
private void processResult(final List<User> list, final BmobException e) {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
if (e == null && list.size() > 0) {
List<String> contacts = DatabaseManager.getInstance().queryAllContacts();
for (int i = 0; i < list.size(); i++) {
AddFriendItem item = new AddFriendItem();
item.timestamp = list.get(i).getCreatedAt();
item.userName = list.get(i).getUsername();
item.isAdded = contacts.contains(item.userName);
mAddFriendItems.add(item);
}
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mAddFriendView.onSearchSuccess();
}
});
} else {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mAddFriendView.onSearchFailed();
}
});
}
}
});
}
代码逻辑比较简单,但是需要注意的是线程的切换,返回的结果在后台线程处理,UI操作需要在UI线程。接下来我们看看UI的好友搜索成功和搜索失败具体是怎么执行的?
搜索失败
@Override
public void onSearchFailed() {
hideProgress();
mFriendNotFound.setVisibility(View.VISIBLE);
mRecyclerView.setVisibility(View.GONE);
}
具体效果:
搜索成功:
@Override
public void onSearchSuccess() {
hideProgress();
mFriendNotFound.setVisibility(View.GONE);
mRecyclerView.setVisibility(View.VISIBLE);
mAddFriendListAdapter.notifyDataSetChanged();
}
//mAddFriendListAdapter的实例化
mAddFriendListAdapter = new AddFriendListAdapter(this, mAddFriendPresenter.getAddFriendList());
我们实例化的时候调用了AddFriendPresenterImpl对象的getAddFriendList()方法获取数据,那么我们回去看看数据返回的什么就明白了!
@Override
public List<AddFriendItem> getAddFriendList() {
return mAddFriendItems;
}
上面搜索成功时我们在后台线程不是完成了对mAddFriendItems数据的填充调用UI线程更新吗?这里就调用了我们填充的数据,最后刷新后如图:
至于为什么有的显示已经添加,有的显示已添加,这个就涉及到之前的模块化思维,大家可以具体看相关的实体以及UI的实现,这里就不继续重复讲了!
我们接下来要讲的就是当我们点击添加的Button时,我们就需要用到EventBus了,上面讲了模块化思维,所以我们的Button点击事件并没有在Adapter里面实现,而是在模块AddFriendItemView里面实现的,具体看代码:
@OnClick(R.id.add)
public void onClick() {
String friendName = mUserName.getText().toString().trim();
AddFriendEvent event = new AddFriendEvent(friendName, "我是马云,有没有兴趣聊一聊?");
EventBus.getDefault().post(event);
}
AddFriendItemView里面并没有注册、注销,因为它只负责说(发布消息),不负责听(订阅消息),所以我们看看AddFriendPresenterImpl听到以后做了什么?
@Subscribe(threadMode = ThreadMode.BACKGROUND,priority = 90)
public void addFriend(AddFriendEvent event) {
Log.d(TAG, "addFriend: ");
try {
EMClient.getInstance().contactManager().addContact(event.getFriendName(), event.getReason());
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mAddFriendView.onAddFriendSuccess();
}
});
} catch (HyphenateException e) {
e.printStackTrace();
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mAddFriendView.onAddFriendFailed();
}
});
}
}
逻辑还是比较简单,调用了环信提供的API添加好友,然后对添加成功和失败分别调用了UI的刷新,此处再次强调线程的切换。接下来我们需要在AddFriendActivity里面看看好友添加成功和添加的相应UI逻辑:
@Override
public void onAddFriendSuccess() {
toast(getString(R.string.add_friend_success));
}
@Override
public void onAddFriendFailed() {
toast(getString(R.string.add_friend_failed));
}
ok,我们已经实现了对于AddFriendActivity界面逻辑,这里就不再更新了有兴趣的朋友可以继续在此基础上继续完善,比如右上角增加一个返回按钮、好友添加成功跳转到聊天界面等。
3. 聊天——ChatActivity用到的知识点
- smoothScrollToPosition(int position)
- scrollToPosition(int position)
二者有什么分别?前者我之前根本没有听说过,然后我就老老实实的查源码,结果发现是一个坑:
public void smoothScrollToPosition(int position) {
if (mLayoutFrozen) {
return;
}
if (mLayout == null) {
Log.e(TAG, "Cannot smooth scroll without a LayoutManager set. " +
"Call setLayoutManager with a non-null argument.");
return;
}
mLayout.smoothScrollToPosition(this, mState, position);
}
继续挖坑:
/**
* <p>Smooth scroll to the specified adapter position.</p>
* <p>To support smooth scrolling, override this method, create your {@link SmoothScroller}
* instance and call {@link #startSmoothScroll(SmoothScroller)}.
* </p>
* @param recyclerView The RecyclerView to which this layout manager is attached
* @param state Current State of RecyclerView
* @param position Scroll to this adapter position.
*/
public void smoothScrollToPosition(RecyclerView recyclerView, State state,
int position) {
Log.e(TAG, "You must override smoothScrollToPosition to support smooth scrolling");
}
我必须实现 smoothScrollToPosition,在源码里要求我实现?我完全是懵逼了,于是我继续查官网文档:
smoothScrollToPosition(int position)
Starts a smooth scroll to an adapter position.
//开始平滑滚动到适配器位置。
我还是不明白什么是平滑的滚动到适配器位置,于是我点开方法看了看:
smoothScrollToPosition
void smoothScrollToPosition (int position)
Starts a smooth scroll to an adapter position.
要支持平滑滚动,必须覆盖smoothScrollToPosition(RecyclerView,State,int)并创建RecyclerView.SmoothScroller。
To support smooth scrolling, you must override smoothScrollToPosition(RecyclerView, State, int) and create a RecyclerView.SmoothScroller.
RecyclerView.LayoutManager负责创建实际的滚动操作。 如果要提供自定义平滑滚动逻辑,请在LayoutManager中覆盖smoothScrollToPosition(RecyclerView,State,int)。
RecyclerView.LayoutManager is responsible for creating the actual scroll action. If you want to provide a custom smooth scroll logic, override smoothScrollToPosition(RecyclerView, State, int) in your LayoutManager.
Parameters
position int: The adapter position to scroll to
See also:
smoothScrollToPosition(RecyclerView, State, int)
看完这里我是哭了,
哭了以后擦干泪水我又去找LinerLayoutManager里面smoothScrollToPosition(RecyclerView,State,int)方法。
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state,
int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext()) {
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return LinearLayoutManager.this
.computeScrollVectorForPosition(targetPosition);
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
public void setTargetPosition(int targetPosition) {
mTargetPosition = targetPosition;
}
然后我查询mTargetPosition,发现在start(RecyclerView recyclerView, LayoutManager layoutManager)、 stop()、onAnimation(int dx, int dy)中用到,我分析这个值应该只是LinerLayoutManager在移动和动画的过程中对于位置的一个判断,但是如何“平滑”的滑动我却是连一个鬼影子都没有看见。所以说这是一次学习源码的反面教材,因为自己最后都不明白自己需要探究的“平滑”是如何实现的?
但是我们不能就此放弃,我不会,难道其他人都不会,如果其他人都不会我更要想办法学会了,如果其他人会的话,那我更要学习,不然差距会越来越大,,,,,,
不废话,直接上大神的干货:
RecyclerView smoothScrollToPosition的滚动时间,文中是这样说的:
当RecyclerView中的数据集很大时,通过smoothScrollToPosition去滚动到一个位置,如果这个位置和当前位置相差很远,比如说300项,你会发现整个过程很长,比如说我遇到的,滚动300项,用了3.5秒。
这主要跟RecyclerView smoothScroll的方式有关,它内部有一个常量值代表每滚动1px需要多少时间,所以滚动的距离越远,需要的时间越长。
如何修改这个值呢?根据上方文档说明,自定义一个LinearSmoothScroller,然后发现需要重写protected int calculateTimeForScrolling(int dx),看这名字就知道函数的作用了,所以修改返回值,让它最多返回小一点就行了。不得不说,这里还有一个坑:
calculateTimeForScrolling(int dx)不是计算这一个smoothScrollToPosition需要的时间的,实际情况时,当实际需要滚动的距离大于10000时,滚动会分多次进行,比如说滚动52000距离,实际会这个函数会调用6次,dx的值前5次是10000,最后一次是2000。实际滚动时间是这6次返回值的和。
知道了这个,解决也简单了,它想分多次调用就让它多次调用吧,我只要每次返回的时间值很小就行了。方法有两个。
- 直接修改返回值,让它足够小
- 修改传入的参数,当dx足够小时,计算出的时间自然就小了。
文中还给出了示例代码:
mLayoutManager = new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) {
@Override
public void smoothScrollToPosition(RecyclerView recyclerView, RecyclerView.State state, int position) {
LinearSmoothScroller linearSmoothScroller =
new LinearSmoothScroller(recyclerView.getContext()) {
@Override
protected int calculateTimeForScrolling(int dx) {
// 此函数计算滚动dx的距离需要多久,当要滚动的距离很大时,比如说52000,
// 经测试,系统会多次调用此函数,每10000距离调一次,所以总的滚动时间
// 是多次调用此函数返回的时间的和,所以修改每次调用该函数时返回的时间的
// 大小就可以影响滚动需要的总时间,可以直接修改些函数的返回值,也可以修改
// dx的值,这里暂定使用后者.
// (See LinearSmoothScroller.TARGET_SEEK_SCROLL_DISTANCE_PX)
if (dx > 3000) {
dx = 3000;
}
return super.calculateTimeForScrolling(dx);
}
@Override
public PointF computeScrollVectorForPosition(int targetPosition) {
return mLayoutManager.computeScrollVectorForPosition(targetPosition);
}
};
linearSmoothScroller.setTargetPosition(position);
startSmoothScroll(linearSmoothScroller);
}
};
我们说的是区别,现在我们知道了怎么“平滑的滚回去”了,那么“滚回去”应该怎么“滚”呢?
这次我们也不再去RecycleView里面踩坑了,直接去LinerLayoutManager里面看看源码:
/**
* <p>Scroll the RecyclerView to make the position visible.</p>
*
* <p>RecyclerView will scroll the minimum amount that is necessary to make the
* target position visible. If you are looking for a similar behavior to
* {@link android.widget.ListView#setSelection(int)} or
* {@link android.widget.ListView#setSelectionFromTop(int, int)}, use
* {@link #scrollToPositionWithOffset(int, int)}.</p>
*
* <p>Note that scroll position change will not be reflected until the next layout call.</p>
*
* @param position Scroll to this adapter position
* @see #scrollToPositionWithOffset(int, int)
*/
@Override
public void scrollToPosition(int position) {
mPendingScrollPosition = position;
mPendingScrollPositionOffset = INVALID_OFFSET;
if (mPendingSavedState != null) {
mPendingSavedState.invalidateAnchor();
}
requestLayout();
}
@Override
public void requestLayout() {
if (mEatRequestLayout == 0 && !mLayoutFrozen) {
super.requestLayout();
} else {
mLayoutRequestEaten = true;
}
}
@CallSuper
public void requestLayout() {
if (mMeasureCache != null) mMeasureCache.clear();
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
// Only trigger request-during-layout logic if this is the view requesting it,
// not the views in its parent hierarchy
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot != null && viewRoot.isInLayout()) {
if (!viewRoot.requestLayoutDuringLayout(this)) {
return;
}
}
mAttachInfo.mViewRequestingLayout = this;
}
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
最后一路追踪到View的requestLayout()方法,感觉这个方法和重新绘制布局,有刷新的意思:
Call this when something has changed which has invalidated the layout of this view. This will schedule a layout pass of the view tree. This should not be called while the view hierarchy is currently in a layout pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the end of the current layout pass (and then layout will run again) or after the current frame is drawn and the next layout occurs. |
当某些更改已使此视图的布局无效时调用此方法。 这将安排视图树的布局传递。 当视图层次结构当前在布局中时,不应调用此方法 如果布局正在进行,请求可以在当前布局传递结束时(然后布局将再次运行)或在当前帧被绘制和下一布局发生之后被执行。 |
于是我看了一下网上大神关于requestLayout的解释,如下:
requestLayout:当view确定自身已经不再适合现有的区域时,该view本身调用这个方法要求parent view重新调用他的onMeasure onLayout来对重新设置自己位置。 特别的当view的layoutparameter发生改变,并且它的值还没能应用到view上,这时候适合调用这个方法。 invalidate:View本身调用迫使view重画。 |
对于上面的判断可以直接通过代码测试,Item我测试了600项,scrollToPosition方法耗时不到1毫秒就解决,但是smoothScrollToPosition方法耗时7.8秒。所以这样可以判断二者的区别了。一个就是慢悠悠的平滑滚动,一个是直接刷新绘制。
4. 聊天——ChatActivity界面逻辑
总的来说,ChatActivity界面逻辑还是相对来讲比较简单的!为了描述清楚,先上图:
首先,我们将界面实例化以后需要干的第一件事就是将好友加入到RecycleView里面来,但是这些数据我们怎么获取呢?这个就由业务逻辑实现类ChatPresenterImpl来完成。接下来不要说话,看代码:
@Override
public void loadMessages(final String userName) {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
EMConversation conversation = EMClient.getInstance().chatManager().getConversation(userName);
if (conversation != null) {
//获取此会话的所有消息
List<EMMessage> messages = conversation.getAllMessages();
mEMMessageList.addAll(messages);
//指定会话消息未读数清零
conversation.markAllMessagesAsRead();
}
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mChatView.onMessagesLoaded();
}
});
}
});
}
再次强调线程的切换——子线程加载数据,主线程更新UI,接下来到Activity的主线程看看更新UI是做了什么工作?
@Override
public void onMoreMessagesLoaded(int size) {
toast("加载数据成功");
mMessageListAdapter.notifyDataSetChanged();
mRecyclerView.scrollToPosition(size);
}
这里比较简单了,就是做个弹窗,然后刷新数据并将RecycleView滚动到最后一条数据的位置。
那么我们在聊天界面发送数据怎么办呢?老规矩,我们支持Button发送或者软键盘的Enter发送,这里需要强调的是软键盘发送处需要处理一个细节,否则程序会崩掉:
Process: com.itheima.leon.qqdemo, PID: 23729
java.lang.NullPointerException: Attempt to invoke virtual method 'void com.hyphenate.chat.EMMessage.setStatus(com.hyphenate.chat.EMMessage$Status)' on a null object reference
at com.itheima.leon.qqdemo.presenter.impl.ChatPresenterImpl$1.run(ChatPresenterImpl.java:41)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
at java.lang.Thread.run(Thread.java:761)
这个原因是客户还没有输入内容的时候触碰了软键盘的Enter键,所以聊天内容就报空指针了。为了更好的体验(最起码不能动不动就崩),我们可以在此处增加一个判断:
mEdit.setOnEditorActionListener(mOnEditorActionListener);
private TextView.OnEditorActionListener mOnEditorActionListener = new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (actionId == EditorInfo.IME_ACTION_SEND && mEdit.getText().toString().length()>0) {
sendMessage();
return true;
}
return false;
}
};
举一反三,那么Button的发送我们是不是也要这样判断一下呢?其实我们有更好的处理方法:
mEdit.addTextChangedListener(mTextWatcher);
private TextWatcherAdapter mTextWatcher = new TextWatcherAdapter() {
@Override
public void afterTextChanged(Editable s) {
mSend.setEnabled(s.length() != 0);
}
};
效果如图:
接下来我们不废话,继续看发送信息的代码是如何实现的?
private void sendMessage() {
mChatPresenter.sendMessage(mUserName, mEdit.getText().toString().trim());
hideKeyBoard();
mEdit.getText().clear();
}
然后我们需要到ChatPresenterImpl继续观察发送信息需要做什么,比如说发送失败了呢?
@Override
public void sendMessage(final String userName, final String message) {
ThreadUtils.runOnBackgroundThread(new Runnable() {
@Override
public void run() {
EMMessage emMessage = EMMessage.createTxtSendMessage(message, userName);
emMessage.setStatus(EMMessage.Status.INPROGRESS);
emMessage.setMessageStatusCallback(mEMCallBackAdapter);
mEMMessageList.add(emMessage);
EMClient.getInstance().chatManager().sendMessage(emMessage);
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mChatView.onStartSendMessage();
}
});
}
});
}
这里大家可能会说,我只看见了正在发送的回调,那么刚刚说的发送失败呢,我怎么没有看见?
其实这里封装了一个接口,我们看看这个mEMCallBackAdapter是怎么定义的?
private EMCallBackAdapter mEMCallBackAdapter = new EMCallBackAdapter() {
@Override
public void onSuccess() {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mChatView.onSendMessageSuccess();
}
});
}
@Override
public void onError(int i, String s) {
ThreadUtils.runOnUiThread(new Runnable() {
@Override
public void run() {
mChatView.onSendMessageFailed();
}
});
}
};
OK,接下来我们一起看看Activity主线程是怎么实现的?
@Override
public void onStartSendMessage() {
dialog.show();
}
private void updateList() {
mMessageListAdapter.notifyDataSetChanged();
smoothScrollToBottom();
}
@Override
public void onSendMessageSuccess() {
dialog.dismiss();
hideProgress();
toast(getString(R.string.send_success));
updateList();
}
@Override
public void onSendMessageFailed() {
dialog.dismiss();
hideProgress();
toast(getString(R.string.send_failed));
}
这里的dialog是我们自定义的,其实也比较简单:
<style name="loading_dialog" parent="Animation.AppCompat.Dialog">
<item name="android:windowFrame">@null</item>
<item name="android:windowNoTitle">true</item>
<item name="android:windowIsFloating">true</item>
<item name="android:windowContentOverlay">@null</item>
</style>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="wrap_content"
android:id="@+id/dialog_view"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/send_message_progress"
/>
</LinearLayout>
/**
* 得到自定义的progressDialog
* @param context
* @return
*/
public static Dialog createLoadingDialog(Context context) {
LayoutInflater inflater = LayoutInflater.from(context);
View v = inflater.inflate(R.layout.loading_dialog, null);// 得到加载view
LinearLayout layout = (LinearLayout) v.findViewById(R.id.dialog_view);// 加载布局
// main.xml中的ImageView
ImageView spaceshipImage = (ImageView) v.findViewById(R.id.img);
AnimationDrawable animationDrawable = (AnimationDrawable) spaceshipImage.getDrawable();
animationDrawable.start();
Dialog loadingDialog = new Dialog(context, R.style.loading_dialog);// 创建自定义样式dialog
loadingDialog.setCancelable(false);// 不可以用“返回键”取消
loadingDialog.setContentView(layout, new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT));// 设置布局
return loadingDialog;
}
OK,这个项目基本上学习结束了,但是不得不说这个项目现在基本上只是一个雏形,还有很多地方没有写好!比如现在最火的表情、语音、未读消息计数更新等等。其实最简单的是将发送信息的表情添加到模块里,然后通过接口调用帧动画播放和关闭。但是因为最近还有其它事情还要做,所以不打算继续在这些细节花费时间了,感兴趣的朋友可以尝试EaseUI 库继续完善,我之前也写过仿微信表情图片,可以借鉴一下。
学习的项目地址: