用sockets打造自己的Android聊天app(安卓篇)

来源:互联网 发布:c语言0xff是什么意思 编辑:程序博客网 时间:2024/05/17 06:54

用sockets打造自己的Android聊天app(安卓篇)

翻译自http://www.androidhive.info/2014/10/android-building-group-chat-app-using-sockets-part-2/

在上一篇文章中我们介绍了web sockets,搭建好了web环境,这篇文章我们开始安卓app的开发。同web应用一样,有两个屏幕,第一个是输入名字,第二个就是显示和发送消息。OK,我们这次的开发环境依然是Eclipse IDE.

6 建立Android App

首先定义一下我们所用到的颜色res ⇒ values ⇒ colors.xml

<?xml version="1.0" encoding="utf-8"?><resources>    <color name="actionbar">#3cb879</color>    <color name="body_background">#e8e8e8</color>    <color name="body_background_green">#82e783</color>    <color name="server_status_bar">#2b2b2b</color>    <color name="title_gray">#434343</color>    <color name="white">#ffffff</color>    <color name="bg_msg_you">#5eb964</color>    <color name="bg_msg_from">#e5e7eb</color>    <color name="msg_border_color">#a1a1a1</color>    <color name="bg_btn_join">#1e6258</color>    <color name="bg_msg_input">#e8e8e8</color>    <color name="text_msg_input">#626262</color>    <color name="lblFromName">#777777</color></resources>

再定义我们所用到的字符串res ⇒ values ⇒ strings.xml

<?xml version="1.0" encoding="utf-8"?><resources>    <string name="app_name">WebMobileGroupChat</string>    <string name="title">(Android WebSockets Chat App)</string>    <string name="author_name">By Ravi Tamada</string>    <string name="author_url">www.androidhive.info</string>    <string name="enter_name">Enter your name</string>    <string name="btn_join">JOIN</string>    <string name="btn_send">Send</string></resources>

再增加样式文件res ⇒ values ⇒ styles.xml

<resources>    <style name="ChatAppTheme" parent="@android:style/Theme.Holo.Light">        <item name="android:actionBarStyle">@style/MyActionBarTheme</item>    </style>    <style name="MyActionBarTheme" parent="@android:style/Widget.Holo.Light.ActionBar">        <item name="android:background">@color/actionbar</item>        <item name="android:titleTextStyle">@style/TitleTextStyle</item>    </style>     <style name="TitleTextStyle" parent="android:TextAppearance.Holo.Widget.ActionBar.Title">        <item name="android:textColor">@color/white</item>    </style></resources>

下面这个布局文件是第一个屏幕,让用户输入用户名:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="@color/actionbar"    android:orientation="vertical" >    <ImageView        android:id="@+id/imgLogo"        android:layout_width="60dp"        android:layout_height="60dp"        android:layout_alignParentTop="true"        android:layout_centerHorizontal="true"        android:layout_gravity="center_horizontal"        android:layout_marginBottom="10dp"        android:layout_marginTop="60dp"        android:src="@drawable/ic_launcher" />    <TextView        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_below="@id/imgLogo"        android:layout_centerHorizontal="true"        android:layout_marginTop="15dp"        android:text="@string/title"        android:textColor="@color/white"        android:textSize="13dp" />    <LinearLayout        android:layout_width="fill_parent"        android:layout_height="wrap_content"        android:layout_centerInParent="true"        android:gravity="center_horizontal"        android:orientation="vertical"        android:padding="20dp" >        <TextView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_marginBottom="10dp"            android:layout_marginTop="15dp"            android:text="@string/enter_name"            android:textColor="@color/white"            android:textSize="18dp" />        <EditText            android:id="@+id/name"            android:layout_width="fill_parent"            android:layout_height="wrap_content"            android:layout_marginBottom="10dp"            android:layout_marginTop="10dp"            android:background="@color/white"            android:inputType="textCapWords"            android:padding="10dp" />        <Button            android:id="@+id/btnJoin"            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:layout_marginTop="40dp"            android:background="@color/bg_btn_join"            android:paddingLeft="25dp"            android:paddingRight="25dp"            android:text="@string/btn_join"            android:textColor="@color/white" />    </LinearLayout>    <!-- author info -->    <LinearLayout        android:layout_width="fill_parent"        android:layout_height="wrap_content"        android:layout_alignParentBottom="true"        android:layout_marginBottom="20dp"        android:gravity="center_horizontal"        android:orientation="vertical" >        <TextView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="@string/author_name"            android:textColor="@color/white"            android:textSize="12dp" />        <TextView            android:layout_width="wrap_content"            android:layout_height="wrap_content"            android:text="@string/author_url"            android:textColor="@color/white"            android:textSize="12dp" />    </LinearLayout></RelativeLayout>

下面是对应的Activity,很简单,就是传递数据
NameActivity.java

package info.androidhive.webgroupchat;import android.app.Activity;import android.content.Intent;import android.os.Bundle;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.Toast;public class NameActivity extends Activity {    private Button btnJoin;    private EditText txtName;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_name);        btnJoin = (Button) findViewById(R.id.btnJoin);        txtName = (EditText) findViewById(R.id.name);        // Hiding the action bar        getActionBar().hide();        btnJoin.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                if (txtName.getText().toString().trim().length() > 0) {                    String name = txtName.getText().toString().trim();                    Intent intent = new Intent(NameActivity.this,                            MainActivity.class);                    intent.putExtra("name", name);                    startActivity(intent);                } else {                    Toast.makeText(getApplicationContext(),                            "Please enter your name", Toast.LENGTH_LONG).show();                }            }        });    }}

不要忘记在清单文件中加上网络权限
在做完这些之后你的app应该是这样子
这里写图片描述

在实现sockets之前,我们还需要定义一些资源,用来显示聊天界面

下载background图片放到drawable目录下。

定义如下三个drawable文件,这些用作聊天的背景tile_bg.xml, bg_msg_from.xml and bg_msg_you.xml

tile_bg.xml<?xml version="1.0" encoding="utf-8"?><bitmap xmlns:android="http://schemas.android.com/apk/res/android"  android:src="@drawable/bg_messages"  android:tileMode="repeat" />
bg_msg_from.xml<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android"    android:shape="rectangle" >    <!-- view background color -->    <solid android:color="@color/bg_msg_from" >    </solid>    <corners android:radius="5dp" >    </corners></shape>
bg_msg_you.xml<?xml version="1.0" encoding="utf-8"?><shape xmlns:android="http://schemas.android.com/apk/res/android"    android:shape="rectangle" >    <!-- view background color -->    <solid android:color="@color/bg_msg_you" >    </solid>    <corners android:radius="5dp" >    </corners></shape>

然后我们在定义两个布局文件,分别是聊天的条目(自己的和别人的)

list_item_message_left.xml<?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"    android:paddingBottom="5dp"    android:paddingTop="5dp"    android:paddingLeft="10dp">    <TextView        android:id="@+id/lblMsgFrom"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:textSize="12dp"        android:textColor="@color/lblFromName"        android:textStyle="italic"        android:padding="5dp"/>    <TextView        android:id="@+id/txtMsg"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:textSize="16dp"        android:layout_marginRight="80dp"        android:textColor="@color/title_gray"        android:paddingLeft="10dp"        android:paddingRight="10dp"        android:paddingTop="5dp"        android:paddingBottom="5dp"        android:background="@drawable/bg_msg_from"/></LinearLayout>
list_item_message_right.xml<?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:gravity="right"    android:orientation="vertical"    android:paddingBottom="5dp"    android:paddingRight="10dp"    android:paddingTop="5dp" >    <TextView        android:id="@+id/lblMsgFrom"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:padding="5dp"        android:textColor="@color/lblFromName"        android:textSize="12dp"        android:textStyle="italic" />    <TextView        android:id="@+id/txtMsg"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_marginLeft="80dp"        android:background="@drawable/bg_msg_you"        android:paddingBottom="5dp"        android:paddingLeft="10dp"        android:paddingRight="10dp"        android:paddingTop="5dp"        android:textColor="@color/white"        android:textSize="16dp" /></LinearLayout>

接下来的这个布局文件就是我们聊天的主界面

activity_main.xml<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:background="@drawable/tile_bg"    android:orientation="vertical" >    <ListView        android:id="@+id/list_view_messages"        android:layout_width="fill_parent"        android:layout_height="0dp"        android:layout_weight="1"        android:background="@null"        android:divider="@null"        android:transcriptMode="alwaysScroll"        android:stackFromBottom="true">    </ListView>    <LinearLayout        android:id="@+id/llMsgCompose"        android:layout_width="fill_parent"        android:layout_height="wrap_content"        android:background="@color/white"        android:orientation="horizontal"        android:weightSum="3" >        <EditText            android:id="@+id/inputMsg"            android:layout_width="0dp"            android:layout_height="fill_parent"            android:layout_weight="2"            android:background="@color/bg_msg_input"            android:textColor="@color/text_msg_input"            android:paddingLeft="6dp"            android:paddingRight="6dp"/>        <Button            android:id="@+id/btnSend"            android:layout_width="0dp"            android:layout_height="wrap_content"            android:layout_weight="1"            android:background="@color/bg_btn_join"            android:textColor="@color/white"            android:text="@string/btn_send" />    </LinearLayout></LinearLayout>

接下来是两个帮助类,第一个Utils类有两个功能,第一个是存储Session id,第二个就是把消息转换成一个JSON字符串,如下

Utils.javapackage info.androidhive.webgroupchat.other;import org.json.JSONException;import org.json.JSONObject;import android.content.Context;import android.content.SharedPreferences;import android.content.SharedPreferences.Editor;public class Utils {    private Context context;    private SharedPreferences sharedPref;    private static final String KEY_SHARED_PREF = "ANDROID_WEB_CHAT";    private static final int KEY_MODE_PRIVATE = 0;    private static final String KEY_SESSION_ID = "sessionId",            FLAG_MESSAGE = "message";    public Utils(Context context) {        this.context = context;        sharedPref = this.context.getSharedPreferences(KEY_SHARED_PREF,                KEY_MODE_PRIVATE);    }    public void storeSessionId(String sessionId) {        Editor editor = sharedPref.edit();        editor.putString(KEY_SESSION_ID, sessionId);        editor.commit();    }    public String getSessionId() {        return sharedPref.getString(KEY_SESSION_ID, null);    }    public String getSendMessageJSON(String message) {        String json = null;        try {            JSONObject jObj = new JSONObject();            jObj.put("flag", FLAG_MESSAGE);            jObj.put("sessionId", getSessionId());            jObj.put("message", message);            json = jObj.toString();        } catch (JSONException e) {            e.printStackTrace();        }        return json;    }}

下面是JavaBean对象

Message.javapackage info.androidhive.webgroupchat.other;public class Message {    private String fromName, message;    private boolean isSelf;    public Message() {    }    public Message(String fromName, String message, boolean isSelf) {        this.fromName = fromName;        this.message = message;        this.isSelf = isSelf;    }    public String getFromName() {        return fromName;    }    public void setFromName(String fromName) {        this.fromName = fromName;    }    public String getMessage() {        return message;    }    public void setMessage(String message) {        this.message = message;    }    public boolean isSelf() {        return isSelf;    }    public void setSelf(boolean isSelf) {        this.isSelf = isSelf;    }}

下面这个类是配置对象

WsConfig.javapackage info.androidhive.webgroupchat.other;public class WsConfig {    public static final String URL_WEBSOCKET = "ws://192.168.0.102:8080/WebMobileGroupChatServer/chat?name=";}

下面这个是ListView的适配器,主要就是判断是自己的消息还是别人的消息,在getView中分别填充

package info.androidhive.webgroupchat;import info.androidhive.webgroupchat.other.Message;import java.util.List;import android.annotation.SuppressLint;import android.app.Activity;import android.content.Context;import android.view.LayoutInflater;import android.view.View;import android.view.ViewGroup;import android.widget.BaseAdapter;import android.widget.TextView;public class MessagesListAdapter extends BaseAdapter {    private Context context;    private List<Message> messagesItems;    public MessagesListAdapter(Context context, List<Message> navDrawerItems) {        this.context = context;        this.messagesItems = navDrawerItems;    }    @Override    public int getCount() {        return messagesItems.size();    }    @Override    public Object getItem(int position) {        return messagesItems.get(position);    }    @Override    public long getItemId(int position) {        return position;    }    @SuppressLint("InflateParams")    @Override    public View getView(int position, View convertView, ViewGroup parent) {        /**         * The following list not implemented reusable list items as list items         * are showing incorrect data Add the solution if you have one         * */        Message m = messagesItems.get(position);        LayoutInflater mInflater = (LayoutInflater) context                .getSystemService(Activity.LAYOUT_INFLATER_SERVICE);        // Identifying the message owner        if (messagesItems.get(position).isSelf()) {            // message belongs to you, so load the right aligned layout            convertView = mInflater.inflate(R.layout.list_item_message_right,                    null);        } else {            // message belongs to other person, load the left aligned layout            convertView = mInflater.inflate(R.layout.list_item_message_left,                    null);        }        TextView lblFrom = (TextView) convertView.findViewById(R.id.lblMsgFrom);        TextView txtMsg = (TextView) convertView.findViewById(R.id.txtMsg);        txtMsg.setText(m.getMessage());        lblFrom.setText(m.getFromName());        return convertView;    }}

下载android websockets library,感谢 Koush大神

将下载的代码导入到eclipse中,并且在自己的项目中引用它
这里写图片描述

开始最重要的了。。。。

同js代码作为sockets客户端类似,WebSocketClient 也有一些回调函数,onConnect, onMessage and onDisconnect.

parseMessage() 函数用作解析从server中获得的Json字符串

在parseMessage()方法中,json的目的有flag表示

当新的消息收到时,要调用adapter.notifyDataSetChanged() 方法去更新列表

sendMessageToServer()发送到服务器

playBeep() 播放声音

package info.androidhive.webgroupchat;import info.androidhive.webgroupchat.other.Message;import info.androidhive.webgroupchat.other.Utils;import info.androidhive.webgroupchat.other.WsConfig;import java.net.URI;import java.net.URLEncoder;import java.util.ArrayList;import java.util.List;import java.util.Locale;import org.json.JSONException;import org.json.JSONObject;import android.app.Activity;import android.content.Intent;import android.media.Ringtone;import android.media.RingtoneManager;import android.net.Uri;import android.os.Bundle;import android.util.Log;import android.view.View;import android.widget.Button;import android.widget.EditText;import android.widget.ListView;import android.widget.Toast;import com.codebutler.android_websockets.WebSocketClient;public class MainActivity extends Activity {    // LogCat tag    private static final String TAG = MainActivity.class.getSimpleName();    private Button btnSend;    private EditText inputMsg;    private WebSocketClient client;    // Chat messages list adapter    private MessagesListAdapter adapter;    private List<Message> listMessages;    private ListView listViewMessages;    private Utils utils;    // Client name    private String name = null;    // JSON flags to identify the kind of JSON response    private static final String TAG_SELF = "self", TAG_NEW = "new",            TAG_MESSAGE = "message", TAG_EXIT = "exit";    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        btnSend = (Button) findViewById(R.id.btnSend);        inputMsg = (EditText) findViewById(R.id.inputMsg);        listViewMessages = (ListView) findViewById(R.id.list_view_messages);        utils = new Utils(getApplicationContext());        // 从上一个屏幕获取姓名        Intent i = getIntent();        name = i.getStringExtra("name");        btnSend.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                // Sending message to web socket server                sendMessageToServer(utils.getSendMessageJSON(inputMsg.getText()                        .toString()));                // Clearing the input filed once message was sent                inputMsg.setText("");            }        });        listMessages = new ArrayList<Message>();        adapter = new MessagesListAdapter(this, listMessages);        listViewMessages.setAdapter(adapter);        /**         * 创建web sockets客户端,有如下的回调函数         * */        client = new WebSocketClient(URI.create(WsConfig.URL_WEBSOCKET                + URLEncoder.encode(name)), new WebSocketClient.Listener() {            @Override            public void onConnect() {            }            /**             * 从服务端接受消息             * */            @Override            public void onMessage(String message) {                Log.d(TAG, String.format("Got string message! %s", message));                parseMessage(message);            }            @Override            public void onMessage(byte[] data) {                Log.d(TAG, String.format("Got binary message! %s",                        bytesToHex(data)));                // Message will be in JSON format                parseMessage(bytesToHex(data));            }            /**             * 连接中断             * */            @Override            public void onDisconnect(int code, String reason) {                String message = String.format(Locale.US,                        "Disconnected! Code: %d Reason: %s", code, reason);                showToast(message);                // clear the session id from shared preferences                utils.storeSessionId(null);            }            @Override            public void onError(Exception error) {                Log.e(TAG, "Error! : " + error);                showToast("Error! : " + error);            }        }, null);        client.connect();    }    /**     * 发送消息     * */    private void sendMessageToServer(String message) {        if (client != null && client.isConnected()) {            client.send(message);        }    }    /**     * 解析从服务端收到的json 消息的目的由flag字段所指定,flag=self,消息属于指定的人,     * new:新人加入   *    到对话中,message:新的消息,exit:退出     *      *     *      *      * */    private void parseMessage(final String msg) {        try {            JSONObject jObj = new JSONObject(msg);            // JSON node 'flag'            String flag = jObj.getString("flag");            // 如果是self,json中包含sessionId信息            if (flag.equalsIgnoreCase(TAG_SELF)) {                String sessionId = jObj.getString("sessionId");                // Save the session id in shared preferences                utils.storeSessionId(sessionId);                Log.e(TAG, "Your session id: " + utils.getSessionId());            } else if (flag.equalsIgnoreCase(TAG_NEW)) {                // If the flag is 'new', new person joined the room                String name = jObj.getString("name");                String message = jObj.getString("message");                // number of people online                String onlineCount = jObj.getString("onlineCount");                showToast(name + message + ". Currently " + onlineCount                        + " people online!");            } else if (flag.equalsIgnoreCase(TAG_MESSAGE)) {                // if the flag is 'message', new message received                String fromName = name;                String message = jObj.getString("message");                String sessionId = jObj.getString("sessionId");                boolean isSelf = true;                // Checking if the message was sent by you                if (!sessionId.equals(utils.getSessionId())) {                    fromName = jObj.getString("name");                    isSelf = false;                }                Message m = new Message(fromName, message, isSelf);                // 把消息加入到arraylist中                appendMessage(m);            } else if (flag.equalsIgnoreCase(TAG_EXIT)) {                // If the flag is 'exit', somebody left the conversation                String name = jObj.getString("name");                String message = jObj.getString("message");                showToast(name + message);            }        } catch (JSONException e) {            e.printStackTrace();        }    }    @Override    protected void onDestroy() {        super.onDestroy();        if(client != null & client.isConnected()){            client.disconnect();        }    }    /**     * 把消息放到listView里     * */    private void appendMessage(final Message m) {        runOnUiThread(new Runnable() {            @Override            public void run() {                listMessages.add(m);                adapter.notifyDataSetChanged();                // Playing device's notification                playBeep();            }        });    }    private void showToast(final String message) {        runOnUiThread(new Runnable() {            @Override            public void run() {                Toast.makeText(getApplicationContext(), message,                        Toast.LENGTH_LONG).show();            }        });    }    /**     * 播放默认的通知声音     * */    public void playBeep() {        try {            Uri notification = RingtoneManager                    .getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);            Ringtone r = RingtoneManager.getRingtone(getApplicationContext(),                    notification);            r.play();        } catch (Exception e) {            e.printStackTrace();        }    }    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();    public static String bytesToHex(byte[] bytes) {        char[] hexChars = new char[bytes.length * 2];        for (int j = 0; j < bytes.length; j++) {            int v = bytes[j] & 0xFF;            hexChars[j * 2] = hexArray[v >>> 4];            hexChars[j * 2 + 1] = hexArray[v & 0x0F];        }        return new String(hexChars);    }}

最终结果应该是这样的

这里写图片描述

这里写图片描述

2 0