制作高仿QQ的聊天系统(上)—— 布局文件 & 减少过度绘制
由于没有自己的服务器,我就找了个能实现双方通信的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>