Android滚动刻度尺实现

作者: LichFaker | 来源:发表于2016-03-20 21:08 被阅读3807次

    缘起

    最近在帮人做一个计步器,其中涉及到身高、体重等信息的采集;我参考了众多app的实现,觉得"乐动力"中滑动刻度的方式比较优雅。于是乎,反编译了该app,结果发现它是采用图片的方式实现的,即ScrollView内嵌了一张带刻度的图片。
    个人觉得该方式太不灵活,且对美工的依赖较大,于是便想自定义一个刻度尺控件。

    需求分析

    1. 绘制刻度,区分整值刻度和普通刻度
    • 红色指针始终在刻度尺的中间,表示当前的刻度
    • 刻度的最大值和最小值可动态设置
    • 刻度尺的高度或宽度可设置,设置后中间刻度不变
    • 可滑动,滑动后当前刻度随之改变

    涉及的知识点

    1. View的机制
    • canvas绘图
    • Scroller工具类的使用
    • 自定义View的属性
    • 点击、滑动事件的处理

    最终效果

    由于简书上无法嵌入gif,为不影响效果,请移步github查看,如果觉得不错,帮忙给个star _
    https://github.com/LichFaker/ScaleView

    实现过程

    1. 新建一个class:HorizontalScaleScrollView, 继承自View
    • 在构造方法中获取自定义属性:
    protected void init(AttributeSet attrs) {    
        // 获取自定义属性    
        TypedArray ta = getContext().obtainStyledAttributes(attrs, ATTR);   
        mMin = ta.getInteger(LF_SCALE_MIN, 0);    
        mMax = ta.getInteger(LF_SCALE_MAX, 200);    
        mScaleMargin = ta.getDimensionPixelOffset(LF_SCALE_MARGIN, 15);    
        mScaleHeight = ta.getDimensionPixelOffset(LF_SCALE_HEIGHT, 20);    
        ta.recycle();    
        mScroller = new Scroller(getContext());    
    }
    
    • 重写onMeasure,计算中间刻度
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int height=MeasureSpec.makeMeasureSpec(mRectHeight, MeasureSpec.AT_MOST);    
        super.onMeasure(widthMeasureSpec, height);        
        mScaleScrollViewRange = getMeasuredWidth();    
        mTempScale = mScaleScrollViewRange / mScaleMargin / 2 + mMin;    
        mMidCountScale = mScaleScrollViewRange / mScaleMargin / 2 + mMin;
    }
    
    • 重写onDraw,绘制刻度和指针
    protected void onDrawScale(Canvas canvas, Paint paint) {    
        paint.setTextSize(mRectHeight / 4);
        for (int i = 0, k = mMin; i <= mMax - mMin; i++) {
            if (i % 10 == 0) { 
                //整值
                canvas.drawLine(i * mScaleMargin, mRectHeight, i * mScaleMargin, mRectHeight - mScaleMaxHeight, paint); 
               //整值文字
                canvas.drawText(String.valueOf(k), i * mScaleMargin, mRectHeight - mScaleMaxHeight - 20, paint);
                k += 10;
            } else {
                canvas.drawLine(i * mScaleMargin, mRectHeight, i * mScaleMargin, mRectHeight - mScaleHeight, paint); 
           }
        }
    }
    
    protected void onDrawPointer(Canvas canvas, Paint paint) {
        paint.setColor(Color.RED);
        //每一屏幕刻度的个数/2
        int countScale = mScaleScrollViewRange / mScaleMargin / 2;
        //根据滑动的距离,计算指针的位置【指针始终位于屏幕中间】
        int finalX = mScroller.getFinalX();
        //滑动的刻度
        int tmpCountScale = (int) Math.rint((double) finalX / (double) mScaleMargin);//四舍五入取整
        //总刻度
        mCountScale = tmpCountScale + countScale + mMin;
        if (mScrollListener != null) { //回调方法
            mScrollListener.onScaleScroll(mCountScale);
        }
        canvas.drawLine(countScale * mScaleMargin + finalX, mRectHeight,
                countScale * mScaleMargin + finalX, mRectHeight - mScaleMaxHeight - mScaleHeight, paint);
    }
    
    • 处理滑动事件
      1. 在手指按下时,记录当前的x坐标(针对水平刻度尺)。
      • 在手指滑动过程中,判断当前指针所指的刻度是否已经超出了边界,如果超出,则禁止滑动,同时刷新当前界面。
      • 在手指抬起时,校正当前的刻度。
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (mScroller != null && !mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                mScrollLastX = x;
                return true;
            case MotionEvent.ACTION_MOVE:
                int dataX = mScrollLastX - x;
                if (mCountScale - mTempScale < 0) { //向右边滑动
                    if (mCountScale <= mMin && dataX <= 0) //禁止继续向右滑动
                        return super.onTouchEvent(event);
                } else if (mCountScale - mTempScale > 0) { //向左边滑动
                    if (mCountScale >= mMax && dataX >= 0) //禁止继续向左滑动
                        return super.onTouchEvent(event);
                }
                smoothScrollBy(dataX, 0);
                mScrollLastX = x;
                postInvalidate();
                mTempScale = mCountScale;
                return true;
            case MotionEvent.ACTION_UP:
                if (mCountScale < mMin) mCountScale = mMin;
                if (mCountScale > mMax) mCountScale = mMax;
                int finalX = (mCountScale - mMidCountScale) * mScaleMargin;
                mScroller.setFinalX(finalX); //纠正指针位置
                postInvalidate();
                return true;
        }
        return super.onTouchEvent(event);
    }
    

    最后的说明

    以上只是针对水平滑动刻度的实现,垂直滑动原理一致,在源码中已经实现,其中也有许多不够完善的地方,如:

    1. 第一次快速滑动时,可以超出边界,之后则不会;
    • 开放的自定义属性不够(根据具体情况);
    • 可以考虑将水平和垂直的实现,在一个类中完成,因为在实现过程中发现其实有很多代码都是类似的,只是个别参数属性的不同,在坐标系中,垂直可以看成是水平旋转了90°,之后有时间可以朝这个方向尝试下。

    相关文章

      网友评论

      • waiwaaa:我也写了个,支持定义显示范围及取值范围的,https://www.jianshu.com/p/ac8ae086ba7c
      • 558733765ca8:楼主,你这个可以双向实现吗?就是edittext输完可以动态设置这个刻度尺的值,滚动刻度尺又可以自动设置edittext的值
      • 蔡汉源:感谢您的分享
      • hello老文:非常感谢
      • juggzhang:为什么 第一次快速滑动可以超出边界?
      • ef0dd1e8ceb3:可以留下个联系方式吗,我有些问题想请教
      • b7e9763ffd86:做得真的挺棒的,确实应该放一张静态图,谢谢,又巩固了一下
        LichFaker:@白一辰 感谢, 下次会改进的
      • AlvinL:建议贴上效果图,可以让读者在阅读之前有个直观的印象
        LichFaker:@zhutoulwz 放了链接,效果图是gif的,这里不好展示

      本文标题:Android滚动刻度尺实现

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