Android 自定義View:實現一個 FM 刻度尺

效果圖

前言

最近在作收音機項目須要繪製一個 FM 刻度尺,剛開始考慮了一下現有的開源庫,後來發現都不太知足 UI 小哥哥的要求,因而決定本身畫一個吧。實現的 Demo 效果如上所示。主要包含大中小三種長度的刻度線,部分刻度整數值和一根指示器。這樣就完美實現了一個 FM 刻度尺。下面大體介紹一下具體的作法。只想看代碼的同窗能夠直奔 Github 地址git

開始繪製

我是經過繼承 View 重寫相關類來實現自定義 View的。最重要的就是實現三個相關方法:github

  • onMeasure():做用就是測量View須要多大的空間
  • onDraw():繪製各類形狀
  • onTouchEvent():觸摸事件的處理

重寫onMeasure()

重寫 onMeasure(),並調用父類 onMeasure()時:canvas

  • 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;
    }
複製代碼

說明ide

MeasureSpec.getSize()會解析 MeasureSpec 值獲得父容器 width 或者 height。spa

MeasureSpec.getMode()會獲得三個int類型的值分別爲:MeasureSpec.EXACTLY MeasureSpec.AT_MOST,MeasureSpec.UNSPECIFIED。rest

  • MeasureSpec.UNSPECIFIED 未指定,因此能夠設置任意大小。
  • MeasureSpec.AT_MOST:RulerView 能夠爲任意大小,可是有一個上限。
  • MeasureSpec.EXACTLY:父容器爲MeasureExampleView決定了一個大小,MeasureExampleView大小隻能在這個父容器限制的範圍以內。

重寫 onDraw()

首先咱們須要初始化畫筆code

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);
    }
複製代碼

開始繪製:orm

@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 對點擊和滑動事件作出響應。若是咱們須要在滑動時得到刻度尺對應的數值還須要定義相應對監聽接口。cdn

重寫 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 方法就能夠解決,代碼以下:blog

@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;
    }
複製代碼

這樣就實現了一個能夠滑動指示器、實時監聽刻度表數值、跳轉至特定數值的刻度尺。

總結

整個刻度尺的實現主要包括刻度線相關元素繪製和滑動事件處理。刻度線繪製看起來麻煩,實際只要理清思路,將對應位置的對應長度的線畫出來便可。這次提到的刻度尺可擴展性較差,須要的同窗能夠在次基礎上從新修改使用。

Github:github.com/gs666/Ruler… 歡迎你們提issue 和 star~

掘金主頁:juejin.im/user/5bffbd… 歡迎關注~

相關文章
相關標籤/搜索