市面上使用listview头部在顶部固定和字母导航的app是非常之多,今天自己也结合之前做的项目,整理了一个在listview中实现头部固定和字母快速定位导航的功能。
效果图如下:
上面效果图如果不对listview进一步封装,就android原生listview提供的知识是难以实现的,在这里对listview进一步封装,让listview功能变得更加强大,代码如下:
public class PinnedHeaderListView extends ListView {
/**
* 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. 渐变色的值在0到255之间
*/
void configurePinnedHeader(View header, int position, int alpha);
}
private static final int MAX_ALPHA = 255;
private PinnedHeaderAdapter mAdapter;
private View mHeaderView;
private boolean mHeaderViewVisible;
private int mHeaderViewWidth;
private int mHeaderViewHeight;
public PinnedHeaderListView(Context context) {
super(context);
}
public PinnedHeaderListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void hindHeaderView(){
if(mHeaderView != null){
mHeaderView.setVisibility(View.GONE);
}
}
public void showHeaderView(){
if(mHeaderView != null){
mHeaderView.setVisibility(View.VISIBLE);
}
}
/**
* 设置头部固定的view
* @param view 头部显示固定view
*/
public void setPinnedHeaderView(View view) {
mHeaderView = view;
if (mHeaderView != null) {
setFadingEdgeLength(0);
}
requestLayout();
}
/**
* Sets the data behind this ListView.
*
* The adapter passed to this method may be wrapped by a {@link WrapperListAdapter},
* depending on the ListView features currently in use. For instance, adding
* headers and/or footers will cause the adapter to be wrapped.
* 这背后的数据视图。适配器传递给这个方法可以包装一个{ @link WrapperListAdapter },
* 取决于ListView目前使用的特性。例如,添加页眉和/或页脚会导致适配器包装。
*
* @param adapter The ListAdapter which is responsible for maintaining the
* data backing this list and for producing a view to represent an
* item in that data set.
* ListAdapter负责维护的数据支持这个列表,生成一个视图来表示一个数据集的项
*
* @see #getAdapter()
*/
@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
mAdapter = (PinnedHeaderAdapter)adapter;
}
/**
* 测量view的大小
* @param widthMeasureSpec
* @param heightMeasureSpec
*/
@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();
}
}
/**
* 设置子view的位置
* @param changed
* @param left
* @param top
* @param right
* @param bottom
*/
@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());
}
}
public void configureHeaderView(int position) {
if (mHeaderView == null) {
return;
}
if (mAdapter != null) {
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);
if (null == firstView) {
return;
}
int bottom = firstView.getBottom();
// int itemHeight = firstView.getHeight();
int headerHeight = mHeaderView.getHeight();
int y;
int alpha;
headerHeight -= 1;//解决推动时候,中间有缝隙问题
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;
}
}
}
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mHeaderViewVisible) {
drawChild(canvas, mHeaderView, getDrawingTime());
}
}
}
对于做个android自定义控件理解上面逻辑是没什么问题的,如果对自定义控件不熟悉的童鞋可以上网一查,网上有大把供你学习。上面代码有注释了,这里就不在多说了
右侧字母导航实现
public class AlphabetListView extends FrameLayout {
private Context mContext;
private ListView mListView;
private LinearLayout alphabetLayout = null;
private TextView mTextView; //用于存放 点击右侧字母导航提示内容
private List<String> indexArray;//索引值存放数组
private int length = 0;//索引值长度
private AlphabetPositionListener positionListener;
private float screenDensity;
private Handler mHandler;
private HideIndicator mHideIndicator = new HideIndicator();
private int indicatorDuration = 1000;
private int textSize = 10;
public void setIndicatorDuration(int duration) {
this.indicatorDuration = duration;
}
private final class HideIndicator implements Runnable {
@Override
public void run() {
mTextView.setVisibility(View.INVISIBLE);
}
}
public AlphabetListView(Context context) {
super(context);
init(context);
}
public AlphabetListView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
/**
* 相关初始化工作
* @param context
*/
private void init(Context context) {
mContext = context;
screenDensity = context.getResources().getDisplayMetrics().density;
mHandler = new Handler();
/***************设置TextView相关属性***************************/
mTextView = new TextView(mContext);
mTextView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 70);
mTextView.setTextColor(Color.parseColor("#ffffff"));
mTextView.setBackgroundResource(R.drawable.alpha_center_corner);
mTextView.setGravity(Gravity.CENTER);
mTextView.setVisibility(View.INVISIBLE);
int width_height = convertDIP2PX(100);
FrameLayout.LayoutParams textLayoutParams = new FrameLayout.LayoutParams(width_height, width_height);
textLayoutParams.gravity = Gravity.CENTER;
mTextView.setLayoutParams(textLayoutParams);
}
public void setTextSize(int textSize) {
this.textSize = textSize;
}
public void setAdapter(ListView expandListView, BaseAdapter adapter, AlphabetPositionListener positionListener, List<String> list) {
if (positionListener == null) {
throw new IllegalArgumentException("AlphabetPositionListener is required");
}
this.removeAllViews();
mListView = expandListView;
expandListView.setAdapter(adapter);
this.positionListener = positionListener;
this.indexArray = list;
if (indexArray != null && indexArray.size() > 0) {
length = list.size();
initAlphabetLayout(mContext);
}
this.addView(mListView);
if (alphabetLayout != null) {
this.addView(alphabetLayout);
this.addView(mTextView);
} else {
}
}
/**
* 添加这个方法是为了当字母列表的容器内容变化时直接传递进变化后的容器
*
* @param dataList
* @return
*/
public AlphabetListView setDataList(ArrayList<String> dataList) {
this.indexArray = dataList;
return this;
}
public void setAdapter(ExpandableListView expandListView, BaseExpandableListAdapter adapter, AlphabetPositionListener positionListener, ArrayList<String> index) {
if (positionListener == null) {
throw new IllegalArgumentException("AlphabetPositionListener is required");
}
this.removeAllViews();
mListView = expandListView;
expandListView.setAdapter(adapter);
this.positionListener = positionListener;
this.indexArray = index;
if (indexArray != null && indexArray.size() > 0) {
length = index.size();
initAlphabetLayout(mContext);
}
this.addView(mListView);
if (alphabetLayout != null) {
this.addView(alphabetLayout);
this.addView(mTextView);
} else {
}
}
//初始化字母索引布局
private void initAlphabetLayout(Context context) {
alphabetLayout = new LinearLayout(context);
alphabetLayout.setOrientation(LinearLayout.VERTICAL);
FrameLayout.LayoutParams alphabetLayoutParams = new FrameLayout.LayoutParams(convertDIP2PX(25), ViewGroup.LayoutParams.MATCH_PARENT);
alphabetLayoutParams.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL;
alphabetLayoutParams.rightMargin = convertDIP2PX(3);
alphabetLayoutParams.topMargin = convertDIP2PX(topMargin);
alphabetLayoutParams.bottomMargin = convertDIP2PX(20);
mTextView.setBackgroundResource(R.drawable.alpha_center_corner);
alphabetLayout.setPadding(convertDIP2PX(6), 0, convertDIP2PX(6), 0);
alphabetLayout.setLayoutParams(alphabetLayoutParams);
alphabetLayout.setBackgroundColor(Color.parseColor("#00E0E0E0"));
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(android.view.ViewGroup.LayoutParams.WRAP_CONTENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
params.weight = 1;
params.gravity = Gravity.CENTER_HORIZONTAL;
for (int i = 0, count = indexArray.size(); i < count; i++) {
/*******************将字母导航的内容添加到右侧*******************************/
TextView textView = new TextView(context);
if (indexArray.get(i).equals("荐")) {
textView.setTextColor(Color.parseColor("#CC0000"));
} else {
textView.setTextColor(Color.argb(140, 105, 115, 125));
}
textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, textSize);
textView.getPaint().setFakeBoldText(true);
textView.setText(indexArray.get(i));
textView.setGravity(Gravity.CENTER);
textView.setLayoutParams(params);
textView.setTag(i + 1);
TextPaint tp = textView.getPaint();
tp.setFakeBoldText(true);
alphabetLayout.addView(textView);
}
//字母索引添加touch事件
alphabetLayout.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//设置字母索引背景
//alphabetLayout.setBackgroundColor(Color.parseColor("#FF0000"));
alphabetLayout.setBackgroundResource(R.drawable.alaph_list_corner);
float len = (float) (length == 0 ? 0.1 : (float) length);
int l = (int) (event.getY() / (alphabetLayout.getHeight() / len));
if (l >= length){
l = length - 1;
}
else if (l < 0) {
l = 0;
}
//得到对应的字母在分组标签对应的列表的位置
int pos = positionListener.getPosition(indexArray.get(l));
if (pos != -1) {
mTextView.setText(indexArray.get(l));
mTextView.setVisibility(View.VISIBLE);
mHandler.removeCallbacks(mHideIndicator);
mHandler.postDelayed(mHideIndicator, indicatorDuration);
if (mListView instanceof ExpandableListView) {
((ExpandableListView) mListView).setSelectedGroup(pos);
} else {
mListView.setSelection(pos);
}
}
break;
case MotionEvent.ACTION_MOVE:
l = (int) ((event.getY() + alphabetLayout.getHeight() / length / 2) / (alphabetLayout.getHeight() / length)) - 1;
if (l >= length) l = length - 1;
else if (l < 0) l = 0;
pos = positionListener.getPosition(indexArray.get(l));
if (pos != -1) {
mTextView.setText(indexArray.get(l));
mTextView.setVisibility(View.VISIBLE);
mHandler.removeCallbacks(mHideIndicator);
mHandler.postDelayed(mHideIndicator, indicatorDuration);
if (mListView instanceof ExpandableListView) {
((ExpandableListView) mListView).setSelectedGroup(pos);
} else {
mListView.setSelection(pos);
}
}
break;
case MotionEvent.ACTION_UP:
alphabetLayout.setBackgroundColor(Color.parseColor("#00E0E0E0"));
break;
}
return true;
}
});
}
private int topMargin = 10;
public void setTopMargin(int dp) {
topMargin = dp;
}
/**
* dp转换成px
* @param dip
* @return
*/
public int convertDIP2PX(float dip) {
return (int) (dip * screenDensity + 0.5f * (dip >= 0 ? 1 : -1));
}
/**
* 定义一个抽象类供被点中的字母回调
*/
public interface AlphabetPositionListener {
//返回对应的内容(字母)在分组标签对应的列表的位置
int getPosition(String letter);
}
}
这也是一个核心类,该类主要实现了右侧字母的绘制和点击右侧字母导航中相关位置,给用户提示当前点击内容是什么,给用户很好的体验效果。
其它的不粘出来了,提供一下源码