自定义收音机搜台控件——RadioRulerView

本文介绍了一款自定义的FM搜台尺子控件,详细讲解了其实现原理,包括如何计算刻度线的位置以确保尺子居中显示,并提供了完整的源代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言:像这类的自定义控件有非常多的开源项目,但还是没有找到我项目想要的,所以简单实现了一个,下面简单讲讲实现原理。

效果图:

image

实现思路:

  • 首先画固定背景尺子,而实现这个则要计算刻度线的宽度、刻度线间的距离,以及要确定刻度线的总是,根据这些可以求出第一条刻度线的x坐标,使得整个尺子居中;下图为尺子尺寸的计算方法:

image

贴上关键代码:

  /**
   * 画固定的尺子
   * @param canvas
   */
  private void drawLine(Canvas canvas) {
      canvas.save();
      int height = mHeight;
      int drawCount = 0;//已经画了刻度线的个数
      float xPosition;
      for(int i=0; drawCount<=mMaxLineCount; i++){
          xPosition = (mLineDivider*mDensity + mLineWidth)*drawCount + mLeftWidth;
          if(i%5 == 0 && i%10 != 0){//刻度为5的倍数,但同时不是10的倍数
              canvas.drawLine(xPosition,height*0.85f-mPaddingBottom,xPosition,height*0.15f+mPaddingTop,mLinePaint);
          }else if(i%10 == 0){//刻度为10的倍数
              canvas.drawLine(xPosition,height-mPaddingBottom,xPosition,mPaddingTop,mLinePaint);
          }else {//普通的刻度
              canvas.drawLine(xPosition,height*0.75f-mPaddingBottom,xPosition,height*0.25f+mPaddingTop,mLinePaint);
          }
          drawCount++;
      }
      canvas.restore();
  }
  • 然后画出可以拖动的刻度线(首图粉红色线),要实现该功能其实不难,第一种情况:通过在onTouch里面获取event.getX()坐标,而在这其中用到PointF类来保存x坐标,然后根据x坐标在onDraw()里面绘制即可;第二种情况:自动搜台,这其实很简单,开启子线程每Thread.sleep(200)就累加一定x值即可实现;

  • 最后通过回调把计算好的值传递到Activity中,任务完成!

要是不太清楚回调原理的可看我另外一篇博客:http://blog.youkuaiyun.com/github_38117599/article/details/68069868

下面贴上View的源码:

package com.xhunmon.radiorule;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.MotionEvent;
import android.view.View;

/**
 * user: uidq0530 ,date: 2017-04-16.
 * description:收音机FM搜台尺子
 *
 * @author xhunmon
 */

public class RadioRulerView extends View {

    private static final String tag = "RadioRulerView";

    private int mHeight;    //view的高度
    private int mWidth;     //view的宽度
    private Paint mLinePaint;   //固定的尺子画笔
    private int mLineWidth;//尺子刻度线的宽
    private int mLineColor;//固定尺子刻度线的颜色
    private int mMoveLineColor;//移动尺子刻度线的颜色
    private float mDensity;
    private int mLineDivider;    //两条刻度线间的距离
    private float mLeftWidth;  //尺子离view左边的距离

    private int mMaxLineCount = 220; //总共要画多少条刻度
    private Paint mMoveLinePaint;   //移动尺子的画笔
    private int mValue;        //尺子被选中的值
    private float mMaxX;  //onTouch中能触摸的最大x值
    private float mMinX;    //onTouch中能触摸的最小x值

    private OnValueChangeListener mListener;

    private SparseArray<PointF> activePointers;
    private PointF xPoint;
    private int mPaddingBottom;
    private int mPaddingTop;
    private boolean mIsAuto = false;

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

    public RadioRulerView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public RadioRulerView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr,0);
    }

    public RadioRulerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mDensity = context.getResources().getDisplayMetrics().density;
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RadioRulerView, defStyleAttr, defStyleRes);
        mLineWidth = (int) a.getDimension(R.styleable.RadioRulerView_line_width,5*mDensity);
        mLineDivider = (int) a.getDimension(R.styleable.RadioRulerView_line_divider,15*mDensity);

        mLineColor = a.getColor(R.styleable.RadioRulerView_line_color,0xff888888);
        mMoveLineColor = a.getColor(R.styleable.RadioRulerView_move_line_color,0xffff0000);
        a.recycle();

        init();
    }


    private void init() {
        activePointers = new SparseArray<>();

        mLinePaint = new Paint();
        mLinePaint.setAntiAlias(true);
        mLinePaint.setColor(mLineColor);
        mLinePaint.setStrokeWidth(mLineWidth);
        mLinePaint.setStyle(Paint.Style.STROKE);

        mMoveLinePaint = new Paint();
        mMoveLinePaint.setAntiAlias(true);
        mMoveLinePaint.setColor(mMoveLineColor);
        mMoveLinePaint.setStrokeWidth(mLineWidth);
        mMoveLinePaint.setStyle(Paint.Style.STROKE);
    }

    //此方法在view的尺寸确定后调用
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mHeight = getHeight();
        mWidth = getWidth();
        mPaddingBottom = getPaddingBottom();
        mPaddingTop = getPaddingTop();
        mLeftWidth = (mWidth - mMaxLineCount*(mLineWidth +mLineDivider))/2;
        mMaxX = mMaxLineCount*(mLineWidth +mLineDivider) + mLeftWidth;
        mMinX = mLeftWidth;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawLine(canvas);

        drawMoveLine(canvas);
    }

    /**
     * 画固定的尺子
     * @param canvas
     */
    private void drawLine(Canvas canvas) {
        canvas.save();
        int height = mHeight;
        int drawCount = 0;//已经画了刻度线的个数
        float xPosition;
        for(int i=0; drawCount<=mMaxLineCount; i++){
            xPosition = (mLineDivider*mDensity + mLineWidth)*drawCount + mLeftWidth;
            if(i%5 == 0 && i%10 != 0){//刻度为5的倍数,但同时不是10的倍数
                canvas.drawLine(xPosition,height*0.85f-mPaddingBottom,xPosition,height*0.15f+mPaddingTop,mLinePaint);
            }else if(i%10 == 0){//刻度为10的倍数
                canvas.drawLine(xPosition,height-mPaddingBottom,xPosition,mPaddingTop,mLinePaint);
            }else {//普通的刻度
                canvas.drawLine(xPosition,height*0.75f-mPaddingBottom,xPosition,height*0.25f+mPaddingTop,mLinePaint);
            }
            drawCount++;
        }
        canvas.restore();
    }


    /**
     * 搜索FM频道的刻度线
     * @param canvas
     */
    private void drawMoveLine(Canvas canvas) {
        canvas.save();
        xPoint = activePointers.valueAt(0);
        if (xPoint != null) {
            canvas.drawLine(xPoint.x,mHeight-mPaddingBottom, xPoint.x,mPaddingTop,mMoveLinePaint);
            setValue(eventXValue(xPoint.x));
        }else {
            canvas.drawLine(mMinX,mHeight-mPaddingBottom, mMinX,mPaddingTop,mMoveLinePaint);
        }
        canvas.restore();
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int pointerIndex = event.getActionIndex();
        int pointerId = event.getPointerId(pointerIndex);
        switch (event.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_POINTER_DOWN: {
                float downX = event.getX(pointerIndex);
                if(downX > mMaxX || downX < mMinX) break;
                PointF position = new PointF(downX, event.getY(pointerIndex));
                activePointers.put(pointerId, position);
                break;
            }
            case MotionEvent.ACTION_MOVE: {
                int pointerCount = event.getPointerCount();
                for (int i = 0; i < pointerCount; i++) {
                    PointF point = activePointers.get(event.getPointerId(i));
                    if (point == null) continue;
                    float moveX = event.getX(i);
                    if(moveX > mMaxX || moveX < mMinX) break;
                    point.x = event.getX(i);
                    point.y = event.getY(i);
                }
                break;
            }
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
            case MotionEvent.ACTION_CANCEL: {
                int pointerCount = event.getPointerCount();
                PointF point = activePointers.get(event.getPointerId(pointerCount-1));
                if (point == null)  break;
                float upX = event.getX(pointerCount-1);
                if(upX > mMaxX || upX < mMinX) break;
                point.x = eventXValue(event.getX(pointerCount-1));
                point.y = event.getY(pointerCount-1);
                break;
            }
        }
        invalidate();

        return true;
    }

    /**
     *作用:使得放手后MoveLine和Line重合;精确mValue
     * @param x onTouch中的event.getX()
     * @return
     */
    public int eventXValue(float x){
        mLineDivider = (int) (mLineDivider*mDensity);
        return (int) ((x-mLeftWidth)%(mLineWidth +mLineDivider)>((mLineWidth +mLineDivider)/2)
                        ? (((mLineWidth +mLineDivider)*((int)((x-mLeftWidth)/(mLineWidth +mLineDivider))+1))+mLeftWidth)
                        : (((mLineWidth +mLineDivider)*((int)((x-mLeftWidth)/(mLineWidth +mLineDivider))))+mLeftWidth));
    }

    /**
     * 设置最大刻度线个数
     * @param count
     */
    public void setMaxLineCount(int count) {
        mMaxLineCount = count;
    }

    /**
     * 设置是否启用自动搜索功能
     * @param isAuto
     */
    public void setAutoSearchFM(boolean isAuto){
        this.mIsAuto = isAuto;
    }

    /**
     * 开始自动搜台
     */
    public void startAutoSeachFM(){
        if(mIsAuto)
            new Thread(new SeachThread()).start();
    }

    /**
     * 搜台要在开启子线程
     */
    private class SeachThread implements Runnable{

        @Override
        public void run() {
            while(mIsAuto){
                xPoint = activePointers.valueAt(0);
                if(xPoint != null){
                    xPoint.x += (mLineWidth + mLineDivider);
                    if(xPoint.x > mMaxX) xPoint.x = mLeftWidth;
                }else {
                    PointF position = new PointF(mLeftWidth, mHeight);
                    activePointers.put(0, position);
                }
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                postInvalidate();
            }
        }
    }

    /*****************************值传递的回调*************************************/
    public interface OnValueChangeListener {
        void onValueChange(float value);
    }

    public void setOnValueChangeListener(OnValueChangeListener listener){
        mListener = listener;
    }

    private void setValue(float value) {
        if(mListener != null){
            mValue = (int) ((value - mLeftWidth)/(mLineDivider*mDensity + mLineWidth));
            //FM的范围从88.0 ~ 108.0
            mListener.onValueChange(mValue/10f + 88);
        }
    }

    /******************************************************************/
}

贴上Activity代码:

package com.xhunmon.radiorule;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;

public class MainActivity extends Activity implements RadioRulerView.OnValueChangeListener,
        CompoundButton.OnCheckedChangeListener, View.OnClickListener {

    private TextView mShow;
    private RadioRulerView mRule;
    private CheckBox mCbAuto;
    private Button mBtStart;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mShow = (TextView) findViewById(R.id.tv);
        mRule = (RadioRulerView) findViewById(R.id.rule);
        mCbAuto = (CheckBox) findViewById(R.id.cb_auto);
        mBtStart = (Button) findViewById(R.id.bt_start);

        mRule.setMaxLineCount(200);//FM从88.0 ~ 108.0总共有200频道
        mRule.setOnValueChangeListener(this);
        mCbAuto.setOnCheckedChangeListener(this);
        mBtStart.setOnClickListener(this);

    }

    @Override
    public void onValueChange(float value) {
        mShow.setText("FM:"+value);
    }

    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if(isChecked){
            mRule.setAutoSearchFM(true);
        }else {
            mRule.setAutoSearchFM(false);
        }
    }

    @Override
    public void onClick(View v) {
        if(v.getId() == R.id.bt_start){
            mRule.startAutoSeachFM();
        }
    }
}

整个项目都放在github上面了,欢迎做客与讨论:
https://github.com/xhunmon/RadioRule

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值