IndexableListView详解之——IndexScroller

本文详细解析了IndexableListView项目中的核心组件IndexScroller,这是一个自定义的索引条类,用于辅助用户快速定位ListView中的数据项。文章通过源码解读了IndexScroller的工作原理、绘制流程及触摸事件处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这是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详解系列在此告一段落啦。这里分享一下我研究这个项目的一些心得,首先是运行项目清楚大概功能,其次是在方法中输出相应信息清楚每个方法的调用时机等,然后就根据方法之间的调用逻辑一步步看代码啦,总之就是从大到小,先理解一整块大的代码是干什么的,再细的就看个人了,有的人只需要了解整段代码的功能,有的人想要深入理解。每个人看代码的方式不一样,希望我的分享对大家有帮助,谢谢。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值