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

之前在使用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 plaincopyandroid自定义listview实现header悬浮框效果android自定义listview实现header悬浮框效果
  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 plaincopyandroid自定义listview实现header悬浮框效果android自定义listview实现header悬浮框效果
  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 plaincopyandroid自定义listview实现header悬浮框效果android自定义listview实现header悬浮框效果
  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.             mHeaderViewVisible = false;  
  10.             break;  
  11.         }  
  12.   
  13.         case PinnedHeaderAdapter.PINNED_HEADER_VISIBLE: {  
  14.             mAdapter.configurePinnedHeader(mHeaderView, position, MAX_ALPHA);  
  15.             if (mHeaderView.getTop() != 0) {  
  16.                 mHeaderView.layout(00, mHeaderViewWidth, mHeaderViewHeight);  
  17.             }  
  18.             mHeaderViewVisible = true;  
  19.             break;  
  20.         }  
  21.   
  22.         case PinnedHeaderAdapter.PINNED_HEADER_PUSHED_UP: {  
  23.             View firstView = getChildAt(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.                 y = 0;  
  34.                 alpha = MAX_ALPHA;  
  35.             }  
  36.             mAdapter.configurePinnedHeader(mHeaderView, position, alpha);  
  37.             if (mHeaderView.getTop() != y) {  
  38.                 mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);  
  39.             }  
  40.             mHeaderViewVisible = true;  
  41.             break;  
  42.         }  
  43.     }  
  44. }  
1.4、onLayout和onMeasure
在这两个方法中,控制Header View的位置及大小
[java] view plaincopyandroid自定义listview实现header悬浮框效果android自定义listview实现header悬浮框效果
  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.         mHeaderView.layout(00, 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 plaincopyandroid自定义listview实现header悬浮框效果android自定义listview实现header悬浮框效果
  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.                 itemView = new TextView(SectionListView.this);  
  76.                 itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
  77.                         mItemHeight));  
  78.                 itemView.setTextSize(16);  
  79.                 itemView.setPadding(10000);  
  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.         Contact itemNext = (Contact) getItem(position + 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,第二个itemsection的话,就返回状态PINNED_HEADER_PUSHED_UP,否则返回PINNED_HEADER_VISIBLE。
configurePinnedHeader方法中,就是将item的section字符串设置到header view上面去。

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

数据结构Contact的定义如下:
[java] view plaincopyandroid自定义listview实现header悬浮框效果android自定义listview实现header悬浮框效果
  1. public class Contact {  
  2.     int id;  
  3.     String name;  
  4.     String pinyin;  
  5.     String sortLetter = "#";  
  6.     String sectionStr;  
  7.     String phoneNumber;  
  8.     boolean isSection;  
  9.     static CharacterParser sParser = CharacterParser.getInstance();  
  10.       
  11.     Contact() {  
  12.           
  13.     }  
  14.       
  15.     Contact(int id, String name) {  
  16.         this.id = id;  
  17.         this.name = name;  
  18.         this.pinyin = sParser.getSpelling(name);  
  19.         if (!TextUtils.isEmpty(pinyin)) {  
  20.             String sortString = this.pinyin.substring(01).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 plaincopyandroid自定义listview实现header悬浮框效果android自定义listview实现header悬浮框效果
  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.         mItemHeight = (int) (density * mItemHeight);  
  34.         mSecHeight = (int) (density * mSecHeight);  
  35.           
  36.         PinnedHeaderListView mListView = new PinnedHeaderListView(this);  
  37.         mListView.setAdapter(new ListViewAdapter(ContactLoader.getInstance().getContacts(this)));  
  38.         mListView.setPinnedHeaderView(getHeaderView());  
  39.         mListView.setBackgroundColor(Color.argb(255202020));  
  40.         mListView.setOnItemClickListener(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.                 Toast.makeText(SectionListView.this, data.toString(), Toast.LENGTH_SHORT).show();  
  46.             }  
  47.         });  
  48.   
  49.         setContentView(mListView);  
  50.     }  
  51.       
  52.     private View getHeaderView() {  
  53.         TextView itemView = new TextView(SectionListView.this);  
  54.         itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
  55.                 mSecHeight));  
  56.         itemView.setGravity(Gravity.CENTER_VERTICAL);  
  57.         itemView.setBackgroundColor(Color.WHITE);  
  58.         itemView.setTextSize(20);  
  59.         itemView.setTextColor(Color.GRAY);  
  60.         itemView.setBackgroundResource(R.drawable.section_listview_header_bg);  
  61.         itemView.setPadding(1000, 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.                     itemView = new TextView(SectionListView.this);  
  141.                     itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
  142.                             mItemHeight));  
  143.                     itemView.setTextSize(16);  
  144.                     itemView.setPadding(10000);  
  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.             Contact itemNext = (Contact) getItem(position + 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实现header悬浮框效果
上一篇:JS字符串拼接对比


下一篇:浏览器的并发数列表