由於沒有自己的服務器,我就找了個能實現雙方通信的SDK,這個SDK是友盟的用戶反饋SDK。本系列的博文關注的不是網絡通信,而是如何在網絡通信機制已經做好的情況下,做出一個可用的聊天系統。其實,剛開始做的時候覺得適配器挺難的,但后來發現實現和QQ相同的布局文件也需要技術,所以本篇就來詳細的說下布局文件該怎么寫。
一、主界面
主界面的元素分為三塊,一個是標題欄,還有是中間的listview,最后是下方的輸入區域。整體分析后發現頂部的
1.1 ActionBar
標題欄我們沒辦法用系統自帶的actionbar來做,因為主要的文字是居中的,所以就自己做個actionbar吧。RelativeLayout是actionbar的父布局,里面放兩個textview和一個imageview。左右兩邊的控件都是緊鄰父控件,中間的“天之界限”是水平+垂直居中。這部分的布局沒啥難度,所以直接貼代碼了。
<RelativeLayout android:id="@+id/actionbar_layout" android:layout_width="match_parent" android:layout_height="50dp" android:layout_alignParentTop="true" android:background="#14a5dc" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:gravity="center_horizontal|center_vertical" android:text="天之界線" android:textColor="#ffffff" android:textSize="20sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:layout_marginLeft="15dp" android:text="消息" android:textColor="#ffffff" android:textSize="18sp" /> <ImageView android:layout_width="25dp" android:layout_height="25dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="15dp" android:src="@drawable/user_pic" /> </RelativeLayout>
1.2 ListView
listview放置的是我們的聊天信息,但因為我們的聊天信息需要有下拉刷新的功能,所以需要給listview再套一個下拉刷新的控件,這個控件可以自由選擇,我選擇的是android自帶的SwipeRefreshLayout。這樣我們就知道中間的布局是怎么做的了。
如果我們僅僅是簡單的放了listview,再加個背景就會出現過度繪制。所以我們必須來分析下布局,核心思想:讓背景圖片僅僅停留在看得到的地方。因為actionbar和底部的inputbox都是有自己背景的,所以我們的背景圖僅僅需要添加到listview中,但因為listview和activity都是有背景的,所以我們完全可以給activity設置為透明背景。
<activity android:name="com.kale.mycmcc.CustomActivity" android:launchMode="singleTop" android:theme="@android:style/Theme.Translucent.NoTitleBar" />
這樣就保證了當前界面僅僅有一層背景,減少過度繪制。此外,我們的listview應該在每次發送一條信息后自動滾動到底部,因此需要給listview添加兩個屬性:
android:stackFromBottom="true"
android:transcriptMode="alwaysScroll"
然后我們再清除掉listview的item之間的分割線,這個分割線我是用java代碼處理的:mListView.setDivider(null);
現在技術難點全部攻克,直接產生如下代碼:
<android.support.v4.widget.SwipeRefreshLayout android:id="@+id/swipe_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/conversation_editText" android:layout_below="@+id/actionbar_layout" > <ListView android:id="@+id/conversation_listView" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/conversation_bg" android:stackFromBottom="true" android:transcriptMode="alwaysScroll" /> </android.support.v4.widget.SwipeRefreshLayout>
1.3 InputBox
底部的輸入框是由editText和button組成的,外加一個背景的view,用來填充灰色的背景。
這里我們需要滿足的是edittext隨着輸入的文字的數目而變高,這就需要讓editview來主導布局。而button一直是在布局的右下方,背景的view也應該是緊貼edittext的頂部,這里為了做出邊界的效果,所以還設置了邊距。
背景的view和緊貼這edittext的上邊距,為了設置邊框效果所以這里設置了-6dp作為頂部的邊框。
<View android:id="@+id/input_box_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignTop="@+id/conversation_editText" android:layout_marginTop="-6dp" android:background="#ebecee" />
這里的Button有可用和不可用的樣式,當editText中沒有文字那么button變為不可用的,如果editText中有文字,那么button就變成藍色,表示可用。
btn_enabled_shape.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" > <corners android:radius="5dp" /> <solid android:color="#02a7e3" /> </shape>
btn_unabled_shape.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" > <corners android:radius="5dp" /> <solid android:color="#ffffff" /> </shape>
btn_bg_selector.xml
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android" > <item android:state_enabled="true" android:drawable="@drawable/btn_enabled_shape" /> <item android:state_enabled="false" android:drawable="@drawable/btn_unabled_shape"/> <!-- 默認樣式 --> <item android:drawable="@drawable/btn_enabled_shape"/> </selector>
Button的布局文件
<Button android:id="@+id/conversation_send_btn" android:layout_width="60dp" android:layout_height="35dp" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_marginBottom="6dp" android:layout_marginLeft="3dp" android:layout_marginRight="5dp" android:background="@drawable/btn_bg_selector" android:enabled="false" android:gravity="center" android:text="發送" android:textSize="15sp" />
EditText的布局文件就很簡單了,沒什么特別的,本來想給自帶的輸入法添加要給發送按鈕的,但由於第三方的輸入法不支持,所以沒出來效果。我們設想中的edittext會自動獲取焦點,進入activity直接彈出輸入法,所以就加了一個requestFocus屬性。
<EditText android:id="@+id/conversation_editText" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_margin="5dp" android:layout_toLeftOf="@+id/conversation_send_btn" android:background="@drawable/input_box" android:ems="10" android:gravity="center_vertical" android:hint=" " android:imeOptions="actionSearch" android:padding="8dp" android:textSize="17sp" > <requestFocus /> </EditText>
1.4 主界面全部的布局代碼

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/umeng_fb_container" android:layout_width="match_parent" android:layout_height="match_parent"> <RelativeLayout android:id="@+id/actionbar_layout" android:layout_width="match_parent" android:layout_height="50dp" android:layout_alignParentTop="true" android:background="#14a5dc" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerHorizontal="true" android:layout_centerVertical="true" android:gravity="center_horizontal|center_vertical" android:text="天之界線" android:textColor="#ffffff" android:textSize="20sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_centerVertical="true" android:layout_marginLeft="15dp" android:text="消息" android:textColor="#ffffff" android:textSize="18sp" /> <ImageView android:layout_width="25dp" android:layout_height="25dp" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginRight="15dp" android:src="@drawable/user_pic" /> </RelativeLayout> <android.support.v4.widget.SwipeRefreshLayout android:id="@+id/swipe_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/conversation_editText" android:layout_below="@+id/actionbar_layout" > <ListView android:id="@+id/conversation_listView" android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/conversation_bg" android:stackFromBottom="true" android:transcriptMode="alwaysScroll" /> </android.support.v4.widget.SwipeRefreshLayout> <View android:id="@+id/input_box_layout" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignTop="@+id/conversation_editText" android:layout_marginTop="-6dp" android:background="#ebecee" /> <Button android:id="@+id/conversation_send_btn" android:layout_width="60dp" android:layout_height="35dp" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_marginBottom="6dp" android:layout_marginLeft="3dp" android:layout_marginRight="5dp" android:background="@drawable/btn_bg_selector" android:enabled="false" android:gravity="center" android:text="發送" android:textSize="15sp" /> <EditText android:id="@+id/conversation_editText" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_margin="5dp" android:layout_toLeftOf="@+id/conversation_send_btn" android:background="@drawable/input_box" android:ems="10" android:gravity="center_vertical" android:hint=" " android:imeOptions="actionSearch" android:padding="8dp" android:textSize="17sp" > <requestFocus /> </EditText> </RelativeLayout>
二、開發者消息的布局文件
2.1 頭像和文字氣泡
因為這里用的是用戶反饋的SDK,所以模擬的是開發者和用戶交流的場景,開發者發送的消息會變成item放入listview中。
布局文件比較簡單,開發者的頭像在父控件的左邊,接着是一個textview,這個textview的背景是一個氣泡。頭像應該永遠在item的左上方,而氣泡應該可以隨着文字的多少來改變自己的高度。下面的示例圖顯示了長文字和短文字的效果:
因此,textView僅僅是應該在頭像的右邊,至於寬度是按照內容來定的。
<ImageView android:id="@+id/head_imageView" android:layout_width="45dp" android:layout_height="45dp" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginRight="3dp" android:src="@drawable/dev_head_photo" /> <TextView android:id="@+id/reply_textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="30dp" android:layout_toRightOf="@id/head_imageView" android:background="@drawable/dev_msg_box" android:gravity="center_vertical" android:paddingBottom="10dp" android:paddingLeft="25dp" android:paddingRight="18dp" android:paddingTop="8dp" android:textColor="#010101" android:textSize="18sp" />
2.2 信息發送時間
我的設想中每條消息應該都帶一個發送時間的,如果兩條消息間隔5分鍾就顯示下時間,因此還需要在消息的底部放入一個textview設置時間。因為這里的時間view應該是在需要顯示的時候就出現,不應該總是出現,所以我用了ViewStub來做處理。ViewStub中的view會在可見時才進行繪制,很容易實現延遲繪制。
<ViewStub android:id="@+id/time_view_stub" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/reply_textView" android:layout_centerHorizontal="true" android:layout_marginTop="30dp" android:layout="@layout/umeng_fb_msg_time" />
這里的ViewStub中包含了一個textview,它的代碼是放在另一個布局中的,方便用戶消息布局文件共用它。代碼如下:
<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/msg_Time_TextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:textColor="#666666" android:layout_gravity="center_horizontal" android:textScaleX="0.8" android:textSize="16sp" android:text="xxxxxxxxxxxx"/>
好了,這樣我們就知道開發者回復的item左邊是頭像,一個textview緊貼着頭像的右側,整個布局的最下方是一個顯示時間的textview,這個textview用stubView進行處理。
2.3 全部代碼

<?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="wrap_content" android:padding="10dp" > <ImageView android:id="@+id/head_imageView" android:layout_width="45dp" android:layout_height="45dp" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginRight="3dp" android:src="@drawable/dev_head_photo" /> <TextView android:id="@+id/reply_textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginRight="30dp" android:layout_toRightOf="@id/head_imageView" android:background="@drawable/dev_msg_box" android:gravity="center_vertical" android:paddingBottom="10dp" android:paddingLeft="25dp" android:paddingRight="18dp" android:paddingTop="8dp" android:textColor="#010101" android:textSize="18sp" /> <ViewStub android:id="@+id/time_view_stub" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/reply_textView" android:layout_centerHorizontal="true" android:layout_marginTop="30dp" android:layout="@layout/umeng_fb_msg_time" /> </RelativeLayout>
三、用戶消息的布局文件
3.1 頭像
用戶的消息和開發者的消息類似,但要復雜一點。因為用戶的消息是在右邊的,並且要有發送消息的指示器,比如正在發送的進度條,發送失敗時顯示的感嘆號。而這兩個指示控件又是在消息氣泡的左邊,所以必須用相對布局。在做這個相對布局的時候我發現很難做到頭像、氣泡、指示器依次放置,所以不得已把頭像獨立了出來,讓指示器和消息氣泡放入一個布局中。
頭像的imageview代碼如下:
<ImageView android:id="@+id/head_imageView" android:layout_width="45dp" android:layout_height="45dp" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:layout_marginLeft="3dp" android:src="@drawable/user_head_photo" />
3.2 消息氣泡 + 指示器
當氣泡僅僅有一個指示器的時候,指示器應該在氣泡的左邊並且在氣泡的垂直居中位置,但如果文字很多,指示器就不需要垂直居中了。所以指示器的安排應該是氣泡的左邊,距離氣泡底部一定的距離。指示器有兩種狀態,一種是進度條表示消息正在發送,一種是感嘆號,表示消息發送失敗。
氣泡和指示器的布局代碼如下:
<RelativeLayout android:id="@+id/repley_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_toLeftOf="@id/head_imageView"> <TextView android:id="@+id/reply_textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_marginLeft="30dp" android:background="@drawable/user_msg_box" android:gravity="center_vertical" android:paddingBottom="10dp" android:paddingLeft="18dp" android:paddingRight="25dp" android:paddingTop="8dp" android:textColor="#ffffff" android:textSize="18sp" /> <ImageView android:id="@+id/msg_error_imageView" android:layout_width="20dp" android:layout_height="20dp" android:layout_alignBottom="@id/reply_textView" android:layout_alignParentLeft="true" android:layout_marginBottom="14dp" android:src="@drawable/msg_error_pic" /> <ProgressBar android:id="@+id/msg_senting_progressBar" style="?android:attr/progressBarStyleSmall" android:layout_width="15dp" android:layout_height="15dp" android:layout_alignBottom="@id/reply_textView" android:layout_alignParentLeft="true" android:layout_marginBottom="16dp" android:layout_marginLeft="3dp" android:visibility="visible" /> </RelativeLayout>
3.3 消息發送時間
這部分的代碼和之前講述的完全一致,直接貼出來了。
<ViewStub android:id="@+id/time_view_stub" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/repley_layout" android:layout_centerHorizontal="true" android:layout_marginTop="30dp" android:layout="@layout/umeng_fb_msg_time" />
3.4 全部代碼

<?xml version="1.0" encoding="utf-8"?> <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:padding="10dp"> <ImageView android:id="@+id/head_imageView" android:layout_width="45dp" android:layout_height="45dp" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:layout_marginLeft="3dp" android:src="@drawable/user_head_photo" /> <RelativeLayout android:id="@+id/repley_layout" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginLeft="10dp" android:layout_toLeftOf="@id/head_imageView"> <TextView android:id="@+id/reply_textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_marginLeft="30dp" android:background="@drawable/user_msg_box" android:gravity="center_vertical" android:paddingBottom="10dp" android:paddingLeft="18dp" android:paddingRight="25dp" android:paddingTop="8dp" android:textColor="#ffffff" android:textSize="18sp" /> <ImageView android:id="@+id/msg_error_imageView" android:layout_width="20dp" android:layout_height="20dp" android:layout_alignBottom="@id/reply_textView" android:layout_alignParentLeft="true" android:layout_marginBottom="14dp" android:src="@drawable/msg_error_pic" /> <ProgressBar android:id="@+id/msg_senting_progressBar" style="?android:attr/progressBarStyleSmall" android:layout_width="15dp" android:layout_height="15dp" android:layout_alignBottom="@id/reply_textView" android:layout_alignParentLeft="true" android:layout_marginBottom="16dp" android:layout_marginLeft="3dp" android:visibility="visible" /> </RelativeLayout> <ViewStub android:id="@+id/time_view_stub" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/repley_layout" android:layout_centerHorizontal="true" android:layout_marginTop="30dp" android:layout="@layout/umeng_fb_msg_time" /> </RelativeLayout>