Android自定义滑动刻度尺

一 基础:

自定义View实现跟随手指滚动的刻度尺,实现了类似SeekBar的滑动选中效果。项目地址,欢迎star!

UI图:

image

功能:

  • 通过设置最小值跟最大值的范围,以及offset值。View将根据这些数据去计算出需要几个小刻度和几个长刻度,和每个长刻度上面显示的数值。
  • 指针可以随意的定制。
  • 当滑动停止后,刻度尺会根据四舍五入将距离指针最近的长刻度滑动到指针的位置。
  • 支持范围越界回弹。
  • 支持设置默认值。
image

二 实现:

先扯一下,再看别人写的控件的时候总有一种一脸懵逼的感觉,好多凌乱的变量和一大堆的计算逻辑都不知道干嘛用的。比如:PullToRefreshLayout。除非自己按着整体的设计流程写一遍,一步步的写,等出了bug你就明白那些操作的价值。结合之前读第三方控件的经验,写这个刻度尺控件的时候就一步步的去完成,从简单的绘制,到点击事件,再到滑动fling,最后滑动结束更正滑动位置。每一步遇到的问题都记录下来,之后再补全解决方法,这就是成长。

1.绘制刻度

这里省略了onMeasure,这里的需求只是计算一下高度就好了。接着看onDraw方法:

 private void drawRuler(Canvas canvas) {
         mTextIndex = 0;
        for (int index = 0; index <= mRulerHelper.getCounts(); index++) {
            boolean longLine = mRulerHelper.isLongLine(index);
            int lineCount = mLineWidth * index;
            mRect.left = index * mLineSpace + lineCount + mMarginLeft;
            mRect.top = getStartY(longLine);
            mRect.right = mRect.left + mLineWidth;
            mRect.bottom = getEndY();
            if (longLine) {
                if (!mRulerHelper.isFull()) {
                    mRulerHelper.addPoint(mRect.left);
                }
                String text = mRulerHelper.getTextByIndex(mTextIndex);
                mTextIndex++;
                canvas.drawText(text, mRect.centerX(), getMeasuredHeight() - dpFor14, mTextPaint);
            }
            canvas.drawRect(mRect, mLinePaint);
            mRect.setEmpty();
        }
    }

这里解释一下为什么刻度采用Rect而不是设置line的宽度,其实最简单的就是设置Paint的宽度然后canvas.drawLine()。刚绘制的时候就是采用的canvas.drawLine(),绘制完之后发现每个刻度的宽度都被削减了一半,canvas.drawLine()是在设置的(x,y)坐标开始平分line的宽度的(这个你要去体验一下就会明白)。所以给定坐标之后每个刻度看起来就像是被挤了一样,所以才采用Rect简单方便一点。进入正题,绘制有几个问题:

  • 怎么确定要绘制几个Rect?

    这个比较灵活,要看具体的需求了。也就是一大格里面包含几个刻度,一般是包含10个刻度,刻度包括长短刻度。然后一大格刻度表示多少数值,也就是offSet值是多少。之后刻度的范围也要明确并且能被offSet整除,比如范围是(low,height),那么(height-low)/(offSet/10)就是你需要绘制多少个刻度。

     public void setScope(int start, int count,int offSet) {
            if(offSet != 0) {
                this.offSet = offSet;
            }
            lineNumbers = (count - start) / (this.offSet / 10);
        }
    
  • 怎么确定那个是长刻度?

    这个问题要确定一大格之间有几个小刻度了,一般为10个的话,那么当前的index/10能整除就是到了该绘制长刻度的时候了,mRulerHelper.getCounts()就是我们计算出的总共有几个刻度。

    for (int index = 0; index <= mRulerHelper.getCounts(); index++) {
              boolean longLine = mRulerHelper.isLongLine(index);
              ...
              if (longLine) {
                  canvas.drawText(text, mRect.centerX(), getMeasuredHeight() - dpFor14, mTextPaint);
              }
              canvas.drawRect(mRect, mLinePaint);
    }                                               
    

之后呢就是我们计算Rect的左边跟绘制Text的坐标了。。。不细讲。。。具体可看这里啊。

有个问题就是你得明白Rect的left top right bottom分别表示那个区间:

[图片上传失败...(image-5d1f26-1554206618213)]

2.处理点击事件

目前采取的是点击该View的事件全拦截,感觉也没别的什么需求需要过滤事件了。事件处理起来很简单的就是计算出每次移动的差值就好了:

           case MotionEvent.ACTION_DOWN:
                mPressUp = false;
                isFling = false;
                startX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                mPressUp = false;
                float distance = event.getX() - startX;
                if (mPreDistance != distance) {
                    doScroll((int) -distance, 0, 0);
                    invalidate();
                }
                startX = event.getX();
                break;

问题就是:

  • 怎么实现滑动的效果?

    刻度尺如果范围很大的话总宽度肯定会超出屏幕的,但是Canvas不会绘制屏幕之外的部分,除非等到屏幕之外的部分显示出来。另外让View滑动的方法很多,最初使用的是scrollTo方法,该方法滑动的是View的内容,也符合我们要的效果,不过结果查强人意。差值计算之后稍微一滑动,刻度直接没了,成了一片空白,看起来那个变化值也不大,ok!这是一个疑问ScrollTo+invalidate内容不会显示,直接没了。之后呢换成了Scroller,这个玩意不用太多的介绍了,使用之后便达到了我们想要的效果,一样的变化值。

     private void doScroll(int dx, int dy, int duration) {
          mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), dx, dy, duration);
     }
    
    

是否有疑问?既然屏幕之外的东西Canvas不会去绘制,那么滑动的时候肯定是将屏幕之外的部分滑到屏幕中,也就是在滑动的过程中要继续绘制。从上面的绘制代码能看到这个绘制过程中跟滑动并没有任何的联系,只是单纯的for循环绘制而已,为什么呢?第一 我们scrollTo移动的是View的内容,一开始View的实际宽度会超过屏幕的宽度,当没有滑动的时候,View只会绘制屏幕中的可见区域,即使for循环依然执行也不会绘制到屏幕外面,然后在滑动的时候会不断的触发invalidate()方法,也就是for循环会被触发,View开始在新出现的未绘制的区域绘制。已经绘制过的区域会被滑出屏幕,这样就会给用户一个平滑的效果。做完以上两步你的刻度尺已经有了滑动的效果了。下面就是解决边界的问题。

3.边界的处理

UI说当超过边界之后松手回弹,这样的交互效果好。这种交互其实最简单了,在手指离开的时候计算当前的x坐标距离中心指针的x坐标的距离,然后让Scroller去执行回弹的效果。不过这个操作是整个控件中最为重要的一步,因为当手指抬起的时候,中间指针必须指向一个长刻度,不能停留再短刻度上面,那这个操作就跟边界回弹的操作重合了,边界回弹也是让最小或者最大长刻度滑动到中间指针的位置。所以松手之后的操作就分为三种:

currentX :滑动停止时的x坐标。

Point:中间指针位置。

low:刻度尺的最小边界。

height:刻度尺的最大边界。

  • 当前的currentX小于中间指针刻度Point的x坐标,并且小于刻度的最小值low的x坐标。

    -----------------Point-currentX--low------height----------

  • 当前的currentX小于中间指针刻度Point的x坐标,并且大于刻度的最小值low表示的x坐标小于刻度尺的最大刻度height的x坐标。

    ------low-------currentX--Point--------height----------

  • 当前的currentX大于中间指针刻度Point的x坐标,并且大于刻度的最大值height表示的x坐标。

    ------low-------height-----currentX-Point-------

简单的表示了一下三种位置。

处理就是,先计算出滑动结束之后的当前x坐标跟中间Point的x坐标的距离,然后不为0就使用Scroller滑动:

//计算距离
public int getScrollDistance(int x) {
        for (int i = 0; i < mPoints.size(); i++) {
            int pointX = mPoints.get(i);
            if (0 == i && x < pointX) {
                //当前的x比第一个位置的x坐标都小 也就是需要往右移动到第一个长线的位置.
                setCurrentText(0);
                return x - pointX;
            } else if (i == mPoints.size() - 1 && x > pointX) {
                //当前的x比最后一个左边的x都大,也就是需要往左移动到最后一个长线位置.
                setCurrentText(texts.size() - 1);
                return x - pointX;
            } else {
                if (i + 1 < mPoints.size()) {
                    int nextX = mPoints.get(i + 1);
                    if (x > pointX && x <= nextX) {
                        int distance = (nextX - pointX) / 2;
                        int dis = x - pointX;
                        if (dis > distance) {
                            //说明往下一个移动
                            setCurrentText(i + 1);
                            return x - nextX;
                        } else {
                            setCurrentText(i);
                            //往前一个移动
                            return x - pointX;
                        }
                    }
                }
            }
        }
        return 0;
    }

开始执行滑动:

 public void scrollFinish() {
        int finalX = mScroller.getFinalX();
        int centerPointX = mRulerHelper.getCenterPointX();
        int currentX = centerPointX + finalX;
        int scrollDistance = mRulerHelper.getScrollDistance(currentX);
        if (0 != scrollDistance) {
            //第一个参数是滚动开始时的x的坐标
            //第二个参数是滚动开始时的y的坐标
            //第三个参数是在X轴上滚动的距离, 负数向右滚动.
            //第四个参数是在Y轴上滚动的距离,负数向下滚动.
            mScroller.startScroll(mScroller.getFinalX(), mScroller.getFinalY(), -scrollDistance, 0, 300);
            invalidate();
            if (scrollSelected != null) {
                scrollSelected.selected(getCurrentText());
            }
        }
    }

这样已经可以使用了,滑动的刻度尺已经完成了。不过交给UI一看,人家说这东西怎么那么难滑动呢,每次怎么只能滑一大格呢,我要那种fling的感觉。确实,因为在MotionEvent.ACTION_UP的时候都会去矫正一下位置,所以给使用者的感觉就是一次只能滑一格,滑动体验很不好,只能去增加fling。。。

4.fling

增加fling多简单啊,Scroller不是有这个方法吗mScroller.fling(),使用方法这里不再介绍了。fling增加之后,用户的体验确实好了很多,不过一个新的问题出现了,就是在fling停止之后怎么矫正位置呢?这是个大问题,卡住了好大一会儿,最终找到了解决方法:

 @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            //这里是结束之后调用矫正位置的方法。scrollFinish()。
            if (mScroller.getCurrX() == mScroller.getFinalX() && mPressUp && isFling) {
                mPressUp = false;
                isFling = false;
                scrollFinish();
            }
            scrollTo(mScroller.getCurrX(), 0);
            invalidate();
        }
        super.computeScroll();
    }

三 结束

效果在文章一开始已经展示出来了,指针并没有在该自定义View中绘制,底部的线也是,因为对于指针的需求是多变的,所以用了一个自定义的ViewGroup去完成剩余的指针和底部的实线。底部的实线放在Group中是因为我们的UI效果,底部的实线上面可以没有刻度,也就是这个底部的线是固定在底部,比我画在刻度下面跟随刻度滑动要简单的多。想到之后的变体,感觉刻度本身的View跟指针分开是比较好扩展的,Group只需要给刻度尺控件传入中间指针的(x,y)坐标就好了。

有好多学习的资源~
我的Github
我的简书

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

推荐阅读更多精彩内容