概述
最近的項目中有個文檔管理的需求,類似windows中文件管理的方式,可以嵌套多層的文件夾,需要在文件顯示的頂部顯示該文件/文件夾的路徑,同時點擊該路徑上對應的文件夾名稱,可以快捷的跳轉到對應的文件夾位置,具體的項目效果這里就不展示了,可以看下案例的效果圖:


功能分析
簡單的分析下需求:
- 有一個根目錄是固定的,子目錄可以進行添加和刪除操作;
- 子目錄的總長度超過了控件的寬度時,默認滾動至右對齊且主目錄位置是固定的;
- 點擊具體的子目錄時可以迅速的定位到指定的目錄位置;
- 監(jiān)聽目錄的操作;
總體的樣子有點像橫向的listview,同時header的位置是固定的;

代碼分析
View中幾個方法的區(qū)別,這個在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來動態(tài)的調整我們根目錄繪制的位置,已達到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的位置來決定是否可以執(zhí)行滾動操作
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();
// 滾動事件,不觸發(fā)點擊事件
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;
}
最后就是監(jiān)聽我們的folder的增加和刪除,然后通過回調函數將我們folder信息傳遞出去,這里需要注意的是,由于計算folder的長度是在 onMeasure() 方法中進行的,因此涉及到folder長度的操作,刷新界面需要調用 requestLayout() 方法,否則是無效的;
查看源碼