之前在使用iOS时,看到过一种分组的View,每一组都有一个Header,在上下滑动的时候,会有一个悬浮的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,通过数据来控制其显示或隐藏,实现原理如下图:
优点:
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字母排序和过滤搜索功能,实现汉字转成拼音
最后来一张截图: