Android自定义View(11)- 画一个FM调频收音机刻度表

概述

这次我们来画一个调频收音机刻度表。这个控件看似并不复杂,但却涉及到蛮多的细节处理,需要对Android坐标系有相当的理解。这次功能的实现会用到 scrollTo()、scrollBy()等方法,还会用到 VelocityTracker及 Scroller实现速度追踪及惯性滑动。我们先看看效果:


Screenrecorder-2021-08-04-18-15-16-2852021841831302.gif

下面我们将这个功能拆解,然后分步来实现:

  • 测量控件宽高,画出图中黑色部分所有刻度线及刻度值
  • 监听 onTouch事件,实现刻度左右滑动
  • 画出中间绿色三角形指针及当前选中的频段值及单位
  • 实现惯性滑动
  • 优化对外接口
1、测量宽高,画出所有刻度线及刻度值

在开始画内容之前我们要先厘清一个问题点。就是我们在布局中放的控件的尺寸是有限的,但控件里的内容尺寸可以是无限。举个例子,比如我给一个TextView定的宽度是 10dp,但是我给TextView设置的字符串内容长度却有 20dp。那么超出控件之外的 10dp内容是不是就不存在了?当然不是,虽然超出部分当前不显示在控件 10dp范围内,但是它真是存在。也就是说,控件的尺寸和控件内容尺寸没有半毛钱关系。而且我们可以使用 View的 scrollTo()、scrollBy()等方法将控件内的内容进行滑动,让超出控件尺寸范围的内容滑动到尺寸范围内。下面草图大概就是这么个意思:

Text.png

好了,不多说。我们先简单测量一下尺寸,再把刻度和刻度值画出来。

   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        if (heightMode == MeasureSpec.AT_MOST) {
            setMeasuredDimension(widthSize, (int) dipToPx(120));
            return;
        }
        setMeasuredDimension(widthSize, heightSize);
    }

刚才说了,控件尺寸和内容尺寸没啥关系。所以这里我们就简单处理下尺寸,高度是 AT_MOST给它指定默认值。宽度就自便了,AT_MOST模式下不指定默认值的话,会跟随父布局的尺寸。下面开始画刻度及刻度值:

    // 每刻度间隔
    private static int defaultMark = 6;
    // 总刻度数
    private static int markCount = 210;
    // 短刻度线长度
    private static int shortLineLength = 16;
    // 长刻度线长度
    private static int longLineLength = 32;
    // 所有刻度总长度(+2,前后各留一个间隙)
    private static int contentTotalLength;

 protected void onDraw(Canvas canvas) {
        // 画底部直线
        float baseStartX = dipToPx(defaultMark);
        float baseStartY = getHeight() - dipToPx(defaultMark);
        float baseEndX = dipToPx(contentTotalLength - defaultMark);
        canvas.drawLine(baseStartX, baseStartY, baseEndX, baseStartY, linePaint);
        // 画所有刻度线
        for (int i = 0; i <= markCount; i++) {
            float markStartX = dipToPx(defaultMark * (i + 1));
            float markStarY = getHeight() - dipToPx(defaultMark);
            float markEndX = dipToPx(defaultMark * (i + 1));
            float markEndY;
            // 每隔 10个小刻度画一条长刻度线
            if (i % 10 == 0) {
                markEndY = getHeight() - dipToPx(defaultMark + longLineLength);// 长刻度     
                // 画刻度值
                drawNumbers(canvas, i);
            } else {
                markEndY = getHeight() - dipToPx(defaultMark + shortLineLength); // 短刻度线

            }
            // 开始绘制刻度线
            canvas.drawLine(markStartX, markStarY, markEndX, markEndY, linePaint); 
        }
    ......
    }

上面方法就是从左到右,将所有刻度线绘制出来。每个10个刻度会画一条长刻度线,并且将长刻度线的刻度值画出来。下面我们来画刻度值 drawNumbers(canvas, i):

  /**
     * 画刻度值
     *
     * @param canvas
     * @param number 长刻度线的位置
     */
    private void drawNumbers(Canvas canvas, int number) {
        // 取刻度值,调频FM是0.1MHZ每个刻度,频段范围是 87~108
        String text = String.valueOf(number / 10 + 87);
        // 获取数字的尺寸
        Rect textRect = getTextRect(numberPaint, text);
        float textWidth = textRect.width();
        float textHeight = textRect.height();
        numberPaint.setFakeBoldText(false);
        numberPaint.setTextSize(dipToPx(22));
        numberPaint.setColor(markLineColor);
        // 开始绘制长刻度线的刻度值
        canvas.drawText(text, dipToPx(defaultMark * (number + 1)) - textWidth / 2,
                getHeight() - dipToPx(defaultMark + longLineLength) - textHeight / 2, numberPaint);
    }

上面方法就是计算出刻度线对应的刻度值,然后绘制在长刻度线之上。一些参数上面也加了注释,我们也可以添加自定义属性集。关于文本绘制的坐标问题可以参考我之前的一篇文章:文字绘制

2、监听onTouch事件,实现内容左右滑动

上面已经说到了,控件的尺寸和内容尺寸没半毛钱关系。屏幕的大小是有限的,而我们这里画出的所有刻度大概率没办法在控件当中全部显示。所以我们就要实现内容滑动,让所有刻度线都有机会显示在控件尺寸范围内:


    /**
     * 开始处理滑动事件
     *
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastX = event.getX();
                break;
            case MotionEvent.ACTION_MOVE:
                int deltaX = (int) (lastX - event.getX());
                Log.d(TAG, "deltaX = " + deltaX + "--getScrollX() = " + getScrollX());
                // 注释 1 限制左边界
                if ((getScrollX() <= leftBorder) && (deltaX < 0)) {
                    scrollTo(leftBorder, 0);
                    break;
                }
                // 注释 2 限制有边界
                if ((getScrollX() >= rightBorder) && (deltaX > 0)) {
                    scrollTo(rightBorder, 0);
                    break;
                }
                // 注释 3 界内滑动
                scrollBy(deltaX, 0);
                lastX = event.getX();
                break;
            case MotionEvent.ACTION_UP:
                break;
        }
        return true;
    }

好了,我们看到上面注释的地方,用到了scrollTo和scrollBy方法。这两个方法是View本身提供给我们的用于内容平移的方法,只能平移内容,并不能将 View本身平移。这两个方法是刻度内容滑动的核心,下面简单介绍一下吧:

  • scrollTo :内容的绝对滑动,按照参数值将内容平移到指定位置。在 X方向上传负值内容将向右滑动(X轴正方向),传正值内容将向左滑动(X轴负方向)。Y轴方向也是同理。
  • scrollBy:实现内容的相对滑动,先获取当前已滑动的偏移量,最终调用 scrollTo方法。参数是滑动偏移量,参数的正负与滑动方向的关系和scrollTo的逻辑相同。

好了,上面看注释 1、注释 2处,我们给刻度尺设置了左边界和右边界。左右边界的值是根据总刻度数和每个刻度的长度计算出来的。当发现滑动偏移量超出左边界或右边界的时候,就用scrollTo方法分别滑动到左边界或右边界。在界内滑动时则用 scrollBy平移。

3、画中间绿色指针及当前频段值

我们知道,当调用 scrollTo或 scrollBy方法实现滑动时,控件里画的所有内容都会跟着一起滑动。但是我们的指针和当前刻度值不能动啊,不然就没法玩了。这里要解决这个问题就要实时获取滑动的当前偏移量。使用到了这个方法:getScrollX()。下面看一下实现:

  protected void onDraw(Canvas canvas) {
  ......
   // 画中心三角形指针
        Path guidePath = getGuidePath();
        canvas.drawPath(guidePath, guideLinePaint);
        // 画当前刻度值
        drawCurrentNumber(canvas);
    }

/**
     * @return 三角形指针
     */
    private Path getGuidePath() {
        if (guidePath == null) guidePath = new Path();
        guidePath.reset();
        //注释 4, 滑动过程中,X轴的中心位置取宽度的一半加上累计偏移量 getScrollX()
        float centerX = getWidth() >> 1 + getScrollX();
        float bottomY = getHeight() - dipToPx(defaultMark);
        float topY = getHeight() - dipToPx(defaultMark + longLineLength * 2);

        guidePath.moveTo(centerX, bottomY);
        guidePath.lineTo(centerX - dipToPx(3), topY);
        guidePath.lineTo(centerX + dipToPx(3), topY);
        guidePath.close();
        return guidePath;
    }

看上面方法,我们用一个闭合的路径 Path画三角形指针。因为指针要显示在 View的中心位置不动,所以这里中心位置我们不能只取View宽度的一半. 还要加上相对于初始状态的累计偏移量。上面注释 4处 getScrollX()也是View提供的获取累计偏移量的方法,在滑动过程中它的返回值是会不断变化。同样的,我们知道怎么让指针在滑动过程中位置不动,那指针上面的当前刻度值也同理实现:

/**
     * 画当前刻度值
     *
     * @param canvas
     */
    private void drawCurrentNumber(Canvas canvas) {
        // 注释 5,通过当前的累计滑动偏移量,计算出水平方向中心位置的坐标
        float centerX = getWidth() / 2 + getScrollX();
        float guideLineTopY = getHeight() - dipToPx(defaultMark + longLineLength * 2);
        //注释 6,当前指针指向的刻度
        int currentMaks = calculateCurrentMarks(null);
        //注释 7, 从 87MHZ开始,0.1每刻度
        double contentNum = 87 + currentMaks * 1.0 / 10;
        String currentNumber = numFormat.format(contentNum);

        Rect textRect = getTextRect(numberPaint, currentNumber);
        float textWidth = textRect.width();
        float textHeight = textRect.height();

        int x = (int) (centerX - textWidth / 2);
        int baseY = (int) (guideLineTopY - textHeight * 3 / 4);

        numberPaint.setFakeBoldText(true);
        numberPaint.setTextSize(dipToPx(25));
        numberPaint.setColor(guideLineColor);
        canvas.drawText(currentNumber, x, baseY, numberPaint);

        numberPaint.setTextSize(dipToPx(12));
        int unitX = (int) (centerX + textWidth * 3 / 4);
        canvas.drawText("MHZ", unitX, baseY, numberPaint);
        // 回调当前FM
        onFMChange();
    }

上面注释 6处要根据当前滑动的偏移量计算出指针指向的刻度,我们来实现这个方法:

    /**
     * 计算当前刻度
     *
     * @param event
     * @return
     */
    private int calculateCurrentMarks(MotionEvent event) {
        float guideLineX = getWidth() / 2;
        // 注释 8,根据累计偏移量,算出指针当前指向的位置.
        float contentX = getScrollX() + guideLineX - dipToPx(defaultMark);
        int marks = (int) (contentX / dipToPx(defaultMark));
        if (contentX % dipToPx(defaultMark) > dipToPx(defaultMark / 2)) {
            marks += 1;
            //  ACTION_UP的话指针取整
            if ((event != null) && (event.getAction() == MotionEvent.ACTION_UP)) {
                scrollBy((int) (dipToPx(defaultMark) - (contentX % dipToPx(defaultMark))), 0); //五入,ACTION_UP时跳到刻度线
            }
        } else {
            if ((event != null) && (event.getAction() == MotionEvent.ACTION_UP)) {
                scrollBy((int) (-contentX % dipToPx(defaultMark)), 0); // 四舍,ACTION_UP时跳到刻度线
            }
        }
        Log.d(TAG, "marks = " + marks);
        return marks;
    }

上面注释 8,又是通过累计偏移量 getScrollX()来计算当前变化的参数.所以变与不变都是我们都是用它来实现.这里如果是 ACTION_UP事件,手指离开屏幕时,如果指针在两刻度线之间,那我们就四舍五入取整。

4、实现惯性滑动

我们上面的内容已经实现平移滑动了,但是体验还要优化一下。实现手指离开屏幕后惯性滑动一段距离。那么接下来我们将用到两个类:VelocityTracker和Scroller。VelocityTracker是用来检测手指滑动速度的,而Scroller是用于在一段时间内生成一组数据,我们动态地拿到数据就可以将内容连续平滑移动。这类似于属性动画。

下面我们先看速度追踪器 VelocityTracker的用法:

private VelocityTracker mVelocityTracker = VelocityTracker.obtain();

 public boolean onTouchEvent(MotionEvent event) {
        // 注释 9, 向追踪器添加 onTouch事件
        mVelocityTracker.addMovement(event);

......
}

  /**
     * 计算滑动速度
     */
    private void computeVelocity() {
       //注释 10, 计算 1秒内的速度
        mVelocityTracker.computeCurrentVelocity(1000);
        float velocityX = mVelocityTracker.getXVelocity();
        Log.d(TAG, "velocityX = " + velocityX);
    }

上面注释 9,我们先把onTouch事件添加进追踪器。然后在注释10处给追踪器设置时间,然后获取速度值。

下面我们来看结合 Scroller实现惯性滑动:

private Scroller mScroller = new Scroller(getContext());

 /**
     * 计算滑动速度
     */
private void computeVelocity() {
        mVelocityTracker.computeCurrentVelocity(1000);
        float velocityX = mVelocityTracker.getXVelocity();
        Log.d(TAG, "velocityX = " + velocityX);
        // 初始化 Scroller
        setFling((int) velocityX);
    }

private void setFling(int vx) {
        fling(getScrollX(), 0, -vx, 0, leftBorder, rightBorder, 0, 0);
    }

 /**
     * @param startX    起始 X
     * @param startY    起始 Y
     * @param velocityX X 方向速度
     * @param velocityY Y 方向速度
     * @param minX      左边界
     * @param maxX      右边界
     * @param minY      上边界
     * @param maxY      下边界
     */
    private void fling(int startX, int startY, int velocityX, int velocityY,
                       int minX, int maxX, int minY, int maxY) {
        if (mScroller == null) return;
        // 注释 11,Scroller.fling方法启动平移计算
        mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
    }

   /**
     * Scroll 回调
     */
    @Override
    public void computeScroll() {
        Log.d(TAG, "computeScroll = " + mScroller.isFinished());
        if (mScroller == null) return;
        if (mScroller.computeScrollOffset()) {
            // 注释 12,动态获取Scroller变化的值,用scrollTo实现平滑移动
            scrollTo(mScroller.getCurrX(), 0);
        } else {
            MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0);
            calculateCurrentMarks(event);
        }
    }

上面注释 11,我们使用了 Scroller的 fling方法来初始化滑动事件,该方法可以将刚才我们用速度追踪器获取到的手指离开屏幕时的速度作为参数传入。相关参数解释在上面方法注释处。Scroller还有另外一个方法可以实现平移:startScroll。但参数不一样。

上面在注释 12的地方,View每次滑动都会回调 computeScroll这个方法。所以我们在该方法里动态获取 Scroller当前改变的值,然后用 scrollTo实现手指离开屏幕后的滑动。这是不是有点像属性动画?

5、优化对外接口

最后我们来完善一下对外接口,主要实现以下几个点:

  • 设置刻度、刻度值、指针以及当前刻度值得颜色
  • 设置 FM频段
  • 获取当前 FM频段
  • 频段变化监听回调

设置颜色:

   /**
     * 刻度及刻度值颜色
     *
     * @param color
     */
    public void setMarkLineColor(@ColorRes int color){
        this.markLineColor = getResources().getColor(color);
        if (linePaint != null)
        linePaint.setColor(markLineColor);
    }

    /**
     * 指针及当前刻度值颜色
     *
     * @param color
     */
    public void setGuideLineColor(@ColorRes int color){
        this.guideLineColor = getResources().getColor(color);
        if (guideLinePaint != null)
        guideLinePaint.setColor(guideLineColor);
    }

设置频段:

   /**
     * 设置频段
     *
     * @param mHZ 频段
     */
    public void setFM(float mHZ){
        if ((mHZ < 87) || (mHZ > 108)) return;
        double destMarks = (mHZ - 87) * 10;
        int currentMaks = calculateCurrentMarks(null);
        Log.d(TAG, "destMarks = " + destMarks + "--currentMaks = " + currentMaks);
        scrollBy((int) ((destMarks - currentMaks) * dipToPx(defaultMark)), 0);
    }

获取当前频段:

  /**
     * 获取当前频段
     *
     * @return 当前频段
     */
    public double getFM(){
        int currentMaks = calculateCurrentMarks(null);
        double currentFM = 87 + currentMaks * 1.0 / 10;
        Log.d(TAG, "currentFM = " + currentFM);
        return currentFM;
    }

频段变化监听回调:

 public interface OnFMChangeListener{
        void onChang(double currentFM);
    }

    public void setOnFMChangeListener(OnFMChangeListener onFMChangeListener){
        this.mOnFMChangeListener = onFMChangeListener;
    }
    /**
     * 回调当前FM
     */
    private void onFMChange(){
        if (this.mOnFMChangeListener == null) return;
        this.mOnFMChangeListener.onChang(getFM());
    }

最后,刻度表完整代码:

/**
* FM刻度尺
* 
* EthanLee
*/
public class FMMarkView extends View {
   private static final String TAG = "FMMarkView";
   // 画刻度线
   private Paint linePaint;
   // 画中间指示线
   private Paint guideLinePaint;
   // 画刻度值
   private Paint numberPaint;
   // 每刻度间隔
   private static int defaultMark = 6;
   // 总刻度数
   private static int markCount = 210;
   // 短刻度线长度
   private static int shortLineLength = 16;
   // 长刻度线长度
   private static int longLineLength = 32;
   // 所有刻度总长度(+2,前后各留一个间隙)
   private static int contentTotalLength;
   // 刻度值保留一位小数
   private DecimalFormat numFormat = new DecimalFormat("0.0");
   // 上一次滑动事件x值
   private float lastX;
   // 内容滑动的左边界
   private int leftBorder = 0;
   // 内容滑动的右边界
   private int rightBorder = 0;
   // 中心三角形指针
   private Path guidePath;
   // 刻度线及刻度值颜色
   private int markLineColor = Color.parseColor("#FF000000");
   // 三角形指针颜色
   private int guideLineColor = Color.parseColor("#FFF10404");

   private OnFMChangeListener mOnFMChangeListener;

   private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
   private Scroller mScroller = new Scroller(getContext());

   public FMMarkView(Context context) {
       this(context, null);
   }

   public FMMarkView(Context context, @Nullable AttributeSet attrs) {
       this(context, attrs, 0);
   }

   public FMMarkView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
       super(context, attrs, defStyleAttr);
       init(context, attrs, defStyleAttr);
   }

   private void init(Context context, AttributeSet attrs, int defStyleAttr) {
       linePaint = new Paint();
       linePaint.setColor(markLineColor);
       linePaint.setAntiAlias(true);
       linePaint.setDither(true);
       linePaint.setStrokeWidth(3);

       guideLinePaint = new Paint();
       guideLinePaint.setColor(guideLineColor);
       guideLinePaint.setAntiAlias(true);
       guideLinePaint.setDither(true);
       guideLinePaint.setStrokeWidth(3);

       numberPaint = new Paint();
       numberPaint.setColor(markLineColor);
       numberPaint.setAntiAlias(true);
       numberPaint.setDither(true);
       numberPaint.setTextSize(dipToPx(22));
       numberPaint.setStrokeWidth(1);

       contentTotalLength = (markCount + 2) * defaultMark;
   }

   /**
    * 刻度及刻度值颜色
    *
    * @param color
    */
   public void setMarkLineColor(@ColorRes int color) {
       this.markLineColor = getResources().getColor(color);
       if (linePaint != null)
           linePaint.setColor(markLineColor);
   }

   /**
    * 指针及当前刻度值颜色
    *
    * @param color
    */
   public void setGuideLineColor(@ColorRes int color) {
       this.guideLineColor = getResources().getColor(color);
       if (guideLinePaint != null)
           guideLinePaint.setColor(guideLineColor);
   }

   /**
    * 设置频段
    *
    * @param mHZ 频段
    */
   public void setFM(float mHZ) {
       if ((mHZ < 87) || (mHZ > 108)) return;
       double destMarks = (mHZ - 87) * 10;
       int currentMaks = calculateCurrentMarks(null);
       Log.d(TAG, "destMarks = " + destMarks + "--currentMaks = " + currentMaks);
       scrollBy((int) ((destMarks - currentMaks) * dipToPx(defaultMark)), 0);
   }

   /**
    * 获取当前频段
    *
    * @return 当前频段
    */
   public double getFM() {
       int currentMaks = calculateCurrentMarks(null);
       double currentFM = 87 + currentMaks * 1.0 / 10;
       Log.d(TAG, "currentFM = " + currentFM);
       return currentFM;
   }

   public interface OnFMChangeListener {
       void onChang(double currentFM);
   }

   public void setOnFMChangeListener(OnFMChangeListener onFMChangeListener) {
       this.mOnFMChangeListener = onFMChangeListener;
   }

   /**
    * 回调当前FM
    */
   private void onFMChange() {
       if (this.mOnFMChangeListener == null) return;
       this.mOnFMChangeListener.onChang(getFM());
   }

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);
       int widthMode = MeasureSpec.getMode(widthMeasureSpec);
       int widthSize = MeasureSpec.getSize(widthMeasureSpec);
       int heightMode = MeasureSpec.getMode(heightMeasureSpec);
       int heightSize = MeasureSpec.getSize(heightMeasureSpec);

       if (heightMode == MeasureSpec.AT_MOST) {
           setMeasuredDimension(widthSize, (int) dipToPx(120));
           return;
       }
       setMeasuredDimension(widthSize, heightSize);
   }

   @Override
   protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
       super.onLayout(changed, left, top, right, bottom);
       // 初始化内容滑动的左右边界
       getLeftBorder();
       getRightBorder();
   }

   @Override
   protected void onDraw(Canvas canvas) {
       // 画底部直线
       float baseStartX = dipToPx(defaultMark);
       float baseStartY = getHeight() - dipToPx(defaultMark);
       float baseEndX = dipToPx(contentTotalLength - defaultMark);
       canvas.drawLine(baseStartX, baseStartY, baseEndX, baseStartY, linePaint);
       // 画所有刻度线
       for (int i = 0; i <= markCount; i++) {
           float markStartX = dipToPx(defaultMark * (i + 1));
           float markStarY = getHeight() - dipToPx(defaultMark);
           float markEndX = dipToPx(defaultMark * (i + 1));
           float markEndY;
           if (i % 10 == 0) {
               markEndY = getHeight() - dipToPx(defaultMark + longLineLength);// 长刻度
               drawNumbers(canvas, i);
           } else {
               markEndY = getHeight() - dipToPx(defaultMark + shortLineLength);
           }
           canvas.drawLine(markStartX, markStarY, markEndX, markEndY, linePaint);
       }
       // 画中心三角形指针
       Path guidePath = getGuidePath();
       canvas.drawPath(guidePath, guideLinePaint);
       // 画当前刻度值
       drawCurrentNumber(canvas);
   }

   /**
    * @return 三角形指针
    */
   private Path getGuidePath() {
       if (guidePath == null) guidePath = new Path();
       guidePath.reset();
       float centerX = getWidth() / 2 + getScrollX();
       float bottomY = getHeight() - dipToPx(defaultMark);
       float topY = getHeight() - dipToPx(defaultMark + longLineLength * 2);

       guidePath.moveTo(centerX, bottomY);
       guidePath.lineTo(centerX - dipToPx(3), topY);
       guidePath.lineTo(centerX + dipToPx(3), topY);
       guidePath.close();
       return guidePath;
   }

   /**
    * 画当前刻度值
    *
    * @param canvas
    */
   private void drawCurrentNumber(Canvas canvas) {
       float centerX = getWidth() / 2 + getScrollX();
       float guideLineTopY = getHeight() - dipToPx(defaultMark + longLineLength * 2);

       int currentMaks = calculateCurrentMarks(null);
       // 从 87MHZ开始,0.1每刻度
       double contentNum = 87 + currentMaks * 1.0 / 10;
       String currentNumber = numFormat.format(contentNum);

       Log.d(TAG, "currentNumber = " + currentNumber + "--" + currentMaks + "--contentNum = " + contentNum);
       Rect textRect = getTextRect(numberPaint, currentNumber);
       float textWidth = textRect.width();
       float textHeight = textRect.height();

       int x = (int) (centerX - textWidth / 2);
       int baseY = (int) (guideLineTopY - textHeight * 3 / 4);

       numberPaint.setFakeBoldText(true);
       numberPaint.setTextSize(dipToPx(25));
       numberPaint.setColor(guideLineColor);
       canvas.drawText(currentNumber, x, baseY, numberPaint);

       numberPaint.setTextSize(dipToPx(12));
       int unitX = (int) (centerX + textWidth * 3 / 4);
       canvas.drawText("MHZ", unitX, baseY, numberPaint);
       // 回调当前FM
       onFMChange();
   }

   /**
    * 画刻度值
    *
    * @param canvas
    * @param number 长刻度线的位置
    */
   private void drawNumbers(Canvas canvas, int number) {
       String text = String.valueOf(number / 10 + 87);
       Rect textRect = getTextRect(numberPaint, text);
       float textWidth = textRect.width();
       float textHeight = textRect.height();
       numberPaint.setFakeBoldText(false);
       numberPaint.setTextSize(dipToPx(22));
       numberPaint.setColor(markLineColor);
       canvas.drawText(text, dipToPx(defaultMark * (number + 1)) - textWidth / 2,
               getHeight() - dipToPx(defaultMark + longLineLength) - textHeight / 2, numberPaint);
   }

   private Rect getTextRect(Paint textPaint, String text) {
       Rect rect = new Rect();
       textPaint.getTextBounds(text, 0, text.length(), rect);
       return rect;
   }

   private int getLeftBorder() {
       leftBorder = (int) (dipToPx(defaultMark) - getWidth() / 2);
       return leftBorder;
   }

   private int getRightBorder() {
       rightBorder = (int) (dipToPx((markCount + 1) * defaultMark) - getWidth() / 2);
       return rightBorder;
   }

   /**
    * 开始处理滑动事件
    *
    * @param event
    * @return
    */
   @Override
   public boolean onTouchEvent(MotionEvent event) {
       mVelocityTracker.addMovement(event);
       switch (event.getAction()) {
           case MotionEvent.ACTION_DOWN:
               lastX = event.getX();
               break;
           case MotionEvent.ACTION_MOVE:
               int deltaX = (int) (lastX - event.getX());
               Log.d(TAG, "deltaX = " + deltaX + "--getScrollX() = " + getScrollX());
               // 限制左边界
               if ((getScrollX() <= leftBorder) && (deltaX < 0)) {
                   scrollTo(leftBorder, 0);
                   break;
               }
               // 限制有边界
               if ((getScrollX() >= rightBorder) && (deltaX > 0)) {
                   scrollTo(rightBorder, 0);
                   break;
               }
               // 界内滑动
               scrollBy(deltaX, 0);
               lastX = event.getX();
               break;
           case MotionEvent.ACTION_UP:
               // 计算滑动速度
               computeVelocity();
               break;
       }
       calculateCurrentMarks(event);
       return true;
   }

   /**
    * 计算滑动速度
    */
   private void computeVelocity() {
       mVelocityTracker.computeCurrentVelocity(1000);
       float velocityX = mVelocityTracker.getXVelocity();
       Log.d(TAG, "velocityX = " + velocityX);
       // 初始化 Scroller
       setFling((int) velocityX);
   }

   private void setFling(int vx) {
       fling(getScrollX(), 0, -vx, 0, leftBorder, rightBorder, 0, 0);
   }

   /**
    * @param startX    起始 X
    * @param startY    起始 Y
    * @param velocityX X 方向速度
    * @param velocityY Y 方向速度
    * @param minX      左边界
    * @param maxX      右边界
    * @param minY      上边界
    * @param maxY      下边界
    */
   private void fling(int startX, int startY, int velocityX, int velocityY,
                      int minX, int maxX, int minY, int maxY) {
       if (mScroller == null) return;
       mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
   }

   /**
    * Scroll 回调
    */
   @Override
   public void computeScroll() {
       Log.d(TAG, "computeScroll = " + mScroller.isFinished());
       if (mScroller == null) return;
       if (mScroller.computeScrollOffset()) {
           scrollTo(mScroller.getCurrX(), 0);
       } else {
           MotionEvent event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_UP, 0, 0, 0);
           calculateCurrentMarks(event);
       }
   }

   /**
    * 计算当前刻度
    *
    * @param event
    * @return
    */
   private int calculateCurrentMarks(MotionEvent event) {
       float guideLineX = getWidth() / 2;
       float contentX = getScrollX() + guideLineX - dipToPx(defaultMark);
       int marks = (int) (contentX / dipToPx(defaultMark));
       if (contentX % dipToPx(defaultMark) > dipToPx(defaultMark / 2)) {
           marks += 1;
           if ((event != null) && (event.getAction() == MotionEvent.ACTION_UP)) {
               scrollBy((int) (dipToPx(defaultMark) - (contentX % dipToPx(defaultMark))), 0); //五入,ACTION_UP时跳到刻度线
           }
       } else {
           if ((event != null) && (event.getAction() == MotionEvent.ACTION_UP)) {
               scrollBy((int) (-contentX % dipToPx(defaultMark)), 0); // 四舍,ACTION_UP时跳到刻度线
           }
       }
       Log.d(TAG, "marks = " + marks);
       return marks;
   }

   private float dipToPx(int dip) {
       return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip, getResources().getDisplayMetrics());
   }
}

你可能感兴趣的:(Android自定义View(11)- 画一个FM调频收音机刻度表)