项目地址:WaveSideBar 本文分析版本:6adc355
1.简介
WaveSideBar是一款快速索引导航栏,实现得比较清晰简单,下面介绍一下使用方法。
2.使用方法
1、在XML中声明
2、在Java代码中设置回调
sideBar = (WaveSideBar) findViewById(R.id.side_bar);
sideBar.setOnSelectIndexItemListener(new WaveSideBar.OnSelectIndexItemListener() {
@Override
public void onSelectIndexItem(String index) {
for (int i=0; i
实现快速索引就是这么简单!
3.源码分析
WaveSideBar 项目只有一个类 WaveSideBar.java。实现比较简单,分析此项目的源码主要是让自己形成看源码的习惯。做到知其然,知其所以然。
首先看一下初始化函数:
public WaveSideBar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mDisplayMetrics = context.getResources().getDisplayMetrics();
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WaveSideBar);
mLazyRespond = typedArray.getBoolean(R.styleable.WaveSideBar_sidebar_lazy_respond, false);
mTextColor = typedArray.getColor(R.styleable.WaveSideBar_sidebar_text_color, Color.GRAY);
mMaxOffset = typedArray.getDimension(R.styleable.WaveSideBar_sidebar_max_offset, dp2px(DEFAULT_MAX_OFFSET));
mSideBarPosition = typedArray.getInt(R.styleable.WaveSideBar_sidebar_position, POSITION_RIGHT);
mTextAlignment = typedArray.getInt(R.styleable.WaveSideBar_sidebar_text_alignment, TEXT_ALIGN_CENTER);
typedArray.recycle();
mTextSize = sp2px(DEFAULT_TEXT_SIZE);
mIndexItems = DEFAULT_INDEX_ITEMS;
initPaint();
}
这部分主要是做了些自定义属性的初始化工作。
接下来看一下 onMeasure
做了些什么工作。
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();
mIndexItemHeight = fontMetrics.bottom - fontMetrics.top;
mBarHeight = mIndexItems.length * mIndexItemHeight;
// calculate the width of the longest text as the width of side bar
for (String indexItem : mIndexItems) {
mBarWidth = Math.max(mBarWidth, mPaint.measureText(indexItem));
}
float areaLeft = (mSideBarPosition == POSITION_LEFT) ? 0 : (width - mBarWidth - getPaddingRight());
float areaRight = (mSideBarPosition == POSITION_LEFT) ? (getPaddingLeft() + areaLeft + mBarWidth) : width;
float areaTop = height / 2 - mBarHeight / 2; float areaBottom = areaTop + mBarHeight;
mStartTouchingArea.set(
areaLeft,
areaTop,
areaRight,
areaBottom);
// the baseline Y of the first item' text to draw
mFirstItemBaseLineY = (height / 2 - mIndexItems.length * mIndexItemHeight / 2)
+ (mIndexItemHeight / 2 - (fontMetrics.descent - fontMetrics.ascent) / 2)
- fontMetrics.ascent;
}
height
、width
取到控件的宽高, mIndexItemHeight
是字体的高度,mBarHeight
计算出总高度,mBarWidth
字符串数组中最大的的值,mStartTouchingArea
保存字符绘制区域的矩阵,mFirstItemBaseLineY
第一个字符绘制的位置。onMeasure
中主要还是做初始化的工作,测量出onDraw
需要的一些值。
接下来分析一下核心部分onDraw
、onTouchEvent
:
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// draw each item
for (int i = 0, mIndexItemsLength = mIndexItems.length; i < mIndexItemsLength; i++) {
float baseLineY = mFirstItemBaseLineY + mIndexItemHeight * i;
// calculate the scale factor of the item to draw
float scale = getItemScale(i);
int alphaScale = (i == mCurrentIndex) ? (255) : (int) (255 * (1 - scale));
mPaint.setAlpha(alphaScale);
mPaint.setTextSize(mTextSize + mTextSize * scale);
float baseLineX = 0f;
switch (mTextAlignment) {
case TEXT_ALIGN_CENTER:
baseLineX = getWidth() - getPaddingRight() - mBarWidth / 2 - mMaxOffset * scale;
break;
case TEXT_ALIGN_RIGHT:
baseLineX = getWidth() - getPaddingRight() - mMaxOffset * scale;
break;
case TEXT_ALIGN_LEFT:
baseLineX = getWidth() - getPaddingRight() - mBarWidth - mMaxOffset * scale;
break;
}
// draw
canvas.drawText(
mIndexItems[i], //item text to draw
baseLineX, //baseLine X
baseLineY, // baseLine Y
mPaint);
}
// reset paint
mPaint.setAlpha(255);
mPaint.setTextSize(mTextSize);
}
onDraw
主要流程都在 for 循环里,每次循环绘制一个字符。在drawText
之前确定 X 坐标和 Y 坐标,设置画笔mPaint
的透明度和字体大小。第一次绘制透明度为0,字体大小是初始值。这两项的值主要是在 WaveSideBar
的移动过程中改变。下面看一下onTouchEvent
的实现:
public boolean onTouchEvent(MotionEvent event) {
if (mIndexItems.length == 0) {
return super.onTouchEvent(event);
}
float eventY = event.getY();
float eventX = event.getX();
mCurrentIndex = getSelectedIndex(eventY);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (mStartTouchingArea.contains(eventX, eventY)) {
mStartTouching = true;
if (!mLazyRespond && onSelectIndexItemListener != null) {
onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]);
}
invalidate();
return true;
} else {
mCurrentIndex = -1;
return false;
}
case MotionEvent.ACTION_MOVE:
if (mStartTouching && !mLazyRespond && onSelectIndexItemListener != null) {
onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]);
}
invalidate();
return true;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
if (mLazyRespond && onSelectIndexItemListener != null) {
onSelectIndexItemListener.onSelectIndexItem(mIndexItems[mCurrentIndex]);
}
mCurrentIndex = -1;
mStartTouching = false;
invalidate();
return true;
}
return super.onTouchEvent(event);
}
如果索引数组值为0直接返回。手指按下后取出X、Y的坐标,根据Y的坐标计算出是第几个字符,getSelectedIndex
函数的实现很简单:
private int getSelectedIndex(float eventY) {
mCurrentY = eventY - (getHeight() / 2 - mBarHeight / 2);
if (mCurrentY <= 0) {
return 0;
}
int index = (int) (mCurrentY / this.mIndexItemHeight);
if (index >= this.mIndexItems.length) {
index = this.mIndexItems.length - 1;
}
return index;
}
继续分析onTouchEvent
函数,在ACTION_DOWN
事件中判断点击的坐标是否在字符的矩阵范围,如果在就调用回调函数把当前位置的字符传递过去,如果不在矩阵范围返回false
,因为在ACTION_DOWN
事件时返回了false
所以就不会接收到后续的事件(ACTION_MOVE
、ACTION_UP
)。每次有ACTION_MOVE
事件产生,都会去重绘控件,重绘时根据当前的位置来计算出周边字符的透明度和TextSize
。最后在ACTION_UP
中重置一些变量。整个过程基本上就是这样。
4、总结
此项目在快速索引的实现上做到了简单易懂,代码规范,扩展性比较好,可以自定义索引内容。