之前实现过一次这种效果的ExpandableListView:
https://www.jb51.net/article/38482.htm,带效果比较挫,最近,在参考联系人源码PinnedHeaderListView,以及网上各位大侠的源码,封装了一个效果最好,而且使用最简单的IphoneTreeView,下面先看看效果图: 
 首先让我们看看封装得比较完善的IphoneTreeView: 
 
public class IphoneTreeView extends ExpandableListView implements 
OnScrollListener, OnGroupClickListener { 
public IphoneTreeView(Context context, AttributeSet attrs, int defStyle) { 
super(context, attrs, defStyle); 
registerListener(); 
} 
public IphoneTreeView(Context context, AttributeSet attrs) { 
super(context, attrs); 
registerListener(); 
} 
public IphoneTreeView(Context context) { 
super(context); 
registerListener(); 
} 
/** 
* Adapter 接口 . 列表必须实现此接口 . 
*/ 
public interface IphoneTreeHeaderAdapter { 
public static final int PINNED_HEADER_GONE = 0; 
public static final int PINNED_HEADER_VISIBLE = 1; 
public static final int PINNED_HEADER_PUSHED_UP = 2; 
/** 
* 获取 Header 的状态 
* 
* @param groupPosition 
* @param childPosition 
* @return 
* PINNED_HEADER_GONE,PINNED_HEADER_VISIBLE,PINNED_HEADER_PUSHED_UP 
* 其中之一 
*/ 
int getTreeHeaderState(int groupPosition, int childPosition); 
/** 
* 配置 QQHeader, 让 QQHeader 知道显示的内容 
* 
* @param header 
* @param groupPosition 
* @param childPosition 
* @param alpha 
*/ 
void configureTreeHeader(View header, int groupPosition, 
int childPosition, int alpha); 
/** 
* 设置组按下的状态 
* 
* @param groupPosition 
* @param status 
*/ 
void onHeadViewClick(int groupPosition, int status); 
/** 
* 获取组按下的状态 
* 
* @param groupPosition 
* @return 
*/ 
int getHeadViewClickStatus(int groupPosition); 
} 
private static final int MAX_ALPHA = 255; 
private IphoneTreeHeaderAdapter mAdapter; 
/** 
* 用于在列表头显示的 View,mHeaderViewVisible 为 true 才可见 
*/ 
private View mHeaderView; 
/** 
* 列表头是否可见 
*/ 
private boolean mHeaderViewVisible; 
private int mHeaderViewWidth; 
private int mHeaderViewHeight; 
public void setHeaderView(View view) { 
mHeaderView = view; 
AbsListView.LayoutParams lp = new AbsListView.LayoutParams( 
ViewGroup.LayoutParams.MATCH_PARENT, 
ViewGroup.LayoutParams.WRAP_CONTENT); 
view.setLayoutParams(lp); 
if (mHeaderView != null) { 
setFadingEdgeLength(0); 
} 
requestLayout(); 
} 
private void registerListener() { 
setOnScrollListener(this); 
setOnGroupClickListener(this); 
} 
/** 
* 点击 HeaderView 触发的事件 
*/ 
private void headerViewClick() { 
long packedPosition = getExpandableListPosition(this 
.getFirstVisiblePosition()); 
int groupPosition = ExpandableListView 
.getPackedPositionGroup(packedPosition); 
if (mAdapter.getHeadViewClickStatus(groupPosition) == 1) { 
this.collapseGroup(groupPosition); 
mAdapter.onHeadViewClick(groupPosition, 0); 
} else { 
this.expandGroup(groupPosition); 
mAdapter.onHeadViewClick(groupPosition, 1); 
} 
this.setSelectedGroup(groupPosition); 
} 
private float mDownX; 
private float mDownY; 
/** 
* 如果 HeaderView 是可见的 , 此函数用于判断是否点击了 HeaderView, 并对做相应的处理 , 因为 HeaderView 
* 是画上去的 , 所以设置事件监听是无效的 , 只有自行控制 . 
*/ 
@Override 
public boolean onTouchEvent(MotionEvent ev) { 
if (mHeaderViewVisible) { 
switch (ev.getAction()) { 
case MotionEvent.ACTION_DOWN: 
mDownX = ev.getX(); 
mDownY = ev.getY(); 
if (mDownX <= mHeaderViewWidth && mDownY <= mHeaderViewHeight) { 
return true; 
} 
break; 
case MotionEvent.ACTION_UP: 
float x = ev.getX(); 
float y = ev.getY(); 
float offsetX = Math.abs(x - mDownX); 
float offsetY = Math.abs(y - mDownY); 
// 如果 HeaderView 是可见的 , 点击在 HeaderView 内 , 那么触发 headerClick() 
if (x <= mHeaderViewWidth && y <= mHeaderViewHeight 
&& offsetX <= mHeaderViewWidth 
&& offsetY <= mHeaderViewHeight) { 
if (mHeaderView != null) { 
headerViewClick(); 
} 
return true; 
} 
break; 
default: 
break; 
} 
} 
return super.onTouchEvent(ev); 
} 
@Override 
public void setAdapter(ExpandableListAdapter adapter) { 
super.setAdapter(adapter); 
mAdapter = (IphoneTreeHeaderAdapter) adapter; 
} 
/** 
* 
* 点击了 Group 触发的事件 , 要根据根据当前点击 Group 的状态来 
*/ 
@Override 
public boolean onGroupClick(ExpandableListView parent, View v, 
int groupPosition, long id) { 
if (mAdapter.getHeadViewClickStatus(groupPosition) == 0) { 
mAdapter.onHeadViewClick(groupPosition, 1); 
parent.expandGroup(groupPosition); 
parent.setSelectedGroup(groupPosition); 
} else if (mAdapter.getHeadViewClickStatus(groupPosition) == 1) { 
mAdapter.onHeadViewClick(groupPosition, 0); 
parent.collapseGroup(groupPosition); 
} 
// 返回 true 才可以弹回第一行 , 不知道为什么 
return true; 
} 
@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(); 
} 
} 
private int mOldState = -1; 
@Override 
protected void onLayout(boolean changed, int left, int top, int right, 
int bottom) { 
super.onLayout(changed, left, top, right, bottom); 
final long flatPostion = getExpandableListPosition(getFirstVisiblePosition()); 
final int groupPos = ExpandableListView 
.getPackedPositionGroup(flatPostion); 
final int childPos = ExpandableListView 
.getPackedPositionChild(flatPostion); 
int state = mAdapter.getTreeHeaderState(groupPos, childPos); 
if (mHeaderView != null && mAdapter != null && state != mOldState) { 
mOldState = state; 
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight); 
} 
configureHeaderView(groupPos, childPos); 
} 
public void configureHeaderView(int groupPosition, int childPosition) { 
if (mHeaderView == null || mAdapter == null 
|| ((ExpandableListAdapter) mAdapter).getGroupCount() == 0) { 
return; 
} 
int state = mAdapter.getTreeHeaderState(groupPosition, childPosition); 
switch (state) { 
case IphoneTreeHeaderAdapter.PINNED_HEADER_GONE: { 
mHeaderViewVisible = false; 
break; 
} 
case IphoneTreeHeaderAdapter.PINNED_HEADER_VISIBLE: { 
mAdapter.configureTreeHeader(mHeaderView, groupPosition, 
childPosition, MAX_ALPHA); 
if (mHeaderView.getTop() != 0) { 
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight); 
} 
mHeaderViewVisible = true; 
break; 
} 
case IphoneTreeHeaderAdapter.PINNED_HEADER_PUSHED_UP: { 
View firstView = getChildAt(0); 
int bottom = firstView.getBottom(); 
// intitemHeight = 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.configureTreeHeader(mHeaderView, groupPosition, 
childPosition, alpha); 
if (mHeaderView.getTop() != y) { 
mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight 
+ y); 
} 
mHeaderViewVisible = true; 
break; 
} 
} 
} 
@Override 
/** 
* 列表界面更新时调用该方法(如滚动时) 
*/ 
protected void dispatchDraw(Canvas canvas) { 
super.dispatchDraw(canvas); 
if (mHeaderViewVisible) { 
// 分组栏是直接绘制到界面中,而不是加入到ViewGroup中 
drawChild(canvas, mHeaderView, getDrawingTime()); 
} 
} 
@Override 
public void onScroll(AbsListView view, int firstVisibleItem, 
int visibleItemCount, int totalItemCount) { 
final long flatPos = getExpandableListPosition(firstVisibleItem); 
int groupPosition = ExpandableListView.getPackedPositionGroup(flatPos); 
int childPosition = ExpandableListView.getPackedPositionChild(flatPos); 
configureHeaderView(groupPosition, childPosition); 
} 
@Override 
public void onScrollStateChanged(AbsListView view, int scrollState) { 
} 
} 
 使用起来也是比较简单的,先在布局文件中声明activity_main.xml: 
 
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 
xmlns:tools="http://schemas.android.com/tools" 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
tools:context=".MainActivity" > 
<com.way.iphonetreeview.IphoneTreeView 
android:id="@+id/iphone_tree_view" 
android:layout_width="match_parent" 
android:layout_height="match_parent" 
android:background="@android:color/transparent" 
android:cacheColorHint="@android:color/transparent" 
android:divider="@null" 
android:transcriptMode="normal" /> 
</RelativeLayout> 
 然后在MainActivity中调用,为了缩减代码,我把Adapter作为内部类放在MainActivity中了: 
 
public class MainActivity extends Activity { 
private LayoutInflater mInflater; 
private IphoneTreeView iphoneTreeView; 
@Override 
protected void onCreate(Bundle savedInstanceState) { 
super.onCreate(savedInstanceState); 
setContentView(R.layout.activity_main); 
initView(); 
} 
private void initView() { 
// TODO Auto-generated method stub 
mInflater = LayoutInflater.from(this); 
iphoneTreeView = (IphoneTreeView) findViewById(R.id.iphone_tree_view); 
iphoneTreeView.setHeaderView(getLayoutInflater().inflate( 
R.layout.list_head_view, iphoneTreeView, false)); 
iphoneTreeView.setGroupIndicator(null); 
iphoneTreeView.setAdapter(new IphoneTreeViewAdapter()); 
} 
public class IphoneTreeViewAdapter extends BaseExpandableListAdapter 
implements IphoneTreeHeaderAdapter { 
// Sample data set. children[i] contains the children (String[]) for 
// groups[i]. 
private HashMap<Integer, Integer> groupStatusMap; 
private String[] groups = { "第一组", "第二组", "第三组", "第四组" }; 
private String[][] children = { 
{ "Way", "Arnold", "Barry", "Chuck", "David", "Afghanistan", 
"Albania", "Belgium", "Lily", "Jim", "LiMing", "Jodan" }, 
{ "Ace", "Bandit", "Cha-Cha", "Deuce", "Bahamas", "China", 
"Dominica", "Jim", "LiMing", "Jodan" }, 
{ "Fluffy", "Snuggles", "Ecuador", "Ecuador", "Jim", "LiMing", 
"Jodan" }, 
{ "Goldy", "Bubbles", "Iceland", "Iran", "Italy", "Jim", 
"LiMing", "Jodan" } }; 
public IphoneTreeViewAdapter() { 
// TODO Auto-generated constructor stub 
groupStatusMap = new HashMap<Integer, Integer>(); 
} 
public Object getChild(int groupPosition, int childPosition) { 
return children[groupPosition][childPosition]; 
} 
public long getChildId(int groupPosition, int childPosition) { 
return childPosition; 
} 
public int getChildrenCount(int groupPosition) { 
return children[groupPosition].length; 
} 
public Object getGroup(int groupPosition) { 
return groups[groupPosition]; 
} 
public int getGroupCount() { 
return groups.length; 
} 
public long getGroupId(int groupPosition) { 
return groupPosition; 
} 
public boolean isChildSelectable(int groupPosition, int childPosition) { 
return true; 
} 
public boolean hasStableIds() { 
return true; 
} 
@Override 
public View getChildView(int groupPosition, int childPosition, 
boolean isLastChild, View convertView, ViewGroup parent) { 
// TODO Auto-generated method stub 
if (convertView == null) { 
convertView = mInflater.inflate(R.layout.list_item_view, null); 
} 
TextView tv = (TextView) convertView 
.findViewById(R.id.contact_list_item_name); 
tv.setText(getChild(groupPosition, childPosition).toString()); 
TextView state = (TextView) convertView 
.findViewById(R.id.cpntact_list_item_state); 
state.setText("爱生活...爱Android..."); 
return convertView; 
} 
@Override 
public View getGroupView(int groupPosition, boolean isExpanded, 
View convertView, ViewGroup parent) { 
// TODO Auto-generated method stub 
if (convertView == null) { 
convertView = mInflater.inflate(R.layout.list_group_view, null); 
} 
TextView groupName = (TextView) convertView 
.findViewById(R.id.group_name); 
groupName.setText(groups[groupPosition]); 
ImageView indicator = (ImageView) convertView 
.findViewById(R.id.group_indicator); 
TextView onlineNum = (TextView) convertView 
.findViewById(R.id.online_count); 
onlineNum.setText(getChildrenCount(groupPosition) + "/" 
+ getChildrenCount(groupPosition)); 
if (isExpanded) { 
indicator.setImageResource(R.drawable.indicator_expanded); 
} else { 
indicator.setImageResource(R.drawable.indicator_unexpanded); 
} 
return convertView; 
} 
@Override 
public int getTreeHeaderState(int groupPosition, int childPosition) { 
final int childCount = getChildrenCount(groupPosition); 
if (childPosition == childCount - 1) { 
return PINNED_HEADER_PUSHED_UP; 
} else if (childPosition == -1 
&& !iphoneTreeView.isGroupExpanded(groupPosition)) { 
return PINNED_HEADER_GONE; 
} else { 
return PINNED_HEADER_VISIBLE; 
} 
} 
@Override 
public void configureTreeHeader(View header, int groupPosition, 
int childPosition, int alpha) { 
// TODO Auto-generated method stub 
((TextView) header.findViewById(R.id.group_name)) 
.setText(groups[groupPosition]); 
((TextView) header.findViewById(R.id.online_count)) 
.setText(getChildrenCount(groupPosition) + "/" 
+ getChildrenCount(groupPosition)); 
} 
@Override 
public void onHeadViewClick(int groupPosition, int status) { 
// TODO Auto-generated method stub 
groupStatusMap.put(groupPosition, status); 
} 
@Override 
public int getHeadViewClickStatus(int groupPosition) { 
if (groupStatusMap.containsKey(groupPosition)) { 
return groupStatusMap.get(groupPosition); 
} else { 
return 0; 
} 
} 
} 
} 
 好了,简单的一个例子就完成了,
总结一下: 原理: 在正在显示的最上面的组的标签位置添加一个和组视图完全一样的视图,作为组标签。这个标签的位置要随着列表的滑动不断变化,以保持总是显示在最上方,并且该消失的时候就消失。给这个标签添加点击事件,实现打开和关闭分组的功能。
组标签总是显示在上方,这是通过不断的调整其在布局中的位置来实现的。这个调整的过程,在初始化的时候,在 onLayout 方法中实现一次,后面都是在滚动过程中,根据对滚动状态的监听来实现的。
实例化要添加的标签的时候(在外面实现,即使调用 setTreeHeaderView之前),parent 要设为该ExpandableListView.
要学习以及好好理解这个,最好的方法是将添加进来的组标签设为半透明,便于观察整个过程。 
源码下载