FolderPathView

概述

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


增加/刪除文件夾

定位到指定的位置

功能分析

簡單的分析下需求:

  1. 有一個根目錄是固定的,子目錄可以進行添加和刪除操作;
  2. 子目錄的總長度超過了控件的寬度時,默認滾動至右對齊且主目錄位置是固定的;
  3. 點擊具體的子目錄時可以迅速的定位到指定的目錄位置;
  4. 監(jiān)聽目錄的操作;

總體的樣子有點像橫向的listview,同時header的位置是固定的;


@A@

代碼分析

View中幾個方法的區(qū)別,這個在Activity中給FolderPath賦值的時候會用到:
requestLayout():調用此方法會從View的onMeasure()方法開始重繪;
invalidate(): 調用此方法會從View的onDraw()方法開始重繪;

我們來分析下兩種情況:

  1. 所有目錄的總寬度小于View的寬度,此時只需要左對齊,按照順序排列;
  2. 所有目錄的總寬度大于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() 方法,否則是無效的;
查看源碼

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容