android 分组列表固定head,Anroid ListView分组和悬浮Header实现方法

之前在使用iOS时,看到过一种分组的View,每一组都有一个Header,在上下滑动的时候,会有一个悬浮的Header,这种体验觉得很不错,请看下图:

7ea61afe4bb5406f67d778a0cad19bb0.png

上图中标红的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效果:

方法一:

每一个ItemView中包含Header,通过数据来控制其显示或隐藏,实现原理如下图:

aa410a89bf143c98f77b858ea1b21390.png

优点:

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 mDatas;

private static final int TYPE_CATEGORY_ITEM = 0;

private static final int TYPE_ITEM = 1;

public ListViewAdapter(ArrayList 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 mDatas;

private static final int TYPE_CATEGORY_ITEM = 0;

private static final int TYPE_ITEM = 1;

public ListViewAdapter(ArrayList 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);

}

}

}

}

}

最后来一张截图:

1a42272312677262a05ae196659dcac9.png以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值