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

Posted u012551350

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 自定义View:实现一个 FM 刻度尺相关的知识,希望对你有一定的参考价值。

本文作者: Rickonl

本文链接: 

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


文末有彩蛋

一、效果图



二、前言


最近在做收音机项目需要绘制一个 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联动效果

微信扫一扫识别小程序


长按识别小程序,参与抽奖


以上是关于Android 自定义View:实现一个 FM 刻度尺的主要内容,如果未能解决你的问题,请参考以下文章

android自定义View全解

Android - View之自定义View实现“刮刮卡”效果

Android进阶之绘制-自定义View完全掌握

Android自定义View之区块选择器

Android自定义View的三种实现方式

Android进阶之绘制-自定义View完全掌握