即时通讯系列阅读
1. 项目简介
做一个类似QQ 的通讯工具,要求有注册、登录、添加好友、添加分组、聊天、退出登录等功能。我用8 张运行效果图来展示我们将要实现的功能。
注意,服务器用的是Openfire,我们可以用Spark 作为另外一个客户端进行测试。
闪屏页进来以后是登录界面,要求记住上次登陆的账号和密码,在界面的右下角有新用户按钮,点击后进入注册界面。注册只需要输入账号和密码即可。账号不能和其他人重复,否则注册不成功。
主界面是一个Activity 绑定3 个Fragment 实现,分别是消息、联系人、动态。
其中消息界面是一个ListView 展出了最近的联系人。点击其中的一个条目可以进入聊天界面。
联系人界面主要是一个ExpandableListView。ExpandableListView 列出了用户组,以及各个组中的好友。点击任意好友可以进入聊天界面。在ExpandableListView 上面有两个图标,分别是新朋友、新群组。点击新朋友弹出一个自定义对话框,在该对话框中输入对方好友的名称,等待对方同意了即可添加为好友。
点击新群组也弹出一个自定义对话框,在该对话框中输入分组名称,则可以创建一个分组。
如果好友不存则添加好友失败,如果分组不存在则创建分组失败。
动态界面主要展示了当前用户的信息,最下面有个退出按钮,点击后退出当前登录,并跳转到登录界面。
聊天界面是一个ListView,该ListView 的条目有两类布局,分别用来表示好友的消息和自己的消息。在最下方的输入框输入文本内容,然后点击发送可以将该消息发送给好友。好友有消息过来也可以直接显示在该界面。
2. 项目搭建
1、下载asmak.jar,是个德国网站
asmack 的版本号从0.x 到现在的4.x 变化比较大。不通过版本差异也比较大。本次我写项目用的是0.8.x 的。
用的是13 年的版本。因为该api 在网上能查到的资料比较多。如果是下一次我再写这个项目我就决定用15 年发布的最新版本的了。
我想看看asmack 公司官网,吧asmack 去掉,想着就是贵公司的网址呢,却得到这样的界面。
2、给工程添加jar
简单极了,只需把下载好的jar 包添加到Android 工程的libs 目录下即可。注意如果eclipse 没有自动将该包添加到环境变量中,我们需要手动添加一下。
3、项目目录结构图
3. 项目实现
3.1 Spark 客户端的下载和安装闪屏界面
新颖的闪屏界面,马老师,学习的目的,我借来用一用哈希望您不用太在意。
我要做的效果是闪屏等待3 秒,然后进入主界面。但是每次都让用户等待3 秒,对于急性子来讲估计会很抓狂。那怎么办呢,只要是这个界面用户触摸屏幕则立即进入主界面。布局太简单了,不给了。直接给代码。
/**
* 闪屏页面默认等待3s 触摸时直接进入主界面
*
* @author wzy 2015-8-14
*
*/
public class SplashActivity extends Activity {
private static final long DURATION = 3000;
/**
* 保证变量的修改是可见的,但是无法保证变量的原子性
*/
private volatile boolean isEntered = false;
private Thread splashThread = new Thread(new Runnable() {
@Override
public void run() {
SystemClock.sleep(DURATION);
enter();
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
splashThread.start();
}
private synchronized void enter() {
if (!isEntered) {
isEntered = true;
startActivity(new Intent(SplashActivity.this, LoginActivity.class));
finish();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
enter();
return true;
}
}
3.2 登录
登录界面布局很简单。如果写不出来就没必要往下看了。登录的核心代码:
mXmppConnection.login(name, pwd);
activity_login.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/login_background" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/login_background"
android:orientation="vertical" >
<ImageView
android:id="@+id/iv_touxiang"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="40dp"
android:contentDescription="@null"
android:src="@drawable/login_image" />
<EditText
android:id="@+id/et_name"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_marginTop="30dp"
android:background="#FFFFFF"
android:hint="请输入账号"
android:paddingLeft="5dp"
android:textSize="20sp" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#55AABBCC" />
<EditText
android:id="@+id/et_pwd"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:background="#FFFFFF"
android:hint="请输入密码"
android:inputType="textPassword"
android:paddingLeft="5dp"
android:textSize="20sp" />
<Button
android:id="@+id/btn_login"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:background="@color/title_layout"
android:onClick="login"
android:text="登录"
android:textColor="#FFFFFF"
android:textSize="20sp" />
</LinearLayout>
<Button
android:id="@+id/register"
android:layout_width="100dp"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginRight="20dp"
android:background="@drawable/register_user_btn"
android:text="新用户"
android:onClick="gotoRegist"
android:textColor="@color/blue"
android:textSize="16sp" />
</RelativeLayout>
LoginActivity 和RegistActivity 都继承自BaseActivity。因此这里先把BaseActivity 的代码列出。
BaseActivity.java
public class XmppConnectionManager {
private static XmppConnectionManager instance = null;
//私有化构造函数
private XmppConnectionManager() {
}
/**
* 获取该对象
* @return
*/
public static XmppConnectionManager getInstance() {
if (instance == null) {
instance = new XmppConnectionManager();
}
return instance;
}
/**
* 执行初始化脚本
* @return
*/
public XMPPConnection init() {
/**
* 创建连接配置对象<br>
* 第一个参数是Openfire 服务器地址<br>
* 第二个参数是Openfire 服务器断开号,默认是5222<br>
* 我们可以把这两个参数配置的清单文件中,也可以写死在代码中
*/
ConnectionConfiguration connectionConfig = new
ConnectionConfiguration(Const.XMPP_HOST,Const.XMPP_PORT);
/**
* 不使用SAL 安全验证
*/
connectionConfig.setSASLAuthenticationEnabled(false);
/**
* 设置TLS 安全模式
*/
connectionConfig.setSecurityMode(ConnectionConfiguration.SecurityMode.enabled);
// 允许自动连接
connectionConfig.setReconnectionAllowed(true);
// 允许登陆成功后更新在线状态
connectionConfig.setSendPresence(true);
//设置为debug 模式,该模式可以在控制台看到接收和发送的xmpp 协议
connectionConfig.setDebuggerEnabled(true);
// 收到好友邀请后同意添加为好友的模式,有三种,manual 表示需要经过同意,accept_all 表示不经同意自动
为好友,reject_all 拒绝加为好友邀请
Roster.setDefaultSubscriptionMode(Roster.SubscriptionMode.accept_all);
/**
* 该配置时为了解决asmack 的一个bug 或者说弥补一个不足之处。不用细细追究。
*/
configure(ProviderManager.getInstance());
//创建一个连接对象,参数为配置对象
XMPPConnection connection = new XMPPConnection(connectionConfig);
return connection;
}
public void configure(ProviderManager pm) {
// Private Data Storage
pm.addIQProvider("query", "jabber:iq:private", new
PrivateDataManager.PrivateDataIQProvider());
// Time
try {
pm.addIQProvider("query", "jabber:iq:time",
Class.forName("org.jivesoftware.smackx.packet.Time"));
} catch (ClassNotFoundException e) {
Log.w("TestClient", "Can't load class for org.jivesoftware.smackx.packet.Time");
}
// Roster Exchange
pm.addExtensionProvider("x", "jabber:x:roster", new RosterExchangeProvider());
// Message Events
pm.addExtensionProvider("x", "jabber:x:event", new MessageEventProvider());
// Chat State
pm.addExtensionProvider("active", "http://jabber.org/protocol/chatstates", new
ChatStateExtension.Provider());
pm.addExtensionProvider("composing", "http://jabber.org/protocol/chatstates", new
ChatStateExtension.Provider());
pm.addExtensionProvider("paused", "http://jabber.org/protocol/chatstates", new
ChatStateExtension.Provider());
pm.addExtensionProvider("inactive", "http://jabber.org/protocol/chatstates", new
ChatStateExtension.Provider());
pm.addExtensionProvider("gone", "http://jabber.org/protocol/chatstates", new
ChatStateExtension.Provider());
// XHTML
pm.addExtensionProvider("html", "http://jabber.org/protocol/xhtml-im", new
XHTMLExtensionProvider());
// Group Chat Invitations
pm.addExtensionProvider("x", "jabber:x:conference", new
GroupChatInvitation.Provider());
// Service Discovery # Items
pm.addIQProvider("query", "http://jabber.org/protocol/disco#items", new
DiscoverItemsProvider());
// Service Discovery # Info
pm.addIQProvider("query", "http://jabber.org/protocol/disco#info", new
DiscoverInfoProvider());
// Data Forms
pm.addExtensionProvider("x", "jabber:x:data", new DataFormProvider());
// MUC User
pm.addExtensionProvider("x", "http://jabber.org/protocol/muc#user", new
MUCUserProvider());
// MUC Admin
pm.addIQProvider("query", "http://jabber.org/protocol/muc#admin", new
MUCAdminProvider());
// MUC Owner
pm.addIQProvider("query", "http://jabber.org/protocol/muc#owner", new
MUCOwnerProvider());
// Delayed Delivery
pm.addExtensionProvider("x", "jabber:x:delay", new DelayInformationProvider());
// Version
try {
pm.addIQProvider("query", "jabber:iq:version",
Class.forName("org.jivesoftware.smackx.packet.Version"));
} catch (ClassNotFoundException e) {
// Not sure what's happening here.
}
// VCard
pm.addIQProvider("vCard", "vcard-temp", new VCardProvider());
// Offline Message Requests
pm.addIQProvider("offline", "http://jabber.org/protocol/offline", new
OfflineMessageRequest.Provider());
// Offline Message Indicator
pm.addExtensionProvider("offline", "http://jabber.org/protocol/offline", new
OfflineMessageInfo.Provider());
// Last Activity
pm.addIQProvider("query", "jabber:iq:last", new LastActivity.Provider());
// User Search
pm.addIQProvider("query", "jabber:iq:search", new UserSearch.Provider());
// SharedGroupsInfo
pm.addIQProvider("sharedgroup",
"http://www.jivesoftware.org/protocol/sharedgroup", new SharedGroupsInfo.Provider());
// JEP-33: Extended Stanza Addressing
pm.addExtensionProvider("addresses", "http://jabber.org/protocol/address", new
MultipleAddressesProvider());
// FileTransfer
pm.addIQProvider("si", "http://jabber.org/protocol/si", new
StreamInitiationProvider());
pm.addIQProvider("query", "http://jabber.org/protocol/bytestreams", new
BytestreamsProvider());
// Privacy
pm.addIQProvider("query", "jabber:iq:privacy", new PrivacyProvider());
pm.addIQProvider("command", "http://jabber.org/protocol/commands", new
AdHocCommandDataProvider());
pm.addExtensionProvider("malformed-action",
"http://jabber.org/protocol/commands", new
AdHocCommandDataProvider.MalformedActionError());
pm.addExtensionProvider("bad-locale", "http://jabber.org/protocol/commands", new
AdHocCommandDataProvider.BadLocaleError());
pm.addExtensionProvider("bad-payload", "http://jabber.org/protocol/commands", new
AdHocCommandDataProvider.BadPayloadError());
pm.addExtensionProvider("bad-sessionid", "http://jabber.org/protocol/commands",
new AdHocCommandDataProvider.BadSessionIDError());
pm.addExtensionProvider("session-expired","http://jabber.org/protocol/commands",
new AdHocCommandDataProvider.SessionExpiredError());
}
}
LoginActivity.java
import org.jivesoftware.smack.XMPPException;
import com.itheima.qq.MainActivity;
import com.itheima.qq.QQApplication;
import com.itheima.qq.R;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import static android.app.Activity.RESULT_CANCELED;
import static android.app.Activity.RESULT_OK;
import static android.content.Context.MODE_PRIVATE;
public class LoginActivity extends BaseActivity {
private EditText et_name;
private EditText et_pwd;
private String name;
private String pwd;
private SharedPreferences sp;
private Handler handler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case RESULT_OK:
Toast.makeText(LoginActivity.this, msg.obj + " 登录成功", 0).show();
//获取自定义的Application,并将连接对象保存在Application