Android 滑动选择身高体重控件——RulerView

本文已授权微信公众号:鸿洋(hongyangAndroid)在微信公众号平台原创首发

前言

隔一段时间工作不忙的时候就想温习一下view相关的知识,比起学习其他东西,感觉做控件不会显的枯燥,日复一日做着重复的工作,维护着项目,总想在里面添加一些新的东西,比如新的界面开始用kotlin,用的第三方不是很满意的控件,想想不是很难就自己来做,闲来无聊就看看python入门,最近项目多了一个需要滑动选择身高和运动时间的控件,在github上没找到合适的,正好抛物线大神发起了一个自定义view的仿写活动,一举两得,就有了该控件。

封面图

封面.png

效果图

RulerViewGif.gif

2017/11/29 新添功能

gif2

使用computeScrollTo(float)

2017/12/22 新添功能

image.png

增加了scaleLimit属性用来设置相邻2个刻度之间的数量属性

支持设置的属性

        <attr name="scaleLimit" format="integer" />                  <!--相邻2个刻度之间的数量-->
        <attr name="rulerHeight" format="dimension" />               <!--尺子的高度-->
        <attr name="rulerToResultgap" format="dimension" />          <!--尺子距离结果的高度-->
        <attr name="scaleGap" format="dimension" />                  <!--刻度间距-->
        <attr name="scaleCount" format="integer" />                  <!--刻度数-->
        <attr name="firstScale" format="float" />                    <!--默认选中的刻度-->
        <attr name="maxScale" format="integer" />                    <!--最大刻度-->
        <attr name="minScale" format="integer" />                    <!--最小刻度-->
        <attr name="bgColor" format="color" />                       <!--背景色-->
        <attr name="smallScaleColor" format="color" />               <!--小刻度的颜色-->
        <attr name="midScaleColor" format="color" />                 <!--中刻度的颜色-->
        <attr name="largeScaleColor" format="color" />               <!--大刻度的颜色-->
        <attr name="scaleNumColor" format="color" />                 <!--刻度数的颜色-->
        <attr name="resultNumColor" format="color" />                <!--结果字体的颜色-->
        <attr name="unit" format="string" />                         <!--单位-->
        <attr name="unitColor" format="color" />                     <!--单位颜色-->
        <attr name="smallScaleStroke" format="dimension" />          <!--小刻度的宽度-->
        <attr name="midScaleStroke" format="dimension" />            <!--中刻度的宽度-->
        <attr name="largeScaleStroke" format="dimension" />          <!--大刻度的宽度-->
        <attr name="resultNumTextSize" format="dimension" />         <!--结果字体大小-->
        <attr name="scaleNumTextSize" format="dimension" />          <!--刻度字体大小-->
        <attr name="unitTextSize" format="dimension" />              <!--单位字体大小-->
        <attr name="showScaleResult" format="boolean" />             <!--是否显示结果值-->
        <attr name="isBgRoundRect" format="boolean" />               <!--背景是否圆角-->

使用

compile 'com.github.superSp:RulerView:v1.5'

源码地址

实现思路

  • 初始化画笔,以及其他需要的参数
  • 重写onMeasuer()确定尺子的大小
  • 重写onDraw()绘画出静态尺子,并且将一些滑动时需要改变的参数设置为变量,绘制时只绘制当前屏幕可见区域,滑动尺子时,只刷新当前屏幕模拟滑动并不是真正的滑动
  • 重写onTouchEvent()处理滑动,增加滑动速率监听VelocityTracker以及惯性滑动以及抬起手指时指针落在刻度上面需要的属性动画ValueAnimator

实现过程

测量

控件的高度=尺子的高度+结果值的高度+尺子距离结果值的高度
控件的宽度=屏幕宽度或者固定宽度

@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int heightModule = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        switch (heightModule) {
            case MeasureSpec.AT_MOST:
                height = rulerHeight + (showScaleResult ? resultNumRect.height() : 0) + rulerToResultgap * 2 + getPaddingTop() + getPaddingBottom();
                break;
            case MeasureSpec.UNSPECIFIED:
            case MeasureSpec.EXACTLY:
                height = heightSize + getPaddingTop() + getPaddingBottom();
                break;
        }

        width = widthSize + getPaddingLeft() + getPaddingRight();

        setMeasuredDimension(width, height);

    }

绘制静态尺子

  • 绘制背景
    drawRect()
    private void drawBg(Canvas canvas) {
        bgRect.set(0, 0, width, height);
        if (isBgRoundRect) {
            canvas.drawRoundRect(bgRect, 20, 20, bgPaint); //20->椭圆的用于圆形角x-radius
        } else {
            canvas.drawRect(bgRect, bgPaint);
        }
    }

  • 绘制尺子
    这一步是绘制控件中最为复杂的一步,需要考虑初始如何默认选中初始刻度,手指抬起时候尺子需要滑动到的位置,计算当前所处刻度等等。

绘制滑动类型的view时,当初的想法是一次性绘制出全部内容,之后使用canvas.clipRect()裁剪掉不可见区域,但是如果内容区域比较大,例如需要绘制1000个内容,则没滑动一次for循环需要执行1000次,而且刻度越大时候循环越多,占用内存更大,会造成卡顿,因此换了另外一种思路,只绘制当前屏幕可见区域内容,这样无论刻度有多大,暂用的内存都很小,滑动时,通过不断刷新来模拟滑动,做到以假乱真的效果。。。

private void drawScaleAndNum(Canvas canvas) {
        canvas.translate(0, (showScaleResult ? resultNumRect.height() : 0) + rulerToResultgap);//移动画布到结果值的下面

        int num1;//确定刻度位置
        float num2;

        if (firstScale != -1) {   //第一次进来的时候计算出默认刻度对应的假设滑动的距离moveX
            moveX = getWhichScalMovex(firstScale);          //如果设置了默认滑动位置,计算出需要滑动的距离
            lastMoveX = moveX;
            firstScale = -1;                                //将结果置为-1,下次不再计算初始位置
        }

        num1 = -(int) (moveX / scaleGap);                   //滑动刻度的整数部分
        num2 = (moveX % scaleGap);                         //滑动刻度的小数部分

        canvas.save();                                      //保存当前画布

        rulerRight = 0;                                    //准备开始绘制当前屏幕,从最左面开始

        if (isUp) {   //这部分代码主要是计算手指抬起时,惯性滑动结束时,刻度需要停留的位置
            num2 = ((moveX - width / 2 % scaleGap) % scaleGap);     //计算滑动位置距离整点刻度的小数部分距离
            if (num2 <= 0) {
                num2 = scaleGap - Math.abs(num2);
            }
            leftScroll = (int) Math.abs(num2);                        //当前滑动位置距离左边整点刻度的距离
            rightScroll = (int) (scaleGap - Math.abs(num2));         //当前滑动位置距离右边整点刻度的距离

            float moveX2 = num2 <= scaleGap / 2 ? moveX - leftScroll : moveX + rightScroll; //最终计算出当前位置到整点刻度位置需要滑动的距离

            if (valueAnimator != null && !valueAnimator.isRunning()) {      //手指抬起,并且当前没有惯性滑动在进行,创建一个惯性滑动
                valueAnimator = ValueAnimator.ofFloat(moveX, moveX2);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        moveX = (float) animation.getAnimatedValue();            //不断滑动去更新新的位置
                        lastMoveX = moveX;
                        invalidate();
                    }
                });
                valueAnimator.addListener(new AnimatorListenerAdapter() {       //增加一个监听,用来返回给使用者滑动结束后的最终结果刻度值
                    @Override
                    public void onAnimationEnd(Animator animation) {
                        super.onAnimationEnd(animation);
                        //这里是滑动结束时候回调给使用者的结果值
                        if (onChooseResulterListener != null) {
                            onChooseResulterListener.onEndResult(resultText);
                        }
                    }
                });
                valueAnimator.setDuration(300);
                valueAnimator.start();
                isUp = false;
            }

            num1 = (int) -(moveX / scaleGap);                //重新计算当前滑动位置的整数以及小数位置
            num2 = (moveX % scaleGap);
        }
        canvas.translate(num2, 0);    //不加该偏移的话,滑动时刻度不会落在0~1之间只会落在整数上面,其实这个都能设置一种模式了,毕竟初衷就是指针不会落在小数上面

        //这里是滑动时候不断回调给使用者的结果值
        resultText = String.valueOf(new WeakReference<>(new BigDecimal((width / 2 - moveX) / (scaleGap * scaleCount))).get().setScale(1, BigDecimal.ROUND_HALF_UP).floatValue() + minScale);
        if (onChooseResulterListener != null) {
            onChooseResulterListener.onScrollResult(resultText); //接口不断回调给使用者结果值
        }
        //绘制当前屏幕可见刻度,不需要裁剪屏幕,while循环只会执行·屏幕宽度/刻度宽度·次,大部分的绘制都是if(curDis<width)这样子内存暂用相对来说会比较高。。
        while (rulerRight < width) {
            if (num1 % scaleCount == 0) {    //绘制整点刻度以及文字
                if ((moveX >= 0 && rulerRight < moveX - scaleGap) || width / 2 - rulerRight <= getWhichScalMovex(maxScale + 1) - moveX) {
                    //当滑动出范围的话,不绘制,去除左右边界
                } else {
                    //绘制刻度,绘制刻度数字
                    canvas.drawLine(0, 0, 0, midScaleHeight, midScalePaint);
                    scaleNumPaint.getTextBounds(num1 / scaleGap + minScale + "", 0, (num1 / scaleGap + minScale + "").length(), scaleNumRect);
                    canvas.drawText(num1 / scaleCount + minScale + "", -scaleNumRect.width() / 2, lagScaleHeight +
                            (rulerHeight - lagScaleHeight) / 2 + scaleNumRect.height(), scaleNumPaint);
                }

            } else {   //绘制小数刻度
                if ((moveX >= 0 && rulerRight < moveX) || width / 2 - rulerRight < getWhichScalMovex(maxScale) - moveX) {
                    //当滑动出范围的话,不绘制,去除左右边界
                } else {
                    //绘制小数刻度
                    canvas.drawLine(0, 0, 0, smallScaleHeight, smallScalePaint);
                }
            }
            ++num1;  //刻度加1
            rulerRight += scaleGap;  //绘制屏幕的距离在原有基础上+1个刻度间距
            canvas.translate(scaleGap, 0); //移动画布到下一个刻度
        }

        canvas.restore();
        //绘制屏幕中间用来选中刻度的最大刻度
        canvas.drawLine(width / 2, 0, width / 2, lagScaleHeight, lagScalePaint);

    }

绘制结果

 //绘制上面的结果 结果值+单位
    private void drawResultText(Canvas canvas, String resultText) {
        if (!showScaleResult) {   //判断用户是否设置需要显示当前刻度值,如果否则取消绘制
            return;
        }
        canvas.translate(0, -resultNumRect.height() - rulerToResultgap / 2);  //移动画布到正确的位置来绘制结果值
        resultNumPaint.getTextBounds(resultText, 0, resultText.length(), resultNumRect);
        canvas.drawText(resultText, width / 2 - resultNumRect.width() / 2, resultNumRect.height(), //绘制当前刻度结果值
                resultNumPaint);
        resultNumRight = width / 2 + resultNumRect.width() / 2 + 10;
        canvas.drawText(unit, resultNumRight, kgRect.height() + 2, kgPaint);            //在当前刻度结果值的又面10px的位置绘制单位
    }

处理滑动

主要是记录moveX,以及添加velocityTracker速度监听器,以及处理惯性滑动

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        currentX = event.getX();
        isUp = false;
        velocityTracker.computeCurrentVelocity(500);
        velocityTracker.addMovement(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                //按下时如果属性动画还没执行完,就终止,记录下当前按下点的位置
                if (valueAnimator != null && valueAnimator.isRunning()) {
                    valueAnimator.end();
                    valueAnimator.cancel();
                }
                downX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                //滑动时候,通过假设的滑动距离,做超出左边界以及右边界的限制。
                moveX = currentX - downX + lastMoveX;
                if (moveX >= width / 2) {
                    moveX = width / 2;
                } else if (moveX <= getWhichScalMovex(maxScale)) {
                    moveX = getWhichScalMovex(maxScale);
                }
                break;
            case MotionEvent.ACTION_UP:
                //手指抬起时候制造惯性滑动
                lastMoveX = moveX;
                xVelocity = (int) velocityTracker.getXVelocity();
                autoVelocityScroll(xVelocity);
                velocityTracker.clear();
                break;
        }
        invalidate();
        return true;
    }

处理惯性滑动的代码

这里就是调节了,根据得到的速率调节出比较舒服的滑动。。。

private void autoVelocityScroll(int xVelocity) {
        //惯性滑动的代码,速率和滑动距离,以及滑动时间需要控制的很好,应该网上已经有关于这方面的算法了吧。。这里是经过N次测试调节出来的惯性滑动
        if (Math.abs(xVelocity) < 50) {
            isUp = true;
            return;
        }
        if (valueAnimator.isRunning()) {
            return;
        }
        valueAnimator = ValueAnimator.ofInt(0, xVelocity / 20).setDuration(Math.abs(xVelocity / 10));
        valueAnimator.setInterpolator(new DecelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                moveX += (int) animation.getAnimatedValue();
                if (moveX >= width / 2) {
                    moveX = width / 2;
                } else if (moveX <= getWhichScalMovex(maxScale)) {
                    moveX = getWhichScalMovex(maxScale);
                }
                lastMoveX = moveX;
                invalidate();
            }

        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                isUp = true;
                invalidate();
            }
        });

        valueAnimator.start();
    }

供外部使用的获取结果值的接口

    public interface OnChooseResulterListener {
        void onEndResult(String result);      //结束滑动时候返回的结果
        void onScrollResult(String result);   //滑动时不断产生的结果
    }

最后再贴一下使用以及地址

compile 'com.github.superSp:RulerView:v1.5'

源码地址

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