android自定义listview实现header悬浮框效果

时间:2022-12-16 19:56:31


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


android自定义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​​ ​​co​​



  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​​ ​​c​​



  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,通过数据来控制其显示或隐藏,实现原理如下图:

android自定义listview实现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字母排序和过滤搜索功能,实现汉字转成拼音​​


最后来一张截图:




android自定义listview实现header悬浮框效果