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() 方法,否则是无效的;
查看源码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容