Android系统联系人全特效实现(上)分组导航和挤压动画(附源码)

时间:2022-03-28 06:21:08

记得在我刚接触android的时候对系统联系人中的特效很感兴趣,它会根据手机中联系人姓氏的首字母进行分组,并在界面的最顶端始终显示一个当前的分组。如下图所示:
Android系统联系人全特效实现(上)分组导航和挤压动画(附源码) 
最让我感兴趣的是,当后一个分组和前一个分组相碰时,会产生一个上顶的挤压动画。那个时候我思考了各种方法想去实现这种特效,可是限于功夫不到家,都未能成功。如今两年多过去了,自己也成长了很多,再回头去想想这个功能,突然发现已经有了思路,于是立刻记录下来与大家分享。

首先讲一下需要提前了解的知识点,这里我们最需要用到的就是sectionindexer,它能够有效地帮助我们对分组进行控制。由于sectionindexer是一个接口,你可以自定义一个子类来实现sectionindexer,不过自己再写一个sectionindexer的实现太麻烦了,这里我们直接使用android提供好的实现alphabetindexer,用它来实现联系人分组功能已经足够了。

alphabetindexer的构造函数需要传入三个参数,第一个参数是cursor,第二个参数是sortedcolumnindex整型,第三个参数是alphabet字符串。其中cursor就是把我们从数据库中查出的游标传进去,sortedcolumnindex就是指明我们是使用哪一列进行排序的,而alphabet则是指定字母表排序规则,比如:"abcdefghijklmnopqrstuvwxyz"。有了alphabetindexer,我们就可以通过它的getpositionforsection和getsectionforposition方法,找出当前位置所在的分组,和当前分组所在的位置,从而实现类似于系统联系人的分组导航和挤压动画效果,关于alphabetindexer更详细的详解,请参考官方文档。
那么我们应该怎样对联系人进行排序呢?前面也提到过,有一个sortedcolumnindex参数,这个sortedcolumn到底在哪里呢?我们来看一下系统联系人的raw_contacts这张表(/data/data/com.android.providers.contacts/databases/contacts2.db),这个表结构比较复杂,里面有二十多个列,其中有一列名叫sort_key,这就是我们要找的了!如下图所示:
Android系统联系人全特效实现(上)分组导航和挤压动画(附源码) 
可以看到,这一列非常人性化地帮我们记录了汉字所对应的拼音,这样我们就可以通过这一列的值轻松为联系人进行排序了。
下面我们就来开始实现,新建一个android项目,命名为contactsdemo。首先我们还是先来完成布局文件,打开或新建activity_main.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="match_parent"
android:orientation="vertical" >
<listview
android:id="@+id/contacts_list_view"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_alignparenttop="true"
android:fadingedge="none" >
</listview>
<linearlayout
android:id="@+id/title_layout"
android:layout_width="fill_parent"
android:layout_height="18dip"
android:layout_alignparenttop="true"
android:background="#303030" >
<textview
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginleft="10dip"
android:textcolor="#ffffff"
android:textsize="13sp" />
</linearlayout>
</relativelayout>


布局文件很简单,里面放入了一个listview,用于展示联系人信息。另外还在头部放了一个linearlayout,里面包含了一个textview,它的作用是在界面头部始终显示一个当前分组。
然后新建一个contact_item.xml的布局,这个布局用于在listview中的每一行进行填充,代码如下:

复制代码 代码如下:


<linearlayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<linearlayout
android:id="@+id/sort_key_layout"
android:layout_width="fill_parent"
android:layout_height="18dip"
android:background="#303030" >
<textview
android:id="@+id/sort_key"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginleft="10dip"
android:textcolor="#ffffff"
android:textsize="13sp" />
</linearlayout>
<linearlayout
android:id="@+id/name_layout"
android:layout_width="fill_parent"
android:layout_height="50dip" >
<imageview
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginleft="10dip"
android:layout_marginright="10dip"
android:src="@drawable/icon" />
<textview
android:id="@+id/name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textcolor="#ffffff"
android:textsize="22sp" />
</linearlayout>
</linearlayout>


在这个布局文件中,首先是放入了一个和前面完成一样的分组布局,因为不仅界面头部需要展示分组,在每个分组内的第一个无素之前都需要展示分组布局。然后是加入一个简单的linearlayout,里面包含了一个imageview用于显示联系人头像,还包含一个textview用于显示联系人姓名。
这样我们的布局文件就全部写完了,下面开始来真正地实现功能。
先从简单的开始,新建一个contact实体类:

复制代码 代码如下:


public class contact {
/**
* 联系人姓名
*/
private string name;
/**
* 排序字母
*/
private string sortkey;
public string getname() {
return name;
}
public void setname(string name) {
this.name = name;
}
public string getsortkey() {
return sortkey;
}
public void setsortkey(string sortkey) {
this.sortkey = sortkey;
}
}


这个实体类很简单,只包含了联系人姓名和排序键。
接下来完成联系人列表适配器的编写,新建一个contactadapter类继承自arrayadapter,加入如下代码:

复制代码 代码如下:


public class contactadapter extends arrayadapter<contact> {
/**
* 需要渲染的item布局文件
*/
private int resource;
/**
* 字母表分组工具
*/
private sectionindexer mindexer;
public contactadapter(context context, int textviewresourceid, list<contact> objects) {
super(context, textviewresourceid, objects);
resource = textviewresourceid;
}
@override
public view getview(int position, view convertview, viewgroup parent) {
contact contact = getitem(position);
linearlayout layout = null;
if (convertview == null) {
layout = (linearlayout) layoutinflater.from(getcontext()).inflate(resource, null);
} else {
layout = (linearlayout) convertview;
}
textview name = (textview) layout.findviewbyid(r.id.name);
linearlayout sortkeylayout = (linearlayout) layout.findviewbyid(r.id.sort_key_layout);
textview sortkey = (textview) layout.findviewbyid(r.id.sort_key);
name.settext(contact.getname());
int section = mindexer.getsectionforposition(position);
if (position == mindexer.getpositionforsection(section)) {
sortkey.settext(contact.getsortkey());
sortkeylayout.setvisibility(view.visible);
} else {
sortkeylayout.setvisibility(view.gone);
}
return layout;
}
/**
* 给当前适配器传入一个分组工具。
*
* @param indexer
*/
public void setindexer(sectionindexer indexer) {
mindexer = indexer;
}
}


上面的代码中,最重要的就是getview方法,在这个方法中,我们使用sectionindexer的getsectionforposition方法,通过当前的position值拿到了对应的section值,然后再反向通过刚刚拿到的section值,调用getpositionforsection方法,取回新的position值。如果当前的position值和新的position值是相等的,那么我们就可以认为当前position的项是某个分组下的第一个元素,我们应该将分组布局显示出来,而其它的情况就应该将分组布局隐藏。
最后我们来编写程序的主界面,打开或新建mainactivity作为程序的主界面,代码如下所示:

复制代码 代码如下:


public class mainactivity extends activity {
/**
* 分组的布局
*/
private linearlayout titlelayout;
/**
* 分组上显示的字母
*/
private textview title;
/**
* 联系人listview
*/
private listview contactslistview;
/**
* 联系人列表适配器
*/
private contactadapter adapter;
/**
* 用于进行字母表分组
*/
private alphabetindexer indexer;
/**
* 存储所有手机中的联系人
*/
private list<contact> contacts = new arraylist<contact>();
/**
* 定义字母表的排序规则
*/
private string alphabet = "#abcdefghijklmnopqrstuvwxyz";
/**
* 上次第一个可见元素,用于滚动时记录标识。
*/
private int lastfirstvisibleitem = -1;
@override
protected void oncreate(bundle savedinstancestate) {
super.oncreate(savedinstancestate);
setcontentview(r.layout.activity_main);
adapter = new contactadapter(this, r.layout.contact_item, contacts);
titlelayout = (linearlayout) findviewbyid(r.id.title_layout);
title = (textview) findviewbyid(r.id.title);
contactslistview = (listview) findviewbyid(r.id.contacts_list_view);
uri uri = contactscontract.commondatakinds.phone.content_uri;
cursor cursor = getcontentresolver().query(uri,
new string[] { "display_name", "sort_key" }, null, null, "sort_key");
if (cursor.movetofirst()) {
do {
string name = cursor.getstring(0);
string sortkey = getsortkey(cursor.getstring(1));
contact contact = new contact();
contact.setname(name);
contact.setsortkey(sortkey);
contacts.add(contact);
} while (cursor.movetonext());
}
startmanagingcursor(cursor);
indexer = new alphabetindexer(cursor, 1, alphabet);
adapter.setindexer(indexer);
if (contacts.size() > 0) {
setupcontactslistview();
}
}
/**
* 为联系人listview设置监听事件,根据当前的滑动状态来改变分组的显示位置,从而实现挤压动画的效果。
*/
private void setupcontactslistview() {
contactslistview.setadapter(adapter);
contactslistview.setonscrolllistener(new onscrolllistener() {
@override
public void onscrollstatechanged(abslistview view, int scrollstate) {
}
@override
public void onscroll(abslistview view, int firstvisibleitem, int visibleitemcount,
int totalitemcount) {
int section = indexer.getsectionforposition(firstvisibleitem);
int nextsecposition = indexer.getpositionforsection(section + 1);
if (firstvisibleitem != lastfirstvisibleitem) {
marginlayoutparams params = (marginlayoutparams) titlelayout.getlayoutparams();
params.topmargin = 0;
titlelayout.setlayoutparams(params);
title.settext(string.valueof(alphabet.charat(section)));
}
if (nextsecposition == firstvisibleitem + 1) {
view childview = view.getchildat(0);
if (childview != null) {
int titleheight = titlelayout.getheight();
int bottom = childview.getbottom();
marginlayoutparams params = (marginlayoutparams) titlelayout
.getlayoutparams();
if (bottom < titleheight) {
float pusheddistance = bottom - titleheight;
params.topmargin = (int) pusheddistance;
titlelayout.setlayoutparams(params);
} else {
if (params.topmargin != 0) {
params.topmargin = 0;
titlelayout.setlayoutparams(params);
}
}
}
}
lastfirstvisibleitem = firstvisibleitem;
}
});
}
/**
* 获取sort key的首个字符,如果是英文字母就直接返回,否则返回#。
*
* @param sortkeystring
* 数据库中读取出的sort key
* @return 英文字母或者#
*/
private string getsortkey(string sortkeystring) {
string key = sortkeystring.substring(0, 1).touppercase();
if (key.matches("[a-z]")) {
return key;
}
return "#";
}
}


可以看到,在oncreate方法中,我们从系统联系人数据库中去查询联系人的姓名和排序键,之后将查询返回的cursor直接传入alphabetindexer作为第一个参数。由于我们一共就查了两列,排序键在第二列,所以我们第二个sortedcolumnindex参数传入1。第三个alphabet参数这里传入了"#abcdefghijklmnopqrstuvwxyz"字符串,因为可能有些联系人的姓名不在字母表范围内,我们统一用#来表示这部分联系人。

然后我们在setupcontactslistview方法中监听了listview的滚动,在onscroll方法中通过getsectionforposition方法获取第一个可见元素的分组值,然后给该分组值加1,再通过getpositionforsection方法或者到下一个分组中的第一个元素,如果下个分组的第一个元素值等于第一个可见元素的值加1,那就说明下个分组的布局要和界面顶部分组布局相碰了。之后再通过listview的getchildat(0)方法,获取到界面上显示的第一个子view,再用view.getbottom获取底部距离父窗口的位置,对比分组布局的高度来对顶部分组布局进行纵向偏移,就可以实现挤压动画的效果了。

最后给出androidmanifest.xml的代码,由于要读取手机联系人,因此需要加上android.permission.read_contacts的声明:

复制代码 代码如下:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.contactsdemo"
android:versioncode="1"
android:versionname="1.0" >
<uses-sdk
android:minsdkversion="8"
android:targetsdkversion="8" />
<uses-permission android:name="android.permission.read_contacts"></uses-permission>
<application
android:allowbackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@android:style/theme.notitlebar"
>
<activity
android:name="com.example.contactsdemo.mainactivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.main" />
<category android:name="android.intent.category.launcher" />
</intent-filter>
</activity>
</application>
</manifest>


现在我们来运行一下程序,效果如下图所示:
Android系统联系人全特效实现(上)分组导航和挤压动画(附源码) 
目前的话,分组导航和挤压动画效果都已经完成了,看起来感觉还是挺不错的,下一篇文章我会带领大家继续完善这个程序,加入字母表快速滚动功能。

好了,今天的讲解到此结束,有疑问的朋友请在下面留言。
源码下载,请点击这里