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

    xiaoxiao2026-02-25  8

    之前在使用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 copy /**   * 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方法中绘制的: [java]  view plain copy @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的移动位移。下面是具体的实现: [java]  view plain copy 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(00, 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的位置及大小 [java]  view plain copy @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(00, 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这两个方法的实现 [java]  view plain copy 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(10000);                   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的定义如下: [java]  view plain copy 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(01).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 + ")";           }       }   }     完整的代码 [java]  view plain copy 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(255202020));           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(1000, 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(10000);                       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字母排序和过滤搜索功能,实现汉字转成拼音 最后来一张截图:
    最新回复(0)