IndexableListView常用于联系人APP及音乐播放器类APP,能快速定位到想到达的位置。
这个版本是在woozzu的Github项目基础上,增加网友翻译的中文注释以及支持中英文混合排序,来不及对性能进行优化,有方向的网友可以在评论进行讨论。
主要源码如下:
IndexableListViewActivity.java
package com.itant.indexablelistview;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.ArrayAdapter;
import android.widget.SectionIndexer;
import com.itant.indexablelistview.utils.Hanzi2Pinyin;
import com.itant.indexablelistview.view.IndexableListView;
public class IndexableListViewActivity extends Activity {
private ArrayList<String> mItems;
private IndexableListView mListView;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mItems = new ArrayList<String>();
mItems.add("Diary of a Wimpy Kid 6: Cabin Fever");
mItems.add("Steve Jobs");
mItems.add("Inheritance (The Inheritance Cycle)");
mItems.add("哈哈: A Novel");
mItems.add("大 Hunger Games");
mItems.add("Catching Fire (The Second Book of the Hunger Games)");
mItems.add("Death Comes to Pemberley");
mItems.add("Diary of a Wimpy Kid 6: Cabin Fever");
mItems.add("Steve Jobs");
mItems.add("11/22/63: A Novel");
mItems.add("The Hunger Games");
mItems.add("The LEGO Ideas Book");
mItems.add("Explosive Eighteen: A Stephanie Plum Novel");
mItems.add("Elder Scrolls V: Skyrim: Prima Official Game Guide");
Collections.sort(mItems, new Comparator<String>() {
@Override
public int compare(String lhs, String rhs) {
Hanzi2Pinyin hanzi2Pinyin = new Hanzi2Pinyin();
// TODO Auto-generated method stub
return hanzi2Pinyin.getStringPinYin(lhs.replaceAll(" ", "")).compareToIgnoreCase(hanzi2Pinyin.getStringPinYin(rhs.replaceAll(" ", "")));
}
});
ContentAdapter adapter = new ContentAdapter(this,
android.R.layout.simple_list_item_1, mItems);
mListView = (IndexableListView) findViewById(R.id.listview);
mListView.setAdapter(adapter);
mListView.setFastScrollEnabled(true);
}
private class ContentAdapter extends ArrayAdapter<String> implements SectionIndexer {
private String mSections = "#ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public ContentAdapter(Context context, int textViewResourceId,
List<String> objects) {
super(context, textViewResourceId, objects);
}
@Override
public int getPositionForSection(int section) {
// section:所点击字母在右侧数组中的位置,从0开始。
// getCount():ListView的总条目数
// 查找结束后的i:真正聚焦的字母在右侧数组中的位置(如果ListView中不存在当前点击的字母,则往前面聚焦。)
// 查找结束后的j:所点击的字母在ListView的位置(如果ListView中不存在当前点击的字母,则往前面聚焦。)
for (int i = section; i >= 0; i--) {
for (int j = 0; j < getCount(); j++) {
if (i == 0) {
// 点击的是数字(或者点击的字母在LIstView项的首字母中没找到)
for (int k = 0; k <= 9; k++) {
//if (StringMatcher.match(String.valueOf(getItem(j).charAt(0)), String.valueOf(k)))
if (TextUtils.equals(String.valueOf(getItem(j).charAt(0)), String.valueOf(k)))
return j;
}
} else {
// 用ListView每一项的首字母与点击的右侧字母进行对比
if (TextUtils.equals(String.valueOf(getItem(j).charAt(0)), String.valueOf(mSections.charAt(i))))
return j;
}
}
}
return 0;
}
@Override
public int getSectionForPosition(int position) {
return 0;
}
@Override
public Object[] getSections() {
String[] sections = new String[mSections.length()];
for (int i = 0; i < mSections.length(); i++)
sections[i] = String.valueOf(mSections.charAt(i));
return sections;
}
}
}
用于将汉字转换为拼音的类Hanzi2Pinyin.java
package com.itant.indexablelistview.utils;
import net.sourceforge.pinyin4j.PinyinHelper;
import net.sourceforge.pinyin4j.format.HanyuPinyinOutputFormat;
import net.sourceforge.pinyin4j.format.HanyuPinyinToneType;
import net.sourceforge.pinyin4j.format.exception.BadHanyuPinyinOutputFormatCombination;
public class Hanzi2Pinyin {
private HanyuPinyinOutputFormat format = null;
private String[] pinyin;
public Hanzi2Pinyin() {
format = new HanyuPinyinOutputFormat();
format.setToneType(HanyuPinyinToneType.WITHOUT_TONE);
pinyin = null;
}
// 转换单个字符
public String getCharacterPinYin(char c) {
try {
pinyin = PinyinHelper.toHanyuPinyinStringArray(c, format);
}
catch (BadHanyuPinyinOutputFormatCombination e) {
e.printStackTrace();
}
// 如果c不是汉字,toHanyuPinyinStringArray会返回null
if (pinyin == null) {
if (c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z') {
return String.valueOf(c);
}
return null;
}
// 只取一个发音,如果是多音字,仅取第一个发音
return pinyin[0];
}
// 转换一个字符串
public String getStringPinYin(String str) {
StringBuilder sb = new StringBuilder();
String tempPinyin = null;
for (int i = 0; i < str.length(); ++i) {
tempPinyin = getCharacterPinYin(str.charAt(i));
if (tempPinyin == null) {
// 如果str.charAt(i)非汉字,且不是数字,则置空
char character = str.charAt(i);
if (character >= '0' && character <= '9') {
sb.append(character);
}
} else {
sb.append(tempPinyin);
}
}
return sb.toString();
}
}
自定义控件IndexableListView.java
/*
* Copyright 2011 woozzu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itant.indexablelistview.view;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.widget.ListAdapter;
import android.widget.ListView;
public class IndexableListView extends ListView {
private boolean mIsFastScrollEnabled = false;
private IndexScroller mScroller = null;
private GestureDetector mGestureDetector = null;
public IndexableListView(Context context) {
super(context);
}
public IndexableListView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public IndexableListView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override
public boolean isFastScrollEnabled() {
return mIsFastScrollEnabled;
}
@Override
public void setFastScrollEnabled(boolean enabled) {
mIsFastScrollEnabled = enabled;
if (mIsFastScrollEnabled) {
if (mScroller == null)
mScroller = new IndexScroller(getContext(), this);
} else {
if (mScroller != null) {
mScroller.hide();
mScroller = null;
}
}
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
// Overlay index bar
if (mScroller != null)
mScroller.draw(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
// 创建一个GestureDetector(手势探测器)
if (mScroller != null && mScroller.onTouchEvent(ev))
return true;
if (mGestureDetector == null) {
mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2,
float velocityX, float velocityY) {
// 快速掠过屏幕,则显示索引条
if (mScroller != null)
mScroller.show();
return super.onFling(e1, e2, velocityX, velocityY);
}
});
}
mGestureDetector.onTouchEvent(ev);
return super.onTouchEvent(ev);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(mScroller.contains(ev.getX(), ev.getY()))
return true;
return super.onInterceptTouchEvent(ev);
}
@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
if (mScroller != null)
mScroller.setAdapter(adapter);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mScroller != null)
mScroller.onSizeChanged(w, h, oldw, oldh);
}
}
右侧索引条IndexScroller.java
/*
* Copyright 2011 woozzu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.itant.indexablelistview.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.view.MotionEvent;
import android.widget.Adapter;
import android.widget.ListView;
import android.widget.SectionIndexer;
public class IndexScroller {
private float mIndexbarWidth;// 索引条宽度
private float mIndexbarMargin;// 索引条外边距
private float mPreviewPadding;
private float mDensity;// 密度
private float mScaledDensity;// 缩放密度
private float mAlphaRate;// 透明度
private int mState = STATE_HIDDEN;// 状态
private int mListViewWidth;// ListView宽度
private int mListViewHeight;// ListView高度
private int mCurrentSection = -1;// 当前部分
private boolean mIsIndexing = false;// 是否正在索引
private ListView mListView = null;
private SectionIndexer mIndexer = null;
private String[] mSections = null;
private RectF mIndexbarRect;
// 4种状态(已隐藏、正在显示、已显示、正在隐藏)
private static final int STATE_HIDDEN = 0;
private static final int STATE_SHOWING = 1;
private static final int STATE_SHOWN = 2;
private static final int STATE_HIDING = 3;
public IndexScroller(Context context, ListView lv) {
mDensity = context.getResources().getDisplayMetrics().density;
mScaledDensity = context.getResources().getDisplayMetrics().scaledDensity;
mListView = lv;
setAdapter(mListView.getAdapter());
mIndexbarWidth = 20 * mDensity;// 索引条宽度 20dp,转换为px
mIndexbarMargin = 10 * mDensity;// 索引条间距 10dp,转换为px
mPreviewPadding = 5 * mDensity;// 内边距 5dp,转换为px
}
public void draw(Canvas canvas) {
if (mState == STATE_HIDDEN)
return;
// mAlphaRate determines the rate of opacity
Paint indexbarPaint = new Paint();
indexbarPaint.setColor(Color.BLACK);
indexbarPaint.setAlpha((int) (64 * mAlphaRate));
indexbarPaint.setAntiAlias(true);
// 画右侧字母索引的圆矩形
canvas.drawRoundRect(mIndexbarRect, 5 * mDensity, 5 * mDensity, indexbarPaint);
if (mSections != null && mSections.length > 0) {
// Preview is shown when mCurrentSection is set
if (mCurrentSection >= 0) {
Paint previewPaint = new Paint();// 用来绘画所以条背景的画笔
previewPaint.setColor(Color.BLACK);// 设置画笔颜色为黑色
previewPaint.setAlpha(96);// 设置透明度
previewPaint.setAntiAlias(true);// 设置抗锯齿
previewPaint.setShadowLayer(3, 0, 0, Color.argb(64, 0, 0, 0));// 设置阴影层
// 屏幕中间的大字母
Paint previewTextPaint = new Paint();// 用来绘画索引字母的画笔
previewTextPaint.setColor(Color.WHITE);// 设置画笔为白色
previewTextPaint.setAntiAlias(true);// 设置抗锯齿
previewTextPaint.setTextSize(50 * mScaledDensity);// 设置字体大小
// 文本的宽度
float previewTextWidth = previewTextPaint.measureText(mSections[mCurrentSection]);
float previewSize = 2 * mPreviewPadding + previewTextPaint.descent() - previewTextPaint.ascent();
RectF previewRect = new RectF((mListViewWidth - previewSize) / 2
, (mListViewHeight - previewSize) / 2
, (mListViewWidth - previewSize) / 2 + previewSize
, (mListViewHeight - previewSize) / 2 + previewSize);
// 中间索引的那个框,圆角5dp
canvas.drawRoundRect(previewRect, 5 * mDensity, 5 * mDensity, previewPaint);
// 屏幕中间的大字母
canvas.drawText(mSections[mCurrentSection], previewRect.left + (previewSize - previewTextWidth) / 2 - 1
, previewRect.top + mPreviewPadding - previewTextPaint.ascent() + 1, previewTextPaint);
}
// 绘画右侧索引条的字母
Paint indexPaint = new Paint();
indexPaint.setColor(Color.WHITE);
indexPaint.setAlpha((int) (255 * mAlphaRate));
indexPaint.setAntiAlias(true);
indexPaint.setTextSize(12 * mScaledDensity);
float sectionHeight = (mIndexbarRect.height() - 2 * mIndexbarMargin) / mSections.length;
float paddingTop = (sectionHeight - (indexPaint.descent() - indexPaint.ascent())) / 2;
for (int i = 0; i < mSections.length; i++) {
float paddingLeft = (mIndexbarWidth - indexPaint.measureText(mSections[i])) / 2;
canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft
, mIndexbarRect.top + mIndexbarMargin + sectionHeight * i + paddingTop - indexPaint.ascent(), indexPaint);
// - indexPaint.ascent()则到达基线
}
}
}
public boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:// 按下,开始索引
// If down event occurs inside index bar region, start indexing
if (mState != STATE_HIDDEN && contains(ev.getX(), ev.getY())) {
setState(STATE_SHOWN);
// It demonstrates that the motion event started from index bar
mIsIndexing = true;
// Determine which section the point is in, and move the list to that section
mCurrentSection = getSectionByPoint(ev.getY());
mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection));
return true;
}
break;
case MotionEvent.ACTION_MOVE:// 移动
if (mIsIndexing) {
// If this event moves inside index bar
if (contains(ev.getX(), ev.getY())) {
// Determine which section the point is in, and move the list to that section
mCurrentSection = getSectionByPoint(ev.getY());
mListView.setSelection(mIndexer.getPositionForSection(mCurrentSection));
}
return true;
}
break;
case MotionEvent.ACTION_UP:// 抬起
if (mIsIndexing) {
mIsIndexing = false;
mCurrentSection = -1;
}
if (mState == STATE_SHOWN)
setState(STATE_HIDING);
break;
}
return false;
}
public void onSizeChanged(int w, int h, int oldw, int oldh) {
mListViewWidth = w;
mListViewHeight = h;
mIndexbarRect = new RectF(w - mIndexbarMargin - mIndexbarWidth
, mIndexbarMargin
, w - mIndexbarMargin
, h - mIndexbarMargin);
}
/**
* 显示
*/
public void show() {
if (mState == STATE_HIDDEN)
setState(STATE_SHOWING);
else if (mState == STATE_HIDING)
setState(STATE_HIDING);
}
/**
* 隐藏
*/
public void hide() {
if (mState == STATE_SHOWN)
setState(STATE_HIDING);
}
/**
* 设置状态
* @param adapter
*/
public void setAdapter(Adapter adapter) {
if (adapter instanceof SectionIndexer) {
mIndexer = (SectionIndexer) adapter;
mSections = (String[]) mIndexer.getSections();
}
}
private void setState(int state) {
if (state < STATE_HIDDEN || state > STATE_HIDING)
return;
mState = state;
switch (mState) {
case STATE_HIDDEN:
// Cancel any fade effect
// 取消渐退的效果
mHandler.removeMessages(0);
break;
case STATE_SHOWING:
// Start to fade in
// 开始渐进效果
mAlphaRate = 0;
fade(0);
break;
case STATE_SHOWN:
// Cancel any fade effect
// 取消渐退的效果
mHandler.removeMessages(0);
break;
case STATE_HIDING:
// Start to fade out after three seconds
// 隐藏3秒钟
mAlphaRate = 1;
fade(3000);
break;
}
}
public boolean contains(float x, float y) {
// Determine if the point is in index bar region, which includes the right margin of the bar
return (x >= mIndexbarRect.left && y >= mIndexbarRect.top && y <= mIndexbarRect.top + mIndexbarRect.height());
}
private int getSectionByPoint(float y) {
if (mSections == null || mSections.length == 0)
return 0;
if (y < mIndexbarRect.top + mIndexbarMargin)
return 0;
if (y >= mIndexbarRect.top + mIndexbarRect.height() - mIndexbarMargin)
return mSections.length - 1;
return (int) ((y - mIndexbarRect.top - mIndexbarMargin) / ((mIndexbarRect.height() - 2 * mIndexbarMargin) / mSections.length));
}
private void fade(long delay) {
mHandler.removeMessages(0);
mHandler.sendEmptyMessageAtTime(0, SystemClock.uptimeMillis() + delay);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (mState) {
case STATE_SHOWING:
// Fade in effect
// 淡进效果
mAlphaRate += (1 - mAlphaRate) * 0.2;
if (mAlphaRate > 0.9) {
mAlphaRate = 1;
setState(STATE_SHOWN);
}
mListView.invalidate();
fade(10);
break;
case STATE_SHOWN:
// If no action, hide automatically
setState(STATE_HIDING);
break;
case STATE_HIDING:
// Fade out effect
// 淡出效果
mAlphaRate -= mAlphaRate * 0.2;
if (mAlphaRate < 0.1) {
mAlphaRate = 0;
setState(STATE_HIDDEN);
}
mListView.invalidate();
fade(10);
break;
}
}
};
}
main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<com.itant.indexablelistview.view.IndexableListView
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/listview" />
</LinearLayout>