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

时间:2022-04-25
本文章向大家介绍android自定义listview实现header悬浮框效果,主要内容包括1、悬浮Header的实现、2、ListView Section实现、3、Adapter的实现、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

之前在使用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的文本,图片等,这个方法是由调用者去实现的。

 /** 
  * Adapter interface.  The list adapter must implement this interface. 
  */ 
 public interface PinnedHeaderAdapter {  
  
  /** 
      * Pinned header state: don't show the header. 
      */ 
  public static final int PINNED_HEADER_GONE = 0;  
  
  /** 
      * Pinned header state: show the header at the top of the list. 
      */ 
  public static final int PINNED_HEADER_VISIBLE = 1;  
  
  /** 
      * Pinned header state: show the header. If the header extends beyond 
      * the bottom of the first shown element, push it up and clip. 
      */ 
  public static final int PINNED_HEADER_PUSHED_UP = 2;  
  
  /** 
      * Computes the desired state of the pinned header for the given 
      * position of the first visible list item. Allowed return values are 
      * {@link #PINNED_HEADER_GONE}, {@link #PINNED_HEADER_VISIBLE} or 
      * {@link #PINNED_HEADER_PUSHED_UP}. 
      */ 
  int getPinnedHeaderState(int position);  
  
  /** 
      * Configures the pinned header view to match the first visible list item. 
      * 
      * @param header pinned header view. 
      * @param position position of the first visible list item. 
      * @param alpha fading of the header view, between 0 and 255. 
      */ 
  void configurePinnedHeader(View header, int position, int alpha);  
 }  

 1.2、如何绘制Header View

这是在dispatchDraw方法中绘制的:

 @Override 
 protected void dispatchDraw(Canvas canvas) {  
  super.dispatchDraw(canvas);  
  if (mHeaderViewVisible) {  
         drawChild(canvas, mHeaderView, getDrawingTime());  
     }  
 }  

1.3、配置Header View

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

 public void configureHeaderView(int position) {  
  if (mHeaderView == null || null == mAdapter) {  
  return;  
     }  
  
  int state = mAdapter.getPinnedHeaderState(position);  
  switch (state) {  
  case PinnedHeaderAdapter.PINNED_HEADER_GONE: {  
             mHeaderViewVisible = false;  
  break;  
         }  
  
  case PinnedHeaderAdapter.PINNED_HEADER_VISIBLE: {  
             mAdapter.configurePinnedHeader(mHeaderView, position, MAX_ALPHA);  
  if (mHeaderView.getTop() != 0) {  
                 mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);  
             }  
             mHeaderViewVisible = true;  
  break;  
         }  
  
  case PinnedHeaderAdapter.PINNED_HEADER_PUSHED_UP: {  
             View firstView = getChildAt(0);  
  int bottom = firstView.getBottom();  
  int itemHeight = firstView.getHeight();  
  int headerHeight = mHeaderView.getHeight();  
  int y;  
  int alpha;  
  if (bottom < headerHeight) {  
                 y = (bottom - headerHeight);  
                 alpha = MAX_ALPHA * (headerHeight + y) / headerHeight;  
             } else {  
                 y = 0;  
                 alpha = MAX_ALPHA;  
             }  
             mAdapter.configurePinnedHeader(mHeaderView, position, alpha);  
  if (mHeaderView.getTop() != y) {  
                 mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);  
             }  
             mHeaderViewVisible = true;  
  break;  
         }  
     }  
 }  

1.4、onLayout和onMeasure

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

 @Override 
 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
  if (mHeaderView != null) {  
         measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);  
         mHeaderViewWidth = mHeaderView.getMeasuredWidth();  
         mHeaderViewHeight = mHeaderView.getMeasuredHeight();  
     }  
 }  
  
 @Override 
 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {  
  super.onLayout(changed, left, top, right, bottom);  
  if (mHeaderView != null) {  
         mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);  
         configureHeaderView(getFirstVisiblePosition());  
     }  
 }  

好了,到这里,悬浮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这两个方法的实现

 private class ListViewAdapter extends BaseAdapter implements PinnedHeaderAdapter {  
  
  private ArrayList<Contact> mDatas;  
  private static final int TYPE_CATEGORY_ITEM = 0;    
  private static final int TYPE_ITEM = 1;    
  
  public ListViewAdapter(ArrayList<Contact> datas) {  
         mDatas = datas;  
     }  
  
  @Override 
  public boolean areAllItemsEnabled() {  
  return false;  
     }  
  
  @Override 
  public boolean isEnabled(int position) {  
  // 异常情况处理   
  if (null == mDatas || position <  0|| position > getCount()) {  
  return true;  
         }   
  
         Contact item = mDatas.get(position);  
  if (item.isSection) {  
  return false;  
         }  
  
  return true;  
     }  
  
  @Override 
  public int getCount() {  
  return mDatas.size();  
     }  
  
  @Override 
  public int getItemViewType(int position) {  
  // 异常情况处理   
  if (null == mDatas || position <  0|| position > getCount()) {  
  return TYPE_ITEM;  
         }   
  
         Contact item = mDatas.get(position);  
  if (item.isSection) {  
  return TYPE_CATEGORY_ITEM;  
         }  
  
  return TYPE_ITEM;  
     }  
  
  @Override 
  public int getViewTypeCount() {  
  return 2;  
     }  
  
  @Override 
  public Object getItem(int position) {  
  return (position >= 0 && position < mDatas.size()) ? mDatas.get(position) : 0;  
     }  
  
  @Override 
  public long getItemId(int position) {  
  return 0;  
     }  
  
  @Override 
  public View getView(int position, View convertView, ViewGroup parent) {  
  int itemViewType = getItemViewType(position);  
         Contact data = (Contact) getItem(position);  
         TextView itemView;  
  
  switch (itemViewType) {  
  case TYPE_ITEM:  
  if (null == convertView) {  
                 itemView = new TextView(SectionListView.this);  
                 itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
                         mItemHeight));  
                 itemView.setTextSize(16);  
                 itemView.setPadding(10, 0, 0, 0);  
                 itemView.setGravity(Gravity.CENTER_VERTICAL);  
  //itemView.setBackgroundColor(Color.argb(255, 20, 20, 20)); 
                 convertView = itemView;  
             }  
  
             itemView = (TextView) convertView;  
             itemView.setText(data.toString());  
  break;  
  
  case TYPE_CATEGORY_ITEM:  
  if (null == convertView) {  
                 convertView = getHeaderView();  
             }  
             itemView = (TextView) convertView;  
             itemView.setText(data.toString());  
  break;  
         }  
  
  return convertView;  
     }  
  
  @Override 
  public int getPinnedHeaderState(int position) {  
  if (position < 0) {  
  return PINNED_HEADER_GONE;  
         }  
  
         Contact item = (Contact) getItem(position);  
         Contact itemNext = (Contact) getItem(position + 1);  
  boolean isSection = item.isSection;  
  boolean isNextSection = (null != itemNext) ? itemNext.isSection : false;  
  if (!isSection && isNextSection) {  
  return PINNED_HEADER_PUSHED_UP;  
         }  
  
  return PINNED_HEADER_VISIBLE;  
     }  
  
  @Override 
  public void configurePinnedHeader(View header, int position, int alpha) {  
         Contact item = (Contact) getItem(position);  
  if (null != item) {  
  if (header instanceof TextView) {  
                 ((TextView) header).setText(item.sectionStr);  
             }  
         }  
     }  
 }  

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

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

【重要说明】

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

数据结构Contact的定义如下:

 public class Contact {  
  int id;  
     String name;  
     String pinyin;  
     String sortLetter = "#";  
     String sectionStr;  
     String phoneNumber;  
  boolean isSection;  
  static CharacterParser sParser = CharacterParser.getInstance();  
  
     Contact() {  
  
     }  
  
     Contact(int id, String name) {  
  this.id = id;  
  this.name = name;  
  this.pinyin = sParser.getSpelling(name);  
  if (!TextUtils.isEmpty(pinyin)) {  
             String sortString = this.pinyin.substring(0, 1).toUpperCase();  
  if (sortString.matches("[A-Z]")) {  
  this.sortLetter = sortString.toUpperCase();  
             } else {  
  this.sortLetter = "#";  
             }  
         }  
     }  
  
  @Override 
  public String toString() {  
  if (isSection) {  
  return name;  
         } else {  
  //return name + " (" + sortLetter + ", " + pinyin + ")"; 
  return name + " (" + phoneNumber + ")";  
         }  
     }  
 }    

完整的代码

 package com.lee.sdk.test.section;  
  
 import java.util.ArrayList;  
  
 import android.graphics.Color;  
 import android.os.Bundle;  
 import android.view.Gravity;  
 import android.view.View;  
 import android.view.ViewGroup;  
 import android.widget.AbsListView;  
 import android.widget.AdapterView;  
 import android.widget.AdapterView.OnItemClickListener;  
 import android.widget.BaseAdapter;  
 import android.widget.TextView;  
 import android.widget.Toast;  
  
 import com.lee.sdk.test.GABaseActivity;  
 import com.lee.sdk.test.R;  
 import com.lee.sdk.widget.PinnedHeaderListView;  
 import com.lee.sdk.widget.PinnedHeaderListView.PinnedHeaderAdapter;  
  
 public class SectionListView extends GABaseActivity {  
  
  private int mItemHeight = 55;  
  private int mSecHeight = 25;  
  
  @Override 
  protected void onCreate(Bundle savedInstanceState) {  
  super.onCreate(savedInstanceState);  
         setContentView(R.layout.activity_main);  
  
  float density = getResources().getDisplayMetrics().density;  
         mItemHeight = (int) (density * mItemHeight);  
         mSecHeight = (int) (density * mSecHeight);  
  
         PinnedHeaderListView mListView = new PinnedHeaderListView(this);  
         mListView.setAdapter(new ListViewAdapter(ContactLoader.getInstance().getContacts(this)));  
         mListView.setPinnedHeaderView(getHeaderView());  
         mListView.setBackgroundColor(Color.argb(255, 20, 20, 20));  
         mListView.setOnItemClickListener(new OnItemClickListener() {  
  @Override 
  public void onItemClick(AdapterView<?> parent, View view, int position, long id) {  
                 ListViewAdapter adapter = ((ListViewAdapter) parent.getAdapter());  
                 Contact data = (Contact) adapter.getItem(position);  
                 Toast.makeText(SectionListView.this, data.toString(), Toast.LENGTH_SHORT).show();  
             }  
         });  
  
         setContentView(mListView);  
     }  
  
  private View getHeaderView() {  
         TextView itemView = new TextView(SectionListView.this);  
         itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
                 mSecHeight));  
         itemView.setGravity(Gravity.CENTER_VERTICAL);  
         itemView.setBackgroundColor(Color.WHITE);  
         itemView.setTextSize(20);  
         itemView.setTextColor(Color.GRAY);  
         itemView.setBackgroundResource(R.drawable.section_listview_header_bg);  
         itemView.setPadding(10, 0, 0, itemView.getPaddingBottom());  
  
  return itemView;  
     }  
  
  private class ListViewAdapter extends BaseAdapter implements PinnedHeaderAdapter {  
  
  private ArrayList<Contact> mDatas;  
  private static final int TYPE_CATEGORY_ITEM = 0;    
  private static final int TYPE_ITEM = 1;    
  
  public ListViewAdapter(ArrayList<Contact> datas) {  
             mDatas = datas;  
         }  
  
  @Override 
  public boolean areAllItemsEnabled() {  
  return false;  
         }  
  
  @Override 
  public boolean isEnabled(int position) {  
  // 异常情况处理   
  if (null == mDatas || position <  0|| position > getCount()) {  
  return true;  
             }   
  
             Contact item = mDatas.get(position);  
  if (item.isSection) {  
  return false;  
             }  
  
  return true;  
         }  
  
  @Override 
  public int getCount() {  
  return mDatas.size();  
         }  
  
  @Override 
  public int getItemViewType(int position) {  
  // 异常情况处理   
  if (null == mDatas || position <  0|| position > getCount()) {  
  return TYPE_ITEM;  
             }   
  
             Contact item = mDatas.get(position);  
  if (item.isSection) {  
  return TYPE_CATEGORY_ITEM;  
             }  
  
  return TYPE_ITEM;  
         }  
  
  @Override 
  public int getViewTypeCount() {  
  return 2;  
         }  
  
  @Override 
  public Object getItem(int position) {  
  return (position >= 0 && position < mDatas.size()) ? mDatas.get(position) : 0;  
         }  
  
  @Override 
  public long getItemId(int position) {  
  return 0;  
         }  
  
  @Override 
  public View getView(int position, View convertView, ViewGroup parent) {  
  int itemViewType = getItemViewType(position);  
             Contact data = (Contact) getItem(position);  
             TextView itemView;  
  
  switch (itemViewType) {  
  case TYPE_ITEM:  
  if (null == convertView) {  
                     itemView = new TextView(SectionListView.this);  
                     itemView.setLayoutParams(new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,  
                             mItemHeight));  
                     itemView.setTextSize(16);  
                     itemView.setPadding(10, 0, 0, 0);  
                     itemView.setGravity(Gravity.CENTER_VERTICAL);  
  //itemView.setBackgroundColor(Color.argb(255, 20, 20, 20)); 
                     convertView = itemView;  
                 }  
  
                 itemView = (TextView) convertView;  
                 itemView.setText(data.toString());  
  break;  
  
  case TYPE_CATEGORY_ITEM:  
  if (null == convertView) {  
                     convertView = getHeaderView();  
                 }  
                 itemView = (TextView) convertView;  
                 itemView.setText(data.toString());  
  break;  
             }  
  
  return convertView;  
         }  
  
  @Override 
  public int getPinnedHeaderState(int position) {  
  if (position < 0) {  
  return PINNED_HEADER_GONE;  
             }  
  
             Contact item = (Contact) getItem(position);  
             Contact itemNext = (Contact) getItem(position + 1);  
  boolean isSection = item.isSection;  
  boolean isNextSection = (null != itemNext) ? itemNext.isSection : false;  
  if (!isSection && isNextSection) {  
  return PINNED_HEADER_PUSHED_UP;  
             }  
  
  return PINNED_HEADER_VISIBLE;  
         }  
  
  @Override 
  public void configurePinnedHeader(View header, int position, int alpha) {  
             Contact item = (Contact) getItem(position);  
  if (null != item) {  
  if (header instanceof TextView) {  
                     ((TextView) header).setText(item.sectionStr);  
                 }  
             }  
         }  
     }  
 }  

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

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

最后来一张截图: