之前在使用iOS时,看到过一种分组的View,每一组都有一个Header,在上下滑动的时候,会有一个悬浮的Header,这种体验觉得很不错,请看下图:

Anroid ListView分组和悬浮Header实现_数据

上图中标红的1,2,3,4四张图中,当向上滑动时,仔细观察灰色条的Header变化,当第二组向上滑动时,会把第一组的悬浮Header挤上去。

这种效果在Android是没有的,iOS的SDK就自带这种效果。这篇文章就介绍如何在Android实现这种效果。

1、悬浮Header的实现



其实Android自带的联系人的App中就有这样的效果,我也是把他的类直接拿过来的,实现了 PinnedHeaderListView这么一个类,扩展于 ListView,核心原理就是在ListView的最顶部 绘制一个调用者设置的Header View,在滑动的时候,根据一些状态来决定是否向上或向下移动Header View(其实就是调用其layout方法,理论上在绘制那里作一些平移也是可以的)。下面说一下具体的实现:



1.1、PinnedHeaderAdapter接口



这个接口需要ListView的Adapter来实现,它定义了两个方法,一个是让Adapter告诉ListView当前指定的position的数据的状态,比如指定position的数据可能是组的header;另一个方法就是设置Header View,比如设置Header View的文本,图片等,这个方法是由调用者去实现的。



[java]  view plain cop


1. /**
2.  * Adapter interface.  The list adapter must implement this interface.
3.  */  
4. public interface PinnedHeaderAdapter {  
5.   
6. /**
7.      * Pinned header state: don't show the header.
8.      */  
9. public static final int PINNED_HEADER_GONE = 0;  
10.   
11. /**
12.      * Pinned header state: show the header at the top of the list.
13.      */  
14. public static final int PINNED_HEADER_VISIBLE = 1;  
15.   
16. /**
17.      * Pinned header state: show the header. If the header extends beyond
18.      * the bottom of the first shown element, push it up and clip.
19.      */  
20. public static final int PINNED_HEADER_PUSHED_UP = 2;  
21.   
22. /**
23.      * Computes the desired state of the pinned header for the given
24.      * position of the first visible list item. Allowed return values are
25.      * {@link #PINNED_HEADER_GONE}, {@link #PINNED_HEADER_VISIBLE} or
26.      * {@link #PINNED_HEADER_PUSHED_UP}.
27.      */  
28. int getPinnedHeaderState(int position);  
29.   
30. /**
31.      * Configures the pinned header view to match the first visible list item.
32.      *
33.      * @param header pinned header view.
34.      * @param position position of the first visible list item.
35.      * @param alpha fading of the header view, between 0 and 255.
36.      */  
37. void configurePinnedHeader(View header, int position, int alpha);  
38. }

1.2、如何绘制Header View



这是在dispatchDraw方法中绘制的:



[java]  view plain cop


1. @Override  
2. protected void dispatchDraw(Canvas canvas) {  
3. super.dispatchDraw(canvas);  
4. if (mHeaderViewVisible) {  
5.         drawChild(canvas, mHeaderView, getDrawingTime());  
6.     }  
7. }

1.3、配置Header View



核心就是根据不同的状态值来控制Header View的状态,比如PINNED_HEADER_GONE(隐藏)的情况,可能需要设置一个flag标记,不绘制Header View,那么就达到隐藏的效果。当PINNED_HEADER_PUSHED_UP状态时,可能需要根据不同的位移来计算Header View的移动位移。下面是具体的实现:


[java]  view plain co

 


1. public void configureHeaderView(int position) {  
2. if (mHeaderView == null || null == mAdapter) {  
3. return;  
4.     }  
5.       
6. int state = mAdapter.getPinnedHeaderState(position);  
7. switch (state) {  
8. case PinnedHeaderAdapter.PINNED_HEADER_GONE: {  
9. false;  
10. break;  
11.         }  
12.   
13. case PinnedHeaderAdapter.PINNED_HEADER_VISIBLE: {  
14.             mAdapter.configurePinnedHeader(mHeaderView, position, MAX_ALPHA);  
15. if (mHeaderView.getTop() != 0) {  
16. 0, 0, mHeaderViewWidth, mHeaderViewHeight);  
17.             }  
18. true;  
19. break;  
20.         }  
21.   
22. case PinnedHeaderAdapter.PINNED_HEADER_PUSHED_UP: {  
23. 0);  
24. int bottom = firstView.getBottom();  
25. int itemHeight = firstView.getHeight();  
26. int headerHeight = mHeaderView.getHeight();  
27. int y;  
28. int alpha;  
29. if (bottom < headerHeight) {  
30.                 y = (bottom - headerHeight);  
31.                 alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;  
32. else {  
33. 0;  
34.                 alpha = MAX_ALPHA;  
35.             }  
36.             mAdapter.configurePinnedHeader(mHeaderView, position, alpha);  
37. if (mHeaderView.getTop() != y) {  
38. 0, y, mHeaderViewWidth, mHeaderViewHeight + y);  
39.             }  
40. true;  
41. break;  
42.         }  
43.     }  
44. }

1.4、onLayout和onMeasure



在这两个方法中,控制Header View的位置及大小



[java]  view plain cop

 



1. @Override  
2. protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
3. super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
4. if (mHeaderView != null) {  
5.         measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);  
6.         mHeaderViewWidth = mHeaderView.getMeasuredWidth();  
7.         mHeaderViewHeight = mHeaderView.getMeasuredHeight();  
8.     }  
9. }  
10.   
11. @Override  
12. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  
13. super.onLayout(changed, left, top, right, bottom);  
14. if (mHeaderView != null) {  
15. 0, 0, mHeaderViewWidth, mHeaderViewHeight);  
16.         configureHeaderView(getFirstVisiblePosition());  
17.     }  
18. }

好了,到这里,悬浮Header View就完了,各位可能看不到完整的代码,只要明白这几个核心的方法,自己写出来,也差不多了。



2、ListView Section实现



有两种方法实现ListView Section效果,请参考http://cyrilmottier.com/2011/07/05/listview-tips-tricks-2-section-your-listview/



方法一:



每一个ItemView中包含Header,通过数据来控制其显示或隐藏,实现原理如下图:

Anroid ListView分组和悬浮Header实现_java_02


 


优点:


1,实现简单,在Adapter.getView的实现中,只需要根据数据来判断是否是header,不是的话,隐藏Item view中的header部分,否则显示。


2,Adapter.getItem(int n)始终返回的数据是在数据列表中对应的第n个数据,这样容易理解。


3,控制header的点击事件更加容易


缺点:


1、使用更多的内存,第一个Item view中都包含一个header view,这样会费更多的内存,多数时候都可能header都是隐藏的。


 


方法二:


使用不同类型的View:重写getItemViewType(int)和getViewTypeCount()方法。


 


优点:


1,允许多个不同类型的item


2,理解更加简单


缺点:


1,实现比较复杂


2,得到指定位置的数据变得复杂一些


 


到这里,我的实现方式是选择第二种方案,尽管它的实现方式要复杂一些,但优点比较明显。


 


3、Adapter的实现


这里主要就是说一下getPinnedHeaderState和configurePinnedHeader这两个方法的实现


[java]  view plain copy

 


 

1. private class ListViewAdapter extends BaseAdapter implements PinnedHeaderAdapter {  
2.       
3. private ArrayList<Contact> mDatas;  
4. private static final int TYPE_CATEGORY_ITEM = 0;    
5. private static final int TYPE_ITEM = 1;    
6.       
7. public ListViewAdapter(ArrayList<Contact> datas) {  
8.         mDatas = datas;  
9.     }  
10.       
11. @Override  
12. public boolean areAllItemsEnabled() {  
13. return false;  
14.     }  
15.       
16. @Override  
17. public boolean isEnabled(int position) {  
18. // 异常情况处理    
19. if (null == mDatas || position <  0|| position > getCount()) {  
20. return true;  
21.         }   
22.           
23.         Contact item = mDatas.get(position);  
24. if (item.isSection) {  
25. return false;  
26.         }  
27.           
28. return true;  
29.     }  
30.       
31. @Override  
32. public int getCount() {  
33. return mDatas.size();  
34.     }  
35.       
36. @Override  
37. public int getItemViewType(int position) {  
38. // 异常情况处理    
39. if (null == mDatas || position <  0|| position > getCount()) {  
40. return TYPE_ITEM;  
41.         }   
42.           
43.         Contact item = mDatas.get(position);  
44. if (item.isSection) {  
45. return TYPE_CATEGORY_ITEM;  
46.         }  
47.           
48. return TYPE_ITEM;  
49.     }  
50.   
51. @Override  
52. public int getViewTypeCount() {  
53. return 2;  
54.     }  
55.   
56. @Override  
57. public Object getItem(int position) {  
58. return (position >= 0 && position < mDatas.size()) ? mDatas.get(position) : 0;  
59.     }  
60.   
61. @Override  
62. public long getItemId(int position) {  
63. return 0;  
64.     }  
65.   
66. @Override  
67. public View getView(int position, View convertView, ViewGroup parent) {  
68. int itemViewType = getItemViewType(position);  
69.         Contact data = (Contact) getItem(position);  
70.         TextView itemView;  
71.           
72. switch (itemViewType) {  
73. case TYPE_ITEM:  
74. if (null == convertView) {  
75. new TextView(SectionListView.this);  
76. new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
77.                         mItemHeight));  
78. 16);  
79. 10, 0, 0, 0);  
80.                 itemView.setGravity(Gravity.CENTER_VERTICAL);  
81. //itemView.setBackgroundColor(Color.argb(255, 20, 20, 20));  
82.                 convertView = itemView;  
83.             }  
84.               
85.             itemView = (TextView) convertView;  
86.             itemView.setText(data.toString());  
87. break;  
88.               
89. case TYPE_CATEGORY_ITEM:  
90. if (null == convertView) {  
91.                 convertView = getHeaderView();  
92.             }  
93.             itemView = (TextView) convertView;  
94.             itemView.setText(data.toString());  
95. break;  
96.         }  
97.           
98. return convertView;  
99.     }  
100.   
101. @Override  
102. public int getPinnedHeaderState(int position) {  
103. if (position < 0) {  
104. return PINNED_HEADER_GONE;  
105.         }  
106.           
107.         Contact item = (Contact) getItem(position);  
108. 1);  
109. boolean isSection = item.isSection;  
110. boolean isNextSection = (null != itemNext) ? itemNext.isSection : false;  
111. if (!isSection && isNextSection) {  
112. return PINNED_HEADER_PUSHED_UP;  
113.         }  
114.           
115. return PINNED_HEADER_VISIBLE;  
116.     }  
117.   
118. @Override  
119. public void configurePinnedHeader(View header, int position, int alpha) {  
120.         Contact item = (Contact) getItem(position);  
121. if (null != item) {  
122. if (header instanceof TextView) {  
123.                 ((TextView) header).setText(item.sectionStr);  
124.             }  
125.         }  
126.     }  
127. }

getPinnedHeaderState方法中,如果第一个item 不是section,第二个item 是section的话,就返回状态PINNED_HEADER_PUSHED_UP,否则返回PINNED_HEADER_VISIBLE。


configurePinnedHeader方法中,就是将item的section字符串设置到header view上面去。


 


【重要说明】


Adapter中的数据里面已经包含了section(header)的数据,数据结构中有一个方法来标识它是否是section。那么,在点击事件就要注意了,通过position可能返回的是section数据结构。


 

数据结构Contact的定义如下:

[java]  view plain copy

 


1. public class Contact {  
2. int id;  
3.     String name;  
4.     String pinyin;  
5. "#";  
6.     String sectionStr;  
7.     String phoneNumber;  
8. boolean isSection;  
9. static CharacterParser sParser = CharacterParser.getInstance();  
10.       
11.     Contact() {  
12.           
13.     }  
14.       
15. int id, String name) {  
16. this.id = id;  
17. this.name = name;  
18. this.pinyin = sParser.getSpelling(name);  
19. if (!TextUtils.isEmpty(pinyin)) {  
20. this.pinyin.substring(0, 1).toUpperCase();  
21. if (sortString.matches("[A-Z]")) {  
22. this.sortLetter = sortString.toUpperCase();  
23. else {  
24. this.sortLetter = "#";  
25.             }  
26.         }  
27.     }  
28.       
29. @Override  
30. public String toString() {  
31. if (isSection) {  
32. return name;  
33. else {  
34. //return name + " (" + sortLetter + ", " + pinyin + ")";  
35. return name + " (" + phoneNumber + ")";  
36.         }  
37.     }  
38. }


 

完整的代码

[java]  view plain copy

 

1. package com.lee.sdk.test.section;  
2.   
3. import java.util.ArrayList;  
4.   
5. import android.graphics.Color;  
6. import android.os.Bundle;  
7. import android.view.Gravity;  
8. import android.view.View;  
9. import android.view.ViewGroup;  
10. import android.widget.AbsListView;  
11. import android.widget.AdapterView;  
12. import android.widget.AdapterView.OnItemClickListener;  
13. import android.widget.BaseAdapter;  
14. import android.widget.TextView;  
15. import android.widget.Toast;  
16.   
17. import com.lee.sdk.test.GABaseActivity;  
18. import com.lee.sdk.test.R;  
19. import com.lee.sdk.widget.PinnedHeaderListView;  
20. import com.lee.sdk.widget.PinnedHeaderListView.PinnedHeaderAdapter;  
21.   
22. public class SectionListView extends GABaseActivity {  
23.   
24. private int mItemHeight = 55;  
25. private int mSecHeight = 25;  
26.       
27. @Override  
28. protected void onCreate(Bundle savedInstanceState) {  
29. super.onCreate(savedInstanceState);  
30.         setContentView(R.layout.activity_main);  
31.           
32. float density = getResources().getDisplayMetrics().density;  
33. int) (density * mItemHeight);  
34. int) (density * mSecHeight);  
35.           
36. new PinnedHeaderListView(this);  
37. new ListViewAdapter(ContactLoader.getInstance().getContacts(this)));  
38.         mListView.setPinnedHeaderView(getHeaderView());  
39. 255, 20, 20, 20));  
40. new OnItemClickListener() {  
41. @Override  
42. public void onItemClick(AdapterView<?> parent, View view, int position, long id) {  
43.                 ListViewAdapter adapter = ((ListViewAdapter) parent.getAdapter());  
44.                 Contact data = (Contact) adapter.getItem(position);  
45. this, data.toString(), Toast.LENGTH_SHORT).show();  
46.             }  
47.         });  
48.   
49.         setContentView(mListView);  
50.     }  
51.       
52. private View getHeaderView() {  
53. new TextView(SectionListView.this);  
54. new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
55.                 mSecHeight));  
56.         itemView.setGravity(Gravity.CENTER_VERTICAL);  
57.         itemView.setBackgroundColor(Color.WHITE);  
58. 20);  
59.         itemView.setTextColor(Color.GRAY);  
60.         itemView.setBackgroundResource(R.drawable.section_listview_header_bg);  
61. 10, 0, 0, itemView.getPaddingBottom());  
62.           
63. return itemView;  
64.     }  
65.   
66. private class ListViewAdapter extends BaseAdapter implements PinnedHeaderAdapter {  
67.           
68. private ArrayList<Contact> mDatas;  
69. private static final int TYPE_CATEGORY_ITEM = 0;    
70. private static final int TYPE_ITEM = 1;    
71.           
72. public ListViewAdapter(ArrayList<Contact> datas) {  
73.             mDatas = datas;  
74.         }  
75.           
76. @Override  
77. public boolean areAllItemsEnabled() {  
78. return false;  
79.         }  
80.           
81. @Override  
82. public boolean isEnabled(int position) {  
83. // 异常情况处理    
84. if (null == mDatas || position <  0|| position > getCount()) {  
85. return true;  
86.             }   
87.               
88.             Contact item = mDatas.get(position);  
89. if (item.isSection) {  
90. return false;  
91.             }  
92.               
93. return true;  
94.         }  
95.           
96. @Override  
97. public int getCount() {  
98. return mDatas.size();  
99.         }  
100.           
101. @Override  
102. public int getItemViewType(int position) {  
103. // 异常情况处理    
104. if (null == mDatas || position <  0|| position > getCount()) {  
105. return TYPE_ITEM;  
106.             }   
107.               
108.             Contact item = mDatas.get(position);  
109. if (item.isSection) {  
110. return TYPE_CATEGORY_ITEM;  
111.             }  
112.               
113. return TYPE_ITEM;  
114.         }  
115.   
116. @Override  
117. public int getViewTypeCount() {  
118. return 2;  
119.         }  
120.   
121. @Override  
122. public Object getItem(int position) {  
123. return (position >= 0 && position < mDatas.size()) ? mDatas.get(position) : 0;  
124.         }  
125.   
126. @Override  
127. public long getItemId(int position) {  
128. return 0;  
129.         }  
130.   
131. @Override  
132. public View getView(int position, View convertView, ViewGroup parent) {  
133. int itemViewType = getItemViewType(position);  
134.             Contact data = (Contact) getItem(position);  
135.             TextView itemView;  
136.               
137. switch (itemViewType) {  
138. case TYPE_ITEM:  
139. if (null == convertView) {  
140. new TextView(SectionListView.this);  
141. new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
142.                             mItemHeight));  
143. 16);  
144. 10, 0, 0, 0);  
145.                     itemView.setGravity(Gravity.CENTER_VERTICAL);  
146. //itemView.setBackgroundColor(Color.argb(255, 20, 20, 20));  
147.                     convertView = itemView;  
148.                 }  
149.                   
150.                 itemView = (TextView) convertView;  
151.                 itemView.setText(data.toString());  
152. break;  
153.                   
154. case TYPE_CATEGORY_ITEM:  
155. if (null == convertView) {  
156.                     convertView = getHeaderView();  
157.                 }  
158.                 itemView = (TextView) convertView;  
159.                 itemView.setText(data.toString());  
160. break;  
161.             }  
162.               
163. return convertView;  
164.         }  
165.   
166. @Override  
167. public int getPinnedHeaderState(int position) {  
168. if (position < 0) {  
169. return PINNED_HEADER_GONE;  
170.             }  
171.               
172.             Contact item = (Contact) getItem(position);  
173. 1);  
174. boolean isSection = item.isSection;  
175. boolean isNextSection = (null != itemNext) ? itemNext.isSection : false;  
176. if (!isSection && isNextSection) {  
177. return PINNED_HEADER_PUSHED_UP;  
178.             }  
179.               
180. return PINNED_HEADER_VISIBLE;  
181.         }  
182.   
183. @Override  
184. public void configurePinnedHeader(View header, int position, int alpha) {  
185.             Contact item = (Contact) getItem(position);  
186. if (null != item) {  
187. if (header instanceof TextView) {  
188.                     ((TextView) header).setText(item.sectionStr);  
189.                 }  
190.             }  
191.         }  
192.     }  
193. }

关于数据加载,分组的逻辑这里就不列出了,数据分组请参考:

Android 实现ListView的A-Z字母排序和过滤搜索功能,实现汉字转成拼音


最后来一张截图:


 


Anroid ListView分组和悬浮Header实现_java_03