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

    [html] view plaincopy
     
    1. <RelativeLayout xmlns: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.   
    7.     <ListView  
    8.         android:id="@+id/contacts_list_view"  
    9.         android:layout_width="fill_parent"  
    10.         android:layout_height="wrap_content"  
    11.         android:layout_alignParentTop="true"  
    12.         android:fadingEdge="none" >  
    13.     </ListView>  
    14.       
    15.      <LinearLayout  
    16.         android:id="@+id/title_layout"  
    17.         android:layout_width="fill_parent"  
    18.         android:layout_height="18dip"  
    19.         android:layout_alignParentTop="true"  
    20.         android:background="#303030" >  
    21.   
    22.         <TextView  
    23.             android:id="@+id/title"  
    24.             android:layout_width="wrap_content"  
    25.             android:layout_height="wrap_content"  
    26.             android:layout_gravity="center_horizontal"  
    27.             android:layout_marginLeft="10dip"  
    28.             android:textColor="#ffffff"  
    29.             android:textSize="13sp" />  
    30.     </LinearLayout>  
    31.   
    32. </RelativeLayout>  

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

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

    [html] view plaincopy
     
    1. <LinearLayout xmlns: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.   
    6.     <LinearLayout  
    7.         android:id="@+id/sort_key_layout"  
    8.         android:layout_width="fill_parent"  
    9.         android:layout_height="18dip"  
    10.         android:background="#303030" >  
    11.   
    12.         <TextView  
    13.             android:id="@+id/sort_key"  
    14.             android:layout_width="wrap_content"  
    15.             android:layout_height="wrap_content"  
    16.             android:layout_gravity="center_horizontal"  
    17.             android:layout_marginLeft="10dip"  
    18.             android:textColor="#ffffff"  
    19.             android:textSize="13sp" />  
    20.     </LinearLayout>  
    21.   
    22.     <LinearLayout  
    23.         android:id="@+id/name_layout"  
    24.         android:layout_width="fill_parent"  
    25.         android:layout_height="50dip" >  
    26.   
    27.         <ImageView  
    28.             android:layout_width="wrap_content"  
    29.             android:layout_height="wrap_content"  
    30.             android:layout_gravity="center_vertical"  
    31.             android:layout_marginLeft="10dip"  
    32.             android:layout_marginRight="10dip"  
    33.             android:src="@drawable/icon" />  
    34.   
    35.         <TextView  
    36.             android:id="@+id/name"  
    37.             android:layout_width="wrap_content"  
    38.             android:layout_height="wrap_content"  
    39.             android:layout_gravity="center_vertical"  
    40.             android:textColor="#ffffff"  
    41.             android:textSize="22sp" />  
    42.     </LinearLayout>  
    43.   
    44. </LinearLayout>  

    在这个布局文件中,首先是放入了一个和前面完成一样的分组布局,因为不仅界面头部需要展示分组,在每个分组内的第一个无素之前都需要展示分组布局。然后是加入一个简单的LinearLayout,里面包含了一个ImageView用于显示联系人头像,还包含一个TextView用于显示联系人姓名。

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

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

    [java] view plaincopy
     
    1. public class Contact {  
    2.   
    3.     /** 
    4.      * 联系人姓名 
    5.      */  
    6.     private String name;  
    7.   
    8.     /** 
    9.      * 排序字母 
    10.      */  
    11.     private String sortKey;  
    12.   
    13.     public String getName() {  
    14.         return name;  
    15.     }  
    16.   
    17.     public void setName(String name) {  
    18.         this.name = name;  
    19.     }  
    20.   
    21.     public String getSortKey() {  
    22.         return sortKey;  
    23.     }  
    24.   
    25.     public void setSortKey(String sortKey) {  
    26.         this.sortKey = sortKey;  
    27.     }  
    28.   
    29. }  

    这个实体类很简单,只包含了联系人姓名和排序键。

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

    [java] view plaincopy
     
    1. public class ContactAdapter extends ArrayAdapter<Contact> {  
    2.   
    3.     /** 
    4.      * 需要渲染的item布局文件 
    5.      */  
    6.     private int resource;  
    7.   
    8.     /** 
    9.      * 字母表分组工具 
    10.      */  
    11.     private SectionIndexer mIndexer;  
    12.   
    13.     public ContactAdapter(Context context, int textViewResourceId, List<Contact> objects) {  
    14.         super(context, textViewResourceId, objects);  
    15.         resource = textViewResourceId;  
    16.     }  
    17.   
    18.     @Override  
    19.     public View getView(int position, View convertView, ViewGroup parent) {  
    20.         Contact contact = getItem(position);  
    21.         LinearLayout layout = null;  
    22.         if (convertView == null) {  
    23.             layout = (LinearLayout) LayoutInflater.from(getContext()).inflate(resource, null);  
    24.         } else {  
    25.             layout = (LinearLayout) convertView;  
    26.         }  
    27.         TextView name = (TextView) layout.findViewById(R.id.name);  
    28.         LinearLayout sortKeyLayout = (LinearLayout) layout.findViewById(R.id.sort_key_layout);  
    29.         TextView sortKey = (TextView) layout.findViewById(R.id.sort_key);  
    30.         name.setText(contact.getName());  
    31.         int section = mIndexer.getSectionForPosition(position);  
    32.         if (position == mIndexer.getPositionForSection(section)) {  
    33.             sortKey.setText(contact.getSortKey());  
    34.             sortKeyLayout.setVisibility(View.VISIBLE);  
    35.         } else {  
    36.             sortKeyLayout.setVisibility(View.GONE);  
    37.         }  
    38.         return layout;  
    39.     }  
    40.   
    41.     /** 
    42.      * 给当前适配器传入一个分组工具。 
    43.      *  
    44.      * @param indexer 
    45.      */  
    46.     public void setIndexer(SectionIndexer indexer) {  
    47.         mIndexer = indexer;  
    48.     }  
    49.   
    50. }  

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

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

    [java] view plaincopy
     
    1. public class MainActivity extends Activity {  
    2.   
    3.     /** 
    4.      * 分组的布局 
    5.      */  
    6.     private LinearLayout titleLayout;  
    7.   
    8.     /** 
    9.      * 分组上显示的字母 
    10.      */  
    11.     private TextView title;  
    12.   
    13.     /** 
    14.      * 联系人ListView 
    15.      */  
    16.     private ListView contactsListView;  
    17.   
    18.     /** 
    19.      * 联系人列表适配器 
    20.      */  
    21.     private ContactAdapter adapter;  
    22.   
    23.     /** 
    24.      * 用于进行字母表分组 
    25.      */  
    26.     private AlphabetIndexer indexer;  
    27.   
    28.     /** 
    29.      * 存储所有手机中的联系人 
    30.      */  
    31.     private List<Contact> contacts = new ArrayList<Contact>();  
    32.   
    33.     /** 
    34.      * 定义字母表的排序规则 
    35.      */  
    36.     private String alphabet = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ";  
    37.   
    38.     /** 
    39.      * 上次第一个可见元素,用于滚动时记录标识。 
    40.      */  
    41.     private int lastFirstVisibleItem = -1;  
    42.   
    43.     @Override  
    44.     protected void onCreate(Bundle savedInstanceState) {  
    45.         super.onCreate(savedInstanceState);  
    46.         setContentView(R.layout.activity_main);  
    47.         adapter = new ContactAdapter(this, R.layout.contact_item, contacts);  
    48.         titleLayout = (LinearLayout) findViewById(R.id.title_layout);  
    49.         title = (TextView) findViewById(R.id.title);  
    50.         contactsListView = (ListView) findViewById(R.id.contacts_list_view);  
    51.         Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;  
    52.         Cursor cursor = getContentResolver().query(uri,  
    53.                 new String[] { "display_name""sort_key" }, nullnull"sort_key");  
    54.         if (cursor.moveToFirst()) {  
    55.             do {  
    56.                 String name = cursor.getString(0);  
    57.                 String sortKey = getSortKey(cursor.getString(1));  
    58.                 Contact contact = new Contact();  
    59.                 contact.setName(name);  
    60.                 contact.setSortKey(sortKey);  
    61.                 contacts.add(contact);  
    62.             } while (cursor.moveToNext());  
    63.         }  
    64.         startManagingCursor(cursor);  
    65.         indexer = new AlphabetIndexer(cursor, 1, alphabet);  
    66.         adapter.setIndexer(indexer);  
    67.         if (contacts.size() > 0) {  
    68.             setupContactsListView();  
    69.         }  
    70.     }  
    71.   
    72.     /** 
    73.      * 为联系人ListView设置监听事件,根据当前的滑动状态来改变分组的显示位置,从而实现挤压动画的效果。 
    74.      */  
    75.     private void setupContactsListView() {  
    76.         contactsListView.setAdapter(adapter);  
    77.         contactsListView.setOnScrollListener(new OnScrollListener() {  
    78.             @Override  
    79.             public void onScrollStateChanged(AbsListView view, int scrollState) {  
    80.             }  
    81.   
    82.             @Override  
    83.             public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,  
    84.                     int totalItemCount) {  
    85.                 int section = indexer.getSectionForPosition(firstVisibleItem);  
    86.                 int nextSecPosition = indexer.getPositionForSection(section + 1);  
    87.                 if (firstVisibleItem != lastFirstVisibleItem) {  
    88.                     MarginLayoutParams params = (MarginLayoutParams) titleLayout.getLayoutParams();  
    89.                     params.topMargin = 0;  
    90.                     titleLayout.setLayoutParams(params);  
    91.                     title.setText(String.valueOf(alphabet.charAt(section)));  
    92.                 }  
    93.                 if (nextSecPosition == firstVisibleItem + 1) {  
    94.                     View childView = view.getChildAt(0);  
    95.                     if (childView != null) {  
    96.                         int titleHeight = titleLayout.getHeight();  
    97.                         int bottom = childView.getBottom();  
    98.                         MarginLayoutParams params = (MarginLayoutParams) titleLayout  
    99.                                 .getLayoutParams();  
    100.                         if (bottom < titleHeight) {  
    101.                             float pushedDistance = bottom - titleHeight;  
    102.                             params.topMargin = (int) pushedDistance;  
    103.                             titleLayout.setLayoutParams(params);  
    104.                         } else {  
    105.                             if (params.topMargin != 0) {  
    106.                                 params.topMargin = 0;  
    107.                                 titleLayout.setLayoutParams(params);  
    108.                             }  
    109.                         }  
    110.                     }  
    111.                 }  
    112.                 lastFirstVisibleItem = firstVisibleItem;  
    113.             }  
    114.         });  
    115.   
    116.     }  
    117.   
    118.     /** 
    119.      * 获取sort key的首个字符,如果是英文字母就直接返回,否则返回#。 
    120.      *  
    121.      * @param sortKeyString 
    122.      *            数据库中读取出的sort key 
    123.      * @return 英文字母或者# 
    124.      */  
    125.     private String getSortKey(String sortKeyString) {  
    126.         String key = sortKeyString.substring(01).toUpperCase();  
    127.         if (key.matches("[A-Z]")) {  
    128.             return key;  
    129.         }  
    130.         return "#";  
    131.     }  
    132.   
    133. }  

    可以看到,在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 plaincopy
     
    1. <manifest xmlns:android="http://schemas.android.com/apk/res/android"  
    2.     package="com.example.contactsdemo"  
    3.     android:versionCode="1"  
    4.     android:versionName="1.0" >  
    5.   
    6.     <uses-sdk  
    7.         android:minSdkVersion="8"  
    8.         android:targetSdkVersion="8" />  
    9.       
    10.     <uses-permission android:name="android.permission.READ_CONTACTS"></uses-permission>  
    11.   
    12.     <application  
    13.         android:allowBackup="true"  
    14.         android:icon="@drawable/ic_launcher"  
    15.         android:label="@string/app_name"  
    16.         android:theme="@android:style/Theme.NoTitleBar"  
    17.         >  
    18.         <activity  
    19.             android:name="com.example.contactsdemo.MainActivity"  
    20.             android:label="@string/app_name" >  
    21.             <intent-filter>  
    22.                 <action android:name="android.intent.action.MAIN" />  
    23.   
    24.                 <category android:name="android.intent.category.LAUNCHER" />  
    25.             </intent-filter>  
    26.         </activity>  
    27.     </application>  
    28.   
    29. </manifest>  

    现在我们来运行一下程序,效果如下图所示:

                                                   

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

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

    源码下载,请点击这里

  • 相关阅读:
    UVa 10213
    树莓派学习路程No.2 GPIO功能初识 wiringPi安装
    树莓派学习路程No.1 树莓派系统安装与登录 更换软件源 配置wifi
    《软件工程》 的课程总结附加题。
    软件工程《个人总结》
    Java super关键字活用
    软件工程:黄金G点小游戏1.0
    Android之获取数据库路径
    Android自定义折线图
    软件工程:vs单元测试
  • 原文地址:https://www.cnblogs.com/zhuzhengwen1983/p/3603749.html
Copyright © 2011-2022 走看看