美文网首页安卓开发笔记Android UIAndroid效果/自定义
Android 滑动选择身高体重控件——RulerView

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

作者: 超神的菠萝 | 来源:发表于2017-10-19 10:17 被阅读3196次

    本文已授权微信公众号:鸿洋(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.4'

    源码地址

    实现思路

    • 初始化画笔,以及其他需要的参数
    • 重写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.4'

    源码地址

    相关文章

      网友评论

      • 4d226614f116:大佬我们项目的需求是 弧形的 ,可以设置吗?
        超神的菠萝:抱歉暂时还不支持弧形
      • b16ab2615da5:嵌入scrollview 滚动的似乎会有冲突,有没有什么好的解决办法?
        超神的菠萝:是左右滑动尺子的时候 会不小心触发上下滑动吗?刚刚外层使用scrollview的时候 没发现太大的问题啊。。
      • 未聞椛洺:可不可以设置成竖直的呢
        超神的菠萝:@未聞椛洺 不好意思啊。。暂且不支持竖直的属性设置
      • andriod小学徒:scaleLimit(相邻2个刻度之间的数量),scaleCount(刻度数-)这两个数值y用的时候有些冲突,我用来显示月份从1到12月,我想以1(刻度)位单位移动,我最大值和最小值1和12,UI效果末尾会出现13,scaleCount(这里的数量是控制两大刻度之间的数量size-1),我试了好10遍,效果都达不到我要的结果(从1到12,每个刻度移动都是1个整数)
        超神的菠萝:不好意思啊,具体的意思不是很理解~~~github上面我看了下有关月份设置的还是有很多的 可以先看下这些是不是比这个更适用些~~~
        https://github.com/lantouzi/WheelView-Android
        https://github.com/wangjiegulu/WheelView
        https://github.com/xuningjack/year_month_wheel_dialog
      • andriod小学徒:这个可不可以改为竖向滑动呢,最近在开发项目时间比较紧,想先拿来用,自定义水平比较菜,:blush:
        超神的菠萝:@andriod小学徒 进度动画效果,就是指定一个默认值自动滚动到目标值?
        andriod小学徒:@超神的菠萝 那个大神有空的话可以改动下这个链接作者的一个自定义控件效果(是一个带温度进度手动触发圆形锯齿旋转进度控件,能否改为自定动进度动画效果)http://blog.csdn.net/kong_gu_you_lan/article/details/53573439
        超神的菠萝:额。。竖向滑动的话项目改动较大可能涉及到重做~~~
      • JarryLeo:Scroller 类原生处理惯性滑动的,很好用;
        超神的菠萝::sob: 当时滑动第一时间想到是scroller,最后发现不适用。。换了属性动画:sob:
      • hashhy:请问可以更改数值吗,都是以1为单位滑动的,能不能把数据调大,比如100一跳。
        超神的菠萝:@hashhy 刚刚更新了一个1.4版本上去,新增加了scaleLimt属性可以设置单位滑动。。那个指针图片不是很理解具体效果是什么样子- - 暂且还没加。。
        hashhy:@超神的菠萝 谢谢,一点个人的小建议,如果可以给指针加图片就更棒了。做得很棒,学到了很多,如果有更新,会继续支持的。
        超神的菠萝:额。。没考虑过这个功能,不过改起来应该不难,我待会儿不忙升级个版本吧:sob:
      • 奋斗小青年Jerome:感谢作者的思路!!! 看完你的demo,我顺手就把我自己写的曲线图加了一个惯性动画进去:stuck_out_tongue_winking_eye:
      • 得瑟的小蚂蚁:moveX = getWhichScalMovex(firstScale); 注释上说是滑动距离

        private float getWhichScalMovex(float scale) {
        return width / 2 - scaleGap * scaleCount * (scale - minScale);
        }
        上面方法里的返回值滑动距离怎么理解啊?如果当前刻度是0,最小刻度minScale也是0,滑动距离就是width/2
        超神的菠萝:@frinda 嗯~~因为绘制的时候是从屏幕最左边开始的~~~
        得瑟的小蚂蚁:@超神的菠萝 噢,明白了,你默认位置0不是在中央
        超神的菠萝:当前刻度是0表示的是选择中的刻度是0,即中间的选择刻度指向的是0,0要滑动屏幕中央的话,滑动距离正好是width/2。。
      • 74a6fe7ac4f8:请问这几行代码怎么理解
        if (moveX >= width / 2) {
        moveX = width / 2;
        } else if (moveX <= getWhichScalMovex(maxScale)) {
        moveX = getWhichScalMovex(maxScale);
        }
        74a6fe7ac4f8:@超神的菠萝 理解了,多谢:smile:
        超神的菠萝:作用是滑动尺子的时候不允许尺子超出刻度的最大值和最小值。。。 widh/2是尺子的左边界,默认moveX=0,初始尺子向右滑动超过屏幕的一半的时选中的刻度正好是左边界,因此不能再滑动,右边界和这个类似。。都是计算出滑动的临界值,限制一下当超出的时候让他等于临界值。。。。
      • 不会飞的小猪:我一开始想的是横向的RecycleView也是可以实现的。
        超神的菠萝:可以的喔。。就是不知道实现过程中会不会遇到什么麻烦:mask:

      本文标题:Android 滑动选择身高体重控件——RulerView

      本文链接:https://www.haomeiwen.com/subject/piayuxtx.html