这是IndexableListView项目详解系列的第四篇博客,也是最后一篇啦,还没看第一篇博客的小伙伴们建议先看哦,链接:IndexableListView详解之——IndexableListViewActivity,简单介绍了整个项目的结构、讲解了IndexableListViewActivity。
这一篇讲的是IndexableListView项目中的重中之重——IndexScroller,这是自定义的索引条类。
IndexScroller
按照惯例,先贴上源码,这个类的的代码会比之前的多,但是只要耐心看下来还是很好看懂的。
/**
* 索引条类
*/
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;
private int mListViewHeight;
private int mCurrentSection = -1; // 当前索引
private boolean mIsIndexing = false; // 是否正在索引
private ListView mListView = null;
private SectionIndexer mIndexer = null; // 列表的适配器
private String[] mSections = null; // 存放索引字符的数组
private RectF mIndexbarRect; // 索引条的背景图形
// 索引条的状态
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;
mIndexbarMargin = 10 * mDensity;
mPreviewPadding = 5 * mDensity;
}
public void draw(Canvas canvas) {
System.out.println("draw");
if (mState == STATE_HIDDEN)
return;
Paint indexbarPaint = new Paint();
indexbarPaint.setColor(Color.BLACK);
indexbarPaint.setAlpha((int) (64 * mAlphaRate));
// 设置抗锯齿,对图像边缘进行柔化处理
indexbarPaint.setAntiAlias(true);
// 参数解释:1.RectF对象 2.x方向上的圆角半径 3.y方向上的圆角半径 4.绘制时使用的画笔
canvas.drawRoundRect(mIndexbarRect, 5 * mDensity, 5 * mDensity,
indexbarPaint);
if (mSections != null && mSections.length > 0) {
// 如果选择了索引,则显示预览图
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);
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;
// 索引条中每个字符的上下边距,indexPaint.descent() - indexPaint.ascent()得到的是字符的高度
float paddingTop = (sectionHeight - (indexPaint.descent() - indexPaint
.ascent())) / 2;
for (int i = 0; i < mSections.length; i++) {
// 索引条中各个字符的左右边距,其中indexPaint.measureText(mSections[i])为字符的宽度
float paddingLeft = (mIndexbarWidth - indexPaint
.measureText(mSections[i])) / 2;
/*
* 参数解释: 1.要绘制的文本 2.该文本的左边在屏幕的位置 3.该文本的baseline在屏幕的位置 4.绘制时使用的画笔
*/
canvas.drawText(mSections[i], mIndexbarRect.left + paddingLeft,
mIndexbarRect.top + mIndexbarMargin + sectionHeight * i
+ paddingTop - indexPaint.ascent(), indexPaint);
}
/*
* 需了解Paint.descent(),Paint.ascent()和baseline
*/
/*
* 说明: paddingTop在for外获取,因为每个字符高度相同,paddingTop也相同
* paddingLeft在for内获取,因为每个字符宽度不同,paddingLeft也不同
*/
}
}
/**
* 索引条的触摸事件
*
* @param ev
* @return 若索引条可见,且触摸事件发生在索引条范围内,则返回true,否则返回false
*/
public boolean onTouchEvent(MotionEvent ev) {
System.out.println("onTouchEvent");
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 索引条可见,且按下事件发生在索引条范围内
if (mState != STATE_HIDDEN && contains(ev.getX(), ev.getY())) {
setState(STATE_SHOWN);
// 设置mIsIndexing为true,表示正在索引
mIsIndexing = true;
// 获取当前点击的索引条中字符的位置
mCurrentSection = getSectionByPoint(ev.getY());
// 列表定位到指定位置
mListView.setSelection(mIndexer
.getPositionForSection(mCurrentSection));
return true;
}
break;
case MotionEvent.ACTION_MOVE:
// 有这个判断,保证按下事件是在索引条中发生,之后不抬起,继续进行该滑动操作
if (mIsIndexing) {
// 滑动事件发生在索引条范围内
if (contains(ev.getX(), ev.getY())) {
// 获取当前手指触摸点对应的索引条中字符的位置
mCurrentSection = getSectionByPoint(ev.getY());
// 列表定位到指定位置
mListView.setSelection(mIndexer
.getPositionForSection(mCurrentSection));
}
return true;
}
break;
case MotionEvent.ACTION_UP:
if (mIsIndexing) {
// 设置mIsIndexing为false,表示正在索引取消
mIsIndexing = false;
// 将当前索引字符位置设置为-1,隐藏预览图
mCurrentSection = -1;
}
if (mState == STATE_SHOWN)
// 渐退索引条
setState(STATE_HIDING);
break;
}
return false;
}
/**
* 定义索引条背景
*/
public void onSizeChanged(int w, int h, int oldw, int oldh) {
System.out.println("onSizeChanged");
mListViewWidth = w;
mListViewHeight = h;
// 参数定义分别是:左上右下
// 定义索引条的左上右下边界
mIndexbarRect = new RectF(w - mIndexbarMargin - mIndexbarWidth,
mIndexbarMargin, w - mIndexbarMargin, h - mIndexbarMargin);
}
/**
* 显示索引条
*/
public void show() {
System.out.println("show");
if (mState == STATE_HIDDEN)
setState(STATE_SHOWING);
else if (mState == STATE_HIDING)
setState(STATE_HIDING);
}
/**
* 隐藏索引条
*/
public void hide() {
System.out.println("hide");
if (mState == STATE_SHOWN)
setState(STATE_HIDING);
}
/**
* 设置mIndexer和mSections
*/
public void setAdapter(Adapter adapter) {
System.out.println("setAdapter");
if (adapter instanceof SectionIndexer) {
mIndexer = (SectionIndexer) adapter;
mSections = (String[]) mIndexer.getSections();
}
}
private void setState(int state) {
System.out.println("setState");
if (state < STATE_HIDDEN || state > STATE_HIDING)
// state不在索引条的四种状态范围内,则不执行以下语句
return;
mState = state;
switch (mState) {
case STATE_HIDDEN:
// 取消渐退效果
mHandler.removeMessages(0);
break;
case STATE_SHOWING:
// 开始渐进显示索引条
mAlphaRate = 0;
fade(0);
break;
case STATE_SHOWN:
// 取消渐进效果
mHandler.removeMessages(0);
break;
case STATE_HIDING:
// 3秒后开始渐退
mAlphaRate = 1;
fade(3000);
break;
}
}
/**
* 判断该点是否在索引条的范围内,包括索引条的右边距
*/
public boolean contains(float x, float y) {
System.out.println("contains");
return (x >= mIndexbarRect.left && y >= mIndexbarRect.top && y <= mIndexbarRect.top
+ mIndexbarRect.height());
}
/**
* 根据手指触摸的位置返回对应索引字符的位置
*/
private int getSectionByPoint(float y) {
System.out.println("getSectionByPoint");
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) {
System.out.println("fade");
mHandler.removeMessages(0);
mHandler.sendEmptyMessageAtTime(0, SystemClock.uptimeMillis() + delay);
}
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
System.out.println("handleMessage");
super.handleMessage(msg);
switch (mState) {
case STATE_SHOWING:
// 逐渐增大mAlphaRate
mAlphaRate += (1 - mAlphaRate) * 0.2;
if (mAlphaRate > 0.9) {
mAlphaRate = 1;
setState(STATE_SHOWN);
}
mListView.invalidate();
fade(10);
break;
case STATE_SHOWN:
// 如果没有动作,则自动隐藏索引条
setState(STATE_HIDING);
break;
case STATE_HIDING:
// 逐渐减小mAlphaRate
mAlphaRate -= mAlphaRate * 0.2;
if (mAlphaRate < 0.1) {
mAlphaRate = 0;
setState(STATE_HIDDEN);
}
mListView.invalidate();
fade(10);
break;
}
}
};
}
讲解
1.draw方法的最后我添加了两段注释,以助于大家更好地理解。这里说一个知识点,就是draw方法中使用到的Paint.ascent()和Paint.descent(),先看一个图:
Paint.ascent():baseLine到字符最高处ascent的距离,为负数。
Paint.descent():baseLine到字符最低处descent的距离,为整数。
所以,Paint.descent()-Paint.ascent()为字符的高度。
结束
IndexableListView详解系列在此告一段落啦。这里分享一下我研究这个项目的一些心得,首先是运行项目清楚大概功能,其次是在方法中输出相应信息清楚每个方法的调用时机等,然后就根据方法之间的调用逻辑一步步看代码啦,总之就是从大到小,先理解一整块大的代码是干什么的,再细的就看个人了,有的人只需要了解整段代码的功能,有的人想要深入理解。每个人看代码的方式不一样,希望我的分享对大家有帮助,谢谢。