【原创】实现类似手机QQ的可折叠固定标题列表
在上篇文章中,简单的模拟了手机QQ列表的列表功能,实现了一级标题的固定功能,但是一级标题靠近时往上推到细节并没有实现。偶然发现android系统自带到联系人列表就是可以固定标题头到,并且实现了往上推动的效果,于是参考android源代码(PinnedHeaderListView)的实现,让ExpandableListView也有类似功能。
效果如下:

实现到原理类似于之前到文章:
/**
* @author douzifly
*
* @Date 2011-9-13
*/
/**
* 可固定标题的ExpandableListView
*
* @author douzifly
* @date 2011-9-13
*/
public class PinnedExpandableListView extends ExpandableListView implements OnScrollListener {
/**
* 该ListView的Adapter必须实现该接口
*
* @author LiuXiaoyuan@hh.com.cn
* @date 2011-9-13
*/
public interface PinnedExpandableListViewAdapter {
/**
* 固定标题状态:不可见
*/
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;
public int getPinnedHeaderState(int groupPosition, int childPosition);
public void configurePinnedHeader(View header, int groupPosition, int childPosition, int alpha);
}
private static final int MAX_ALPHA = 255;
private PinnedExpandableListViewAdapter mAdapter;
private View mHeaderView;
private boolean mHeaderVisible;
private int mHeaderViewWidth;
private int mHeaderViewHeight;
private OnClickListener mPinnedHeaderClickLisenter;
public void setOnPinnedHeaderClickLisenter(OnClickListener listener) {
mPinnedHeaderClickLisenter = listener;
}
public PinnedExpandableListView(Context context, AttributeSet attrs) {
super(context, attrs);
setOnScrollListener(this);
}
public void setPinnedHeaderView(View view) {
mHeaderView = view;
if (mHeaderView != null) {
setFadingEdgeLength(0);
}
requestLayout();
}
@Override
public void setAdapter(ExpandableListAdapter adapter) {
super.setAdapter(adapter);
mAdapter = (PinnedExpandableListViewAdapter) adapter;
}
@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 l, int t, int r, int b) {
super.onLayout(changed, l, t, r, b);
final long flatPostion = getExpandableListPosition(getFirstVisiblePosition());
final int groupPos = ExpandableListView.getPackedPositionGroup(flatPostion);
final int childPos = ExpandableListView.getPackedPositionChild(flatPostion);
int state = mAdapter.getPinnedHeaderState(groupPos, childPos);
//只有在状态改变时才layout,这点相当重要,不然可能导致视图不断的刷新
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) {
return;
}
final int state = mAdapter.getPinnedHeaderState(groupPosition, childPosition);
switch (state) {
case PinnedExpandableListViewAdapter.PINNED_HEADER_GONE: {
mHeaderVisible = false;
break;
}
case PinnedExpandableListViewAdapter.PINNED_HEADER_VISIBLE: {
mAdapter.configurePinnedHeader(mHeaderView, groupPosition, childPosition, MAX_ALPHA);
if (mHeaderView.getTop() != 0) {
mHeaderView.layout(0, 0, mHeaderViewWidth, mHeaderViewHeight);
}
mHeaderVisible = true;
break;
}
case PinnedExpandableListViewAdapter.PINNED_HEADER_PUSHED_UP: {
final View firstView = getChildAt(0);
if (firstView == null) {
break;
}
int bottom = firstView.getBottom();
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, groupPosition, childPosition, alpha);
if (mHeaderView.getTop() != y) {
mHeaderView.layout(0, y, mHeaderViewWidth, mHeaderViewHeight + y);
}
mHeaderVisible = true;
break;
}
default:
break;
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
//由于HeaderView并没有添加到ExpandableListView的子控件中,所以要draw他
if (mHeaderVisible) {
drawChild(canvas, mHeaderView, getDrawingTime());
}
}
private float mDownX;
private float mDownY;
private static final float FINGER_WIDTH = 20;
@Override
public boolean onTouchEvent(MotionEvent ev) {
if (mHeaderVisible) {
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);
// 如果在固定标题内点击了,那么触发事件
if (x <= mHeaderViewWidth && y <= mHeaderViewHeight && offsetX <= FINGER_WIDTH && offsetY <= FINGER_WIDTH) {
if (mPinnedHeaderClickLisenter != null) {
mPinnedHeaderClickLisenter.onClick(mHeaderView);
}
return true;
}
break;
default:
break;
}
}
return super.onTouchEvent(ev);
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
}
@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);
Log.d("TEST", "configure in onScroll");
configureHeaderView(groupPosition, childPosition);
}
}
一般情况下,通过继承该类来使用,列表标题当前的状态(可见,不可见,正在推动)由Adapter来决定,所以Adapter里面的代码就相当重要了。以下是我的一个实现:
@Override
public int getPinnedHeaderState(int groupPosition, int childPosition) {
final int childCount = getChildrenCount(groupPosition);
if(childPosition == childCount - 1){
return PINNED_HEADER_PUSHED_UP;
}else if(childPosition == -1 && !BlockListView.this.isGroupExpanded(groupPosition)){
return PINNED_HEADER_GONE;
}else {
return PINNED_HEADER_VISIBLE;
}
}
@Override
public void configurePinnedHeader(View header, int groupPosition, int childPosition, int alpha) {
TextView pinned = (TextView) header;
pinned.setText((String)getGroup(groupPosition));
}
当然需要在子类初始化的地方加上对HeaderView的配置
txtPinned = new TextView(context);
txtPinned.setTextSize(mGroupTextSize);
txtPinned.setBackgroundResource(mGroupBgDrawableId);
AbsListView.LayoutParams lp = new AbsListView.LayoutParams(-1, -2);
txtPinned.setLayoutParams(lp);
txtPinned.setTextColor(Color.WHITE);
setPinnedHeaderView(txtPinned);
setOnPinnedHeaderClickLisenter(new OnClickListener() {
@Override
public void onClick(View v) {
final long flatPos = getExpandableListPosition(getFirstVisiblePosition());
final int groupPos = ExpandableListView.getPackedPositionGroup(flatPos);
collapseGroup(groupPos);
}
});
这样,上图中的效果就实现了。
需要注意的就是HeaderView并没有加入到ExpandableListView的子控件中,所以要重写dispatchDraw函数,并在里面绘制HeaderView,这样导致一个问题就是直接在HeaderView上面设置的click监听函数无效,所以需要重写onTouchEvent来模拟在HeaderView上的OnClick事件。
转载请注明出处。

浙公网安备 33010602011771号