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

本文介绍如何在Android应用中实现类似系统联系人的分组导航及挤压动画特效,包括使用AlphabetIndexer进行分组和监听ListView滚动实现动画。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

记得在我刚接触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项目,命名为ContactsDemo。首先我们还是先来完成布局文件,打开或新建activity_main.xml作为程序的主布局文件,在里面加入如下代码:

[html] view plain copy
  1. <RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. android:orientation="vertical">
  6. <ListView
  7. android:id="@+id/contacts_list_view"
  8. android:layout_width="fill_parent"
  9. android:layout_height="wrap_content"
  10. android:layout_alignParentTop="true"
  11. android:fadingEdge="none">
  12. </ListView>
  13. <LinearLayout
  14. android:id="@+id/title_layout"
  15. android:layout_width="fill_parent"
  16. android:layout_height="18dip"
  17. android:layout_alignParentTop="true"
  18. android:background="#303030">
  19. <TextView
  20. android:id="@+id/title"
  21. android:layout_width="wrap_content"
  22. android:layout_height="wrap_content"
  23. android:layout_gravity="center_horizontal"
  24. android:layout_marginLeft="10dip"
  25. android:textColor="#ffffff"
  26. android:textSize="13sp"/>
  27. </LinearLayout>
  28. </RelativeLayout>

布局文件很简单,里面放入了一个ListView,用于展示联系人信息。另外还在头部放了一个LinearLayout,里面包含了一个TextView,它的作用是在界面头部始终显示一个当前分组。

然后新建一个contact_item.xml的布局,这个布局用于在ListView中的每一行进行填充,代码如下:

[html] view plain copy
  1. <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
  2. android:layout_width="match_parent"
  3. android:layout_height="match_parent"
  4. android:orientation="vertical">
  5. <LinearLayout
  6. android:id="@+id/sort_key_layout"
  7. android:layout_width="fill_parent"
  8. android:layout_height="18dip"
  9. android:background="#303030">
  10. <TextView
  11. android:id="@+id/sort_key"
  12. android:layout_width="wrap_content"
  13. android:layout_height="wrap_content"
  14. android:layout_gravity="center_horizontal"
  15. android:layout_marginLeft="10dip"
  16. android:textColor="#ffffff"
  17. android:textSize="13sp"/>
  18. </LinearLayout>
  19. <LinearLayout
  20. android:id="@+id/name_layout"
  21. android:layout_width="fill_parent"
  22. android:layout_height="50dip">
  23. <ImageView
  24. android:layout_width="wrap_content"
  25. android:layout_height="wrap_content"
  26. android:layout_gravity="center_vertical"
  27. android:layout_marginLeft="10dip"
  28. android:layout_marginRight="10dip"
  29. android:src="@drawable/icon"/>
  30. <TextView
  31. android:id="@+id/name"
  32. android:layout_width="wrap_content"
  33. android:layout_height="wrap_content"
  34. android:layout_gravity="center_vertical"
  35. android:textColor="#ffffff"
  36. android:textSize="22sp"/>
  37. </LinearLayout>
  38. </LinearLayout>
在这个布局文件中,首先是放入了一个和前面完成一样的分组布局,因为不仅界面头部需要展示分组,在每个分组内的第一个无素之前都需要展示分组布局。然后是加入一个简单的LinearLayout,里面包含了一个ImageView用于显示联系人头像,还包含一个TextView用于显示联系人姓名。

这样我们的布局文件就全部写完了,下面开始来真正地实现功能。

先从简单的开始,新建一个Contact实体类:

[java] view plain copy
  1. publicclassContact{
  2. /**
  3. *联系人姓名
  4. */
  5. privateStringname;
  6. /**
  7. *排序字母
  8. */
  9. privateStringsortKey;
  10. publicStringgetName(){
  11. returnname;
  12. }
  13. publicvoidsetName(Stringname){
  14. this.name=name;
  15. }
  16. publicStringgetSortKey(){
  17. returnsortKey;
  18. }
  19. publicvoidsetSortKey(StringsortKey){
  20. this.sortKey=sortKey;
  21. }
  22. }
这个实体类很简单,只包含了联系人姓名和排序键。

接下来完成联系人列表适配器的编写,新建一个ContactAdapter类继承自ArrayAdapter,加入如下代码:

[java] view plain copy
  1. publicclassContactAdapterextendsArrayAdapter<Contact>{
  2. /**
  3. *需要渲染的item布局文件
  4. */
  5. privateintresource;
  6. /**
  7. *字母表分组工具
  8. */
  9. privateSectionIndexermIndexer;
  10. publicContactAdapter(Contextcontext,inttextViewResourceId,List<Contact>objects){
  11. super(context,textViewResourceId,objects);
  12. resource=textViewResourceId;
  13. }
  14. @Override
  15. publicViewgetView(intposition,ViewconvertView,ViewGroupparent){
  16. Contactcontact=getItem(position);
  17. LinearLayoutlayout=null;
  18. if(convertView==null){
  19. layout=(LinearLayout)LayoutInflater.from(getContext()).inflate(resource,null);
  20. }else{
  21. layout=(LinearLayout)convertView;
  22. }
  23. TextViewname=(TextView)layout.findViewById(R.id.name);
  24. LinearLayoutsortKeyLayout=(LinearLayout)layout.findViewById(R.id.sort_key_layout);
  25. TextViewsortKey=(TextView)layout.findViewById(R.id.sort_key);
  26. name.setText(contact.getName());
  27. intsection=mIndexer.getSectionForPosition(position);
  28. if(position==mIndexer.getPositionForSection(section)){
  29. sortKey.setText(contact.getSortKey());
  30. sortKeyLayout.setVisibility(View.VISIBLE);
  31. }else{
  32. sortKeyLayout.setVisibility(View.GONE);
  33. }
  34. returnlayout;
  35. }
  36. /**
  37. *给当前适配器传入一个分组工具。
  38. *
  39. *@paramindexer
  40. */
  41. publicvoidsetIndexer(SectionIndexerindexer){
  42. mIndexer=indexer;
  43. }
  44. }

上面的代码中,最重要的就是getView方法,在这个方法中,我们使用SectionIndexer的getSectionForPosition方法,通过当前的position值拿到了对应的section值,然后再反向通过刚刚拿到的section值,调用getPositionForSection方法,取回新的position值。如果当前的position值和新的position值是相等的,那么我们就可以认为当前position的项是某个分组下的第一个元素,我们应该将分组布局显示出来,而其它的情况就应该将分组布局隐藏。

最后我们来编写程序的主界面,打开或新建MainActivity作为程序的主界面,代码如下所示:

[java] view plain copy
  1. publicclassMainActivityextendsActivity{
  2. /**
  3. *分组的布局
  4. */
  5. privateLinearLayouttitleLayout;
  6. /**
  7. *分组上显示的字母
  8. */
  9. privateTextViewtitle;
  10. /**
  11. *联系人ListView
  12. */
  13. privateListViewcontactsListView;
  14. /**
  15. *联系人列表适配器
  16. */
  17. privateContactAdapteradapter;
  18. /**
  19. *用于进行字母表分组
  20. */
  21. privateAlphabetIndexerindexer;
  22. /**
  23. *存储所有手机中的联系人
  24. */
  25. privateList<Contact>contacts=newArrayList<Contact>();
  26. /**
  27. *定义字母表的排序规则
  28. */
  29. privateStringalphabet="#ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  30. /**
  31. *上次第一个可见元素,用于滚动时记录标识。
  32. */
  33. privateintlastFirstVisibleItem=-1;
  34. @Override
  35. protectedvoidonCreate(BundlesavedInstanceState){
  36. super.onCreate(savedInstanceState);
  37. setContentView(R.layout.activity_main);
  38. adapter=newContactAdapter(this,R.layout.contact_item,contacts);
  39. titleLayout=(LinearLayout)findViewById(R.id.title_layout);
  40. title=(TextView)findViewById(R.id.title);
  41. contactsListView=(ListView)findViewById(R.id.contacts_list_view);
  42. Uriuri=ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
  43. Cursorcursor=getContentResolver().query(uri,
  44. newString[]{"display_name","sort_key"},null,null,"sort_key");
  45. if(cursor.moveToFirst()){
  46. do{
  47. Stringname=cursor.getString(0);
  48. StringsortKey=getSortKey(cursor.getString(1));
  49. Contactcontact=newContact();
  50. contact.setName(name);
  51. contact.setSortKey(sortKey);
  52. contacts.add(contact);
  53. }while(cursor.moveToNext());
  54. }
  55. startManagingCursor(cursor);
  56. indexer=newAlphabetIndexer(cursor,1,alphabet);
  57. adapter.setIndexer(indexer);
  58. if(contacts.size()>0){
  59. setupContactsListView();
  60. }
  61. }
  62. /**
  63. *为联系人ListView设置监听事件,根据当前的滑动状态来改变分组的显示位置,从而实现挤压动画的效果。
  64. */
  65. privatevoidsetupContactsListView(){
  66. contactsListView.setAdapter(adapter);
  67. contactsListView.setOnScrollListener(newOnScrollListener(){
  68. @Override
  69. publicvoidonScrollStateChanged(AbsListViewview,intscrollState){
  70. }
  71. @Override
  72. publicvoidonScroll(AbsListViewview,intfirstVisibleItem,intvisibleItemCount,
  73. inttotalItemCount){
  74. intsection=indexer.getSectionForPosition(firstVisibleItem);
  75. intnextSecPosition=indexer.getPositionForSection(section+1);
  76. if(firstVisibleItem!=lastFirstVisibleItem){
  77. MarginLayoutParamsparams=(MarginLayoutParams)titleLayout.getLayoutParams();
  78. params.topMargin=0;
  79. titleLayout.setLayoutParams(params);
  80. title.setText(String.valueOf(alphabet.charAt(section)));
  81. }
  82. if(nextSecPosition==firstVisibleItem+1){
  83. ViewchildView=view.getChildAt(0);
  84. if(childView!=null){
  85. inttitleHeight=titleLayout.getHeight();
  86. intbottom=childView.getBottom();
  87. MarginLayoutParamsparams=(MarginLayoutParams)titleLayout
  88. .getLayoutParams();
  89. if(bottom<titleHeight){
  90. floatpushedDistance=bottom-titleHeight;
  91. params.topMargin=(int)pushedDistance;
  92. titleLayout.setLayoutParams(params);
  93. }else{
  94. if(params.topMargin!=0){
  95. params.topMargin=0;
  96. titleLayout.setLayoutParams(params);
  97. }
  98. }
  99. }
  100. }
  101. lastFirstVisibleItem=firstVisibleItem;
  102. }
  103. });
  104. }
  105. /**
  106. *获取sortkey的首个字符,如果是英文字母就直接返回,否则返回#。
  107. *
  108. *@paramsortKeyString
  109. *数据库中读取出的sortkey
  110. *@return英文字母或者#
  111. */
  112. privateStringgetSortKey(StringsortKeyString){
  113. Stringkey=sortKeyString.substring(0,1).toUpperCase();
  114. if(key.matches("[A-Z]")){
  115. returnkey;
  116. }
  117. return"#";
  118. }
  119. }

可以看到,在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的声明:

[html] view plain copy
  1. <manifestxmlns:android="http://schemas.android.com/apk/res/android"
  2. package="com.example.contactsdemo"
  3. android:versionCode="1"
  4. android:versionName="1.0">
  5. <uses-sdk
  6. android:minSdkVersion="8"
  7. android:targetSdkVersion="8"/>
  8. <uses-permissionandroid:name="android.permission.READ_CONTACTS"></uses-permission>
  9. <application
  10. android:allowBackup="true"
  11. android:icon="@drawable/ic_launcher"
  12. android:label="@string/app_name"
  13. android:theme="@android:style/Theme.NoTitleBar"
  14. >
  15. <activity
  16. android:name="com.example.contactsdemo.MainActivity"
  17. android:label="@string/app_name">
  18. <intent-filter>
  19. <actionandroid:name="android.intent.action.MAIN"/>
  20. <categoryandroid:name="android.intent.category.LAUNCHER"/>
  21. </intent-filter>
  22. </activity>
  23. </application>
  24. </manifest>
现在我们来运行一下程序,效果如下图所示:

目前的话,分组导航和挤压动画效果都已经完成了,看起来感觉还是挺不错的,下一篇文章我会带领大家继续完善这个程序,加入字母表快速滚动功能,感兴趣的朋友请继续阅读Android系统联系人全特效实现(下),字母表快速滚动

好了,今天的讲解到此结束,有疑问的朋友请在下面留言。

源码下载,请点击这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值