概述
最近的项目中有个文档管理的需求,类似windows中文件管理的方式,可以嵌套多层的文件夹,需要在文件显示的顶部显示该文件/文件夹的路径,同时点击该路径上对应的文件夹名称,可以快捷的跳转到对应的文件夹位置,具体的项目效果这里就不展示了,可以看下案例的效果图:
功能分析
简单的分析下需求:
- 有一个根目录是固定的,子目录可以进行添加和删除操作;
- 子目录的总长度超过了控件的宽度时,默认滚动至右对齐且主目录位置是固定的;
- 点击具体的子目录时可以迅速的定位到指定的目录位置;
- 监听目录的操作;
总体的样子有点像横向的listview,同时header的位置是固定的;
代码分析
View中几个方法的区别,这个在Activity中给FolderPath赋值的时候会用到:
requestLayout():调用此方法会从View的onMeasure()方法开始重绘;
invalidate(): 调用此方法会从View的onDraw()方法开始重绘;
我们来分析下两种情况:
- 所有目录的总宽度小于View的宽度,此时只需要左对齐,按照顺序排列;
- 所有目录的总宽度大于View的宽度,此时左侧有一个主目录,子目录右对齐,同时可以滚动;
接下来我们通过代码来说明来分析,demo的地址会在文章的最后面给出;
首先我们需要在onDraw()
方法之前计算出folder的总长度,然后判断是否需要右对齐,因为folder的总长度是可变的,后面的 onTouchEvent()
方法调用了 invalidate()方法来更新界面,因此计算folder的总长度以及右对齐我们就放到了onMeasure()
方法里面,所有涉及到folder长度的参数的刷新我们都需要调用requestLayout()来刷新界面;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
mWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
mHeight = getMeasuredHeight();
calculateScroll();
}
/**
* 计算滚动
*/
private void calculateScroll() {
int foldersLength = getFoldersPathLength();
scrollTo(0, getScrollY());
if (foldersLength > mWidth) {
// 计算scrollTo的距离
mScrollOffset = foldersLength - mWidth;
scrollTo(mScrollOffset, getScrollY());
}
}
/**
* 获取文本的边框
*
* @param str
* @return
*/
private Rect getTextBounds(String str) {
Rect rect = new Rect();
if (TextUtils.isEmpty(str)) {
return rect;
}
mTextPaint.getTextBounds(str, 0, str.length(), rect);
return rect;
}
由于界面涉及到Scroll滚动,同时有个根目录位置是固定的,所以我们会在onDraw()
方法中绘制两次,第一次是绘制所有的folder(可以滚动),第二次通过mScrollX来动态的调整我们根目录绘制的位置,已达到View在滚动,根目录位置不变的效果,如对寻找文字基线不了解的可以查看我的文章;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 计算所有文本的基线,已达到所有的文本绘制在同一条直线上的效果
calculateBaseLine();
drawFoldersPath(canvas);
drawRootFolderPath(canvas);
}
/**
* 绘制所有目录(包含主目录)
*
* @param canvas
*/
private void drawFoldersPath(Canvas canvas) {
int startX = getPaddingLeft();
for (int i = 0; i < mFolders.size(); i++) {
String folder = mFolders.get(i);
// 绘制文本
canvas.drawText(folder, startX, mBaseLine, mTextPaint);
Rect rect = getTextBounds(folder);
startX += rect.width() + mSpan;
startX = drawSeparator(canvas, startX);
}
}
/**
* 绘制分隔符
*
* @param canvas
* @param startX
* @return
*/
private int drawSeparator(Canvas canvas, int startX) {
// 绘制分隔符
if (mSeparator != null) {
canvas.drawBitmap(mSeparator, startX, (mHeight - mSeparatorHeight) / 2, mTextPaint);
startX += mSeparatorWidth + mSpan;
} else {
startX += mSpan;
}
return startX;
}
/**
* 计算文本绘制的基线
*/
private void calculateBaseLine() {
Paint.FontMetrics metrics = mTextPaint.getFontMetrics();
mBaseLine = (int) ((mHeight + Math.abs(metrics.descent + metrics.ascent)) / 2);
}
/**
* 绘制主目录
*
* @param canvas
*/
private void drawRootFolderPath(Canvas canvas) {
mRootBgPaint.setColor(getBgColor());
int w = getMeasuredWidth() + getScrollX();
// 绘制右边的padding
canvas.drawRect(w - getPaddingRight(), 0, w, getMeasuredHeight(), mRootBgPaint);
w = getRootFolderPathLength() + getPaddingLeft() + getScrollX();
// 绘制主目录
canvas.drawRect(0, 0, w, getMeasuredHeight(), mRootBgPaint);
int startX = getPaddingLeft() + getScrollX();
// 绘制主目录
canvas.drawText(mRootFolder, startX, mBaseLine, mTextPaint);
Rect rect = getTextBounds(mRootFolder);
startX += rect.width();
// 绘制分隔的图标
if (mSeparator != null) {
startX += mSpan;
canvas.drawBitmap(mSeparator, startX, (mHeight - mSeparatorHeight) / 2, mTextPaint);
startX += mSeparatorWidth;
}
startX += mScrollSpan;
// 绘制滚动分界线
if (getFoldersPathLength() > mWidth) {
mRootBgPaint.setColor(mScrollSpanColor);
canvas.drawRect(startX - mScrollSpanWidth, (mHeight - rect.height()) / 2, startX, (mHeight + rect.height()) / 2, mRootBgPaint);
}
}
如果folder的长度超过View的宽度,就需要滚动View,同时我们需要计算点击位置对应folder的位置,因此我们需要重写onTouchEvent()
方法,这里的关键在于计算点击的位置的时候,我们需要加上mScrollX;
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
pointX = (int) event.getX();
pointY = (int) event.getY();
arrowScroll = true;
isClick = true;
// 判断down的位置来决定是否可以执行滚动操作
if (pointX <= getRootFolderPathLength() + getPaddingLeft()
|| getFoldersPathLength() <= mWidth) {
arrowScroll = false;
}
break;
case MotionEvent.ACTION_MOVE:
if (!arrowScroll) {
break;
}
if (Math.abs(event.getY() - pointY) > mTouchSlop) {
isClick = false;
pointY = (int) event.getY();
}
int distance = (int) (pointX - event.getX());
if (Math.abs(distance) >= mTouchSlop) {
isClick = false;
if (distance + getScrollX() < 0) {
distance = -getScrollX();
} else if (distance + getScrollX() > mScrollOffset) {
distance = mScrollOffset - getScrollX();
}
scrollBy(distance, getScrollY());
pointX = (int) event.getX();
return true;
}
break;
case MotionEvent.ACTION_UP:
int y = (int) event.getY();
// 滚动事件,不触发点击事件
if (!isClick || y < 0 || y > mHeight) {
return true;
}
pointX = (int) event.getX();
// 计算我们点击的位置所对应的folder
int position = checkPosition(pointX);
if (position != -1) {
removeFoldersToPosition(position + 1);
}
break;
}
// 刷新界面,因此不能在 onDraw 方法中调用 scrollTo 方法
invalidate();
return super.onTouchEvent(event);
}
/**
* 判断点击的位置
*
* @param x
* @return
*/
private int checkPosition(int x) {
int position = -1;
// 点击的是padding的位置
if (x < getPaddingLeft() || x > getMeasuredWidth() - getPaddingRight()) {
return position;
}
// 点击的是主目录的位置
if (x <= getRootFolderPathLength()) {
if (mFolders.size() > 1) {
position = 0;
return position;
}
}
// 计算x位置对应的folder,这里需要注意mScrollX对folder的影响
if (mFolders.size() > 1) {
x += getScrollX();
int startX = getPaddingLeft() + getRootFolderPathLength() - mScrollSpan;
int endX = startX;
for (int i = 1; i < mFolders.size(); i++) {
String folder = mFolders.get(i);
Rect rect = getTextBounds(folder);
endX += rect.width() + mSpan;
if (mSeparator != null) {
endX += mSeparatorWidth + mSpan;
}
if (x >= startX && x < endX) {
return i;
}
startX = endX;
}
}
return position;
}
最后就是监听我们的folder的增加和删除,然后通过回调函数将我们folder信息传递出去,这里需要注意的是,由于计算folder的长度是在 onMeasure()
方法中进行的,因此涉及到folder长度的操作,刷新界面需要调用 requestLayout() 方法,否则是无效的;
查看源码