Android 自定义View:实现一个 FM 刻度尺

本文作者: Rickonl

本文链接: 

https://juejin.im/post/5d0afe1f51882508be27a504


文末有彩蛋

一、效果图

640?wx_fmt=other



二、前言


最近在做收音机项目需要绘制一个 FM 刻度尺,刚开始考虑了一下现有的开源库,后来发现都不太满足 UI 小哥哥的要求,于是决定自己画一个吧。实现的 Demo 效果如上所示。主要包含大中小三种长度的刻度线,部分刻度整数值和一根指示器。


这样就完美实现了一个 FM 刻度尺。下面大致介绍一下具体的做法。只想看代码的同学可以直奔:


https://github.com/gs666/RulerDemo


三、开始绘制

我是通过继承 View 重写相关类来实现自定义 View的。最重要的就是实现三个相关方法:

  • onMeasure():作用就是测量View需要多大的空间

  • onDraw():绘制各种形状

  • onTouchEvent():触摸事件的处理


四、重写onMeasure()


重写 onMeasure(),并调用父类 onMeasure()时:


  • RulerView 的 layout_width 以及 layout_height 属性值 match_parent 或者 wrap_content 显示大小由其父容器控件决定。

  • RulerView 设置为固定的值,就显示为该设定的值。

 
 
@Override	
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {	
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);	
        setMeasuredDimension(setMeasureWidth(widthMeasureSpec), setMeasureHeight(heightMeasureSpec));	
    }	
	
    private int setMeasureHeight(int spec) {	
        int mode = MeasureSpec.getMode(spec);	
        int size = MeasureSpec.getSize(spec);	
        int result = Integer.MAX_VALUE;	
        switch (mode) {	
            case MeasureSpec.AT_MOST:	
                size = Math.min(result, size);	
                break;	
            case MeasureSpec.EXACTLY:	
                break;	
            default:	
                size = result;	
                break;	
        }	
        return size;	
    }	
	
    private int setMeasureWidth(int spec) {	
        int mode = MeasureSpec.getMode(spec);	
        int size = MeasureSpec.getSize(spec);	
        int result = Integer.MAX_VALUE;	
        switch (mode) {	
            case MeasureSpec.AT_MOST:	
                size = Math.min(result, size);	
                break;	
            case MeasureSpec.EXACTLY:	
                break;	
            default:	
                size = result;	
                break;	
        }	
        return size;	
    }

说明


MeasureSpec.getSize()会解析 MeasureSpec 值得到父容器 width 或者 height。


MeasureSpec.getMode()会得到三个int类型的值分别为:MeasureSpec.EXACTLY,AT_MOST,UNSPECIFIED。


  • MeasureSpec.UNSPECIFIED 未指定,所以可以设置任意大小。

  • MeasureSpec.AT_MOST:RulerView 可以为任意大小,但是有一个上限。

  • MeasureSpec.EXACTLY:父容器为MeasureExampleView决定了一个大小,MeasureExampleView大小只能在这个父容器限制的范围之内。


五、重写 onDraw()


首先我们需要初始化画笔:


 
 
private Paint mLinePaint;//刻度线画笔	
private Paint mTextPaint;//指示数字画笔	
private Paint mRulerPaint;//指示线画笔	
	
private void init() {	
        mLinePaint = new Paint();	
        mLinePaint.setColor(getResources().getColor(R.color.grey));	
        //抗锯齿	
        mLinePaint.setAntiAlias(true);	
        mLinePaint.setStyle(Paint.Style.STROKE);	
        mLinePaint.setStrokeWidth(1);	
	
        mTextPaint = new Paint();	
        mTextPaint.setColor(getResources().getColor(R.color.grey));	
        mTextPaint.setAntiAlias(true);	
        mTextPaint.setStyle(Paint.Style.FILL);	
        mTextPaint.setStrokeWidth(2);	
        mTextPaint.setTextSize(24);	
	
        mRulerPaint = new Paint();	
        mRulerPaint.setAntiAlias(true);	
        mRulerPaint.setStyle(Paint.Style.FILL_AND_STROKE);	
        mRulerPaint.setColor(getResources().getColor(R.color.ruler_line));	
        mRulerPaint.setStrokeWidth(3);	
    }

开始绘制:

 
 
  @Override	
    protected void onDraw(Canvas canvas) {	
        super.onDraw(canvas);	
        canvas.save();	
        //绘制刻度线	
        for (int i = min; i <= max; i++) {	
            if (i % 10 == 0) {	
                canvas.drawLine(20, 0, 20, 140, mLinePaint);	
	
                String text = i / 10 + "";	
                Rect rect = new Rect();	
                float txtWidth = mTextPaint.measureText(text);	
                mTextPaint.getTextBounds(text, 0, text.length(), rect);	
                if (i / 10 % 2 == 1 && i / 10 != 107) {	
                    canvas.drawText(text, 20 - txtWidth / 2, 72 + rect.height() + 74, mTextPaint);	
                }	
                if (i / 10 == 108) {	
                    canvas.drawText(text, 20 - txtWidth / 2, 72 + rect.height() + 74, mTextPaint);	
                }	
            } else if (i % 5 == 0) {	
                canvas.drawLine(20, 30, 20, 110, mLinePaint);	
            } else {	
                canvas.drawLine(20, 54, 20, 86, mLinePaint);	
            }	
            canvas.translate((float) 8, 0);	
        }	
        canvas.restore();	
	
        //绘制指示线	
        canvas.drawLine(position, 0, position, 140, mRulerPaint);	
        mTextPaint.setTextSize(24);	
    }

上面的代码分别画出了三种长度不同的刻度线、刻度数字和指示器的线。就这样我们完成了刻度尺的绘制。但是只有一个刻度尺是不够的,我们还需要重写 onTouchEvent 对点击和滑动事件做出响应。如果我们需要在滑动时获得刻度尺对应的数值还需要定义相应对监听接口。


六、重写 onTouchEvent()

 
 
	
@Override	
    public boolean onTouchEvent(MotionEvent event) {	
        switch (event.getAction()) {	
            case MotionEvent.ACTION_DOWN:	
                break;	
            case MotionEvent.ACTION_MOVE:	
                float x = event.getX();	
                if (x < MIN_POSITION) {	
                    setPosition(MIN_POSITION);	
                } else if (x > MAX_POSITION) {	
                    setPosition(MAX_POSITION);	
                } else {	
                    setPosition((int) x);	
                }	
                //移动指示条	
                if (mMove != null) {	
                    mMove.onMove(Double.parseDouble(String.format("%.1f", getFmChannel())));	
                }	
                Log.d("TAG", "position:" + position);	
                Log.d("TAG", "channel:" + getFmChannel());	
            case MotionEvent.ACTION_CANCEL:	
                //只停在0.1(刻度线上)的位置	
                setFmChanel(Double.parseDouble(String.format("%.1f", getFmChannel())));	
                Log.d("停下来后", "channel:" + Double.parseDouble(String.format("%.1f", getFmChannel())));	
                break;	
            default:	
        }	
        return true;	
    }	
    	
    public void setPosition(int i) {	
        position = i;	
        invalidate();	
    }	
	
    public void setFmChanel(double fmChanel) {	
        int temp = (int) ((fmChanel - 87) * 80) + 20;	
        setPosition(temp);	
    }	
	
    public double getFmChannel() {	
        return ((position - 20.0) / 80.0 + 87.0);	
    }

这样我们对刻度尺就是一个可以滑动指示器的刻度尺了。我在 ViewPager 中使用这个刻度尺的过程中遇到了一个问题:无法顺利滑动刻度尺了。这是因为和父控件滑动事件冲突,只需要重写 dispatchTouchEvent 方法就可以解决,代码如下:

 
 
   @Override	
    public boolean dispatchTouchEvent(MotionEvent ev) {	
        //解决刻度尺和viewPager的滑动冲突	
        //当滑动刻度尺时,告知父控件不要拦截事件,交给子view处理	
        getParent().requestDisallowInterceptTouchEvent(true);	
        return super.dispatchTouchEvent(ev);	
    }

如果我们需要实时监听刻度尺滑动时的值就需要设置相应监听接口。代码如下:

 
 
  /**	
     * 定义监听接口	
     */	
    public interface OnMoveActionListener {	
        void onMove(double x);	
    }	
	
    /**	
     * 为每个接口设置监听器	
     */	
    public void setOnMoveActionListener(OnMoveActionListener move) {	
        mMove = move;	
    }

这样就实现了一个可以滑动指示器、实时监听刻度表数值、跳转至特定数值的刻度尺。


七、总结


整个刻度尺的实现主要包括刻度线相关元素绘制和滑动事件处理。刻度线绘制看起来麻烦,实际只要理清思路,将对应位置的对应长度的线画出来即可。此次提到的刻度尺可扩展性较差,需要的同学可以在次基础上重新修改使用。


推荐阅读

RecyclerView 性能优化

分享一套Android快速开发模板,包含常用主流框架,下载即用

简历上的哪些内容才是 HR 眼中的干货?

第一站小红书图片裁剪控件之二,自定义CoordinatorLayout联动效果

微信扫一扫识别小程序


640?wx_fmt=png长按识别小程序,参与抽奖

640?wx_fmt=png


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值