FolderPathView

概述

最近的项目中有个文档管理的需求,类似windows中文件管理的方式,可以嵌套多层的文件夹,需要在文件显示的顶部显示该文件/文件夹的路径,同时点击该路径上对应的文件夹名称,可以快捷的跳转到对应的文件夹位置,具体的项目效果这里就不展示了,可以看下案例的效果图:


增加/删除文件夹

定位到指定的位置

功能分析

简单的分析下需求:

  1. 有一个根目录是固定的,子目录可以进行添加和删除操作;
  2. 子目录的总长度超过了控件的宽度时,默认滚动至右对齐且主目录位置是固定的;
  3. 点击具体的子目录时可以迅速的定位到指定的目录位置;
  4. 监听目录的操作;

总体的样子有点像横向的listview,同时header的位置是固定的;


@A@

代码分析

View中几个方法的区别,这个在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来动态的调整我们根目录绘制的位置,已达到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() 方法,否则是无效的;
查看源码

你可能感兴趣的:(FolderPathView)