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

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

效果图:

实现思路:

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

贴上关键代码:

 /**
 * 画固定的尺子
 * @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中,任务完成!

要是不太清楚回调原理的可看我另外一篇博客:Android回调与观察者模式的实现原理

下面贴上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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • android实现读取、搜索联系人的代码

    代码很简单,就不多废话了 复制代码 代码如下: //读取联系人 public static Uri CONTACTSURI = ContactsContract.Contacts.CONTENT_URI;//联系人     public static void getContactsInfo(Context context,String tag){         String[] projections = new String[]{ContactsContract.Contacts._ID,

  • Android 百度地图POI搜索功能实例代码

    在没介绍正文之前先给大家说下poi是什么意思. 由于工作的关系,经常在文件中会看到POI这三个字母的缩写,但是一直对POI的概念和含义没有很详细的去研究其背后代表的意思.今天下班之前,又看到了POI这三个字母,决定认认真真的搜索一些POI具体的含义. POI是英文的缩写,原来的单词是point of interest, 直译成中文就是兴趣点的意思.兴趣点这个词最早来自于导航地图厂商.地图厂商为了提供尽可能多的位置信息,花费了很大的精力去寻找诸如加油站,餐馆,酒店,景点等目的地,这些目的地其实都可

  • Android文本框搜索和清空效果实现代码及简要概述

    前言 本文实现的效果:文本框输入为空时显示输入的图标;不为空时显示清空的图标,此时点击清空图标能清空文本框内输入文字. 正文 一.实现效果 二.实现代码 绑定事件 复制代码 代码如下: private Drawable mIconSearchDefault; // 搜索文本框默认图标 private Drawable mIconSearchClear; // 搜索文本框清除文本内容图标 @Override protected void onCreate(Bundle savedInstanceS

  • Android百度地图实现搜索和定位及自定义图标绘制并点击时弹出泡泡

    一.问题描述 上一次我们使用百度地图实现基本的定位功能,接下来我们继续实现搜索和定位,并使用LocationOverlay绘制定位位置,同时展示如何使用自定义图标绘制并点击时弹出泡泡 如图所示: 二.编写MyApplication类 public class MyApplication extends Application { private static MyApplication mInstance = null; public boolean m_bKeyRight = true; pu

  • Android实现搜索功能并本地保存搜索历史记录

    本文实例为大家分享了Android实现搜索功能,并且需要显示搜索的历史记录,供大家参考,具体内容如下 效果图: 本案例实现起来很简单,所以可以直接拿来嵌入项目中使用,涉及到的知识点: - 数据库的增删改查操作 - ListView和ScrollView的嵌套冲突解决 - 监听软键盘回车按钮设置为搜索按钮 - 使用TextWatcher( )实时筛选 - 已搜索的关键字再次搜索不重复添加到数据库 - 刚进入页面设置软键盘不因为EditText而自动弹出 代码 RecordSQLiteOpenHel

  • Android实现带列表的地图POI周边搜索功能

    先看效果图:(以公司附近的国贸为中心点) 上面是地图,下面是地理位置列表,有的只有地理位置列表(QQ动态的位置),这是个很常见的功能.它有个专门的叫法:POI周边搜索. 实现: 这个效果实现起来其实很简单,不过需要你先阅读下地图的API,这里使用的是高德地图的Android SDK,SDK的配置这里不作讲解,文末会放一些链接供学习. 思路: 1.利用地图的定位功能,获取用户当前的位置 2.根据获得的位置信息调用POI搜索,获取位置列表 3.ListView展示位置列表 4.用户拖动地图,获取地图

  • Android拨号盘 支持T9搜索和号码搜索等拨号盘案例

    之前做通讯录软件,其中在做拨号盘的时候一直为怎么实现T9输入烦恼,上网找了很多帖子,都没有满意的答案.不过最后终于是实现了,看社区内好像也有不少朋友需要,在此分享一下.这个是在我项目中提取出来的拨号盘案例,功能上完全实现了目前其他通讯录中拨号盘的功能,但在加载效率上还有不足,请各位大侠指教. 有图有真相:     目前我用1g单核cpu的索爱mt15i的测试机,1500多的联系人,加载时间大概在8秒左右,当然,一般用户不会有这么多联系人的.由于我做的程序不是在一开始的界面就是拨号盘,所以我在程序

  • Android自定义收音机搜台控件RadioRulerView

    前言:像这类的自定义控件有非常多的开源项目,但还是没有找到我项目想要的,所以简单实现了一个,下面简单讲讲实现原理. 效果图: 实现思路: 首先画固定背景尺子,而实现这个则要计算刻度线的宽度.刻度线间的距离,以及要确定刻度线的总是,根据这些可以求出第一条刻度线的x坐标,使得整个尺子居中:下图为尺子尺寸的计算方法: 贴上关键代码: /** * 画固定的尺子 * @param canvas */ private void drawLine(Canvas canvas) { canvas.save();

  • Android自定义View之组合控件实现类似电商app顶部栏

    本文实例为大家分享了Android自定义View之组合控件,仿电商app顶部栏的相关代码,供大家参考,具体内容如下 效果图: 分析:左右两边可以是TextView和Button,设置drawableTop即可,中间的看着像是EditText,但是用过淘宝天猫等类似app的话会发现点击搜索不是在当前Activit进行搜索的,是跳转到另外的页面进行的,所以用TextView然后设置背景即可. 实现流程 参数列表: 设置属性文件:values下建立attrs.xml文件,添加需要自定义的属性. <?x

  • Android 自定义底部上拉控件的实现方法

    前言 又到了新的一月,今天提供一个Android自定义底部上拉布局的实现,起因是自己在项目中需要实现这样一个控件,干脆自己写一个练练手. 写完了觉得能想到的需求都基本有了(可能会有其它需求,不过基本上改吧改吧就行了),又花了一点时间直接放到了Github上托管,希望能给您一些参考价值: SlideBottomLayout-Android 简单易上手的Android底部上拉控件 先看一下实现效果: 分析一下这种控件的基本需求有以下几种: 1.有一个部分是能够作为把手(就是图中的handle,)进行

  • android自定义圆形倒计时显示控件

    本文实例为大家分享了android自定义圆形倒计时显示控件的具体代码,供大家参考,具体内容如下 先上效果图 - 倒计时结束 代码块 attr.xml 控件需要用到的属性: <?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="CountDownView"> <!--颜色--> <attr name

  • Android自定义view实现输入控件

    本文实例为大家分享了Android自定义view实现输入控件的具体代码,供大家参考,具体内容如下 网络上大部分的输入控件都是多个EditText组合而成,本例中采用的是: 单个EditText作为输入的捕捉控件 多个ImageView的子类作为显示的控件,绘制EditText中的数据 如上图: 输入前和输入后输入框需要发生响应的改变 点击自定义控件要弹出软键盘 EditText数据捕捉,以及EditView不能操作(如果可以操作,数据处理会混乱) 输完后会得到相应的提示 ImageView的子类

  • Android自定义顶部导航栏控件实例代码

    下面一段代码给大家介绍了android 自定义顶部导航栏控件功能,具体代码如下所示: class HeaderBar @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : FrameLayout(context, attrs, defStyleAttr) { //重写构造方法 在java里面 我们一般是重写三个构造方法//在kotlin中 我们可以使用

  • android自定义WaveView水波纹控件

    本文实例为大家分享了android自定义WaveView水波纹控件的使用方法,供大家参考,具体内容如下 Github Repository and libaray WaveView水波纹控件 首先看下演示demo demo中可以看到不同高度,不同速度,不同幅度的水波纹:你可以通过view的参数直接控制view的表现形式. 引入你的工程 在项目的根目录下的build.gradle文件中添加如下代码: allprojects { repositories { ... maven { url 'htt

  • Android 自定义日期段选择控件功能(开始时间-结束时间)

    开发中碰到个需求,需要在一个空间中选择完成开始和结束时间.实现的过程走的是程序员开发的老路子,找到轮子后自己改吧改吧就成了. 当时做的时候有几个需求:1.当天为最大的结束日期,2.最大选择范围1年,3.开始时间和结束时间可以为同一天.如有其他需求实现,可以参考代码改进一下.先上效果图: 视频点击后的虚影是屏幕录制的原因.实现步骤:(如有缺失什么资源,请告知.开始时间和结束时间显示自己布局内添加就可以) 1.自定义控件属性 <declare-styleable name="MyCalenda

  • Android自定义view实现倒计时控件

    本文实例为大家分享了Android自定义view实现倒计时控件的具体代码,供大家参考,具体内容如下 直接上代码 自定义TextView 文字展示 public class StrokeTextView extends TextView { private TextView borderText = null;///用于描边的TextView private Context mContext; public StrokeTextView(Context context) { super(conte

  • Android自定义滑动接听电话控件组实例

    本文根据组件开发思想,首先介绍android自定义控件,然后将自定义的控件封装为jar包.最为实现滑动接听电话控件组. 一.目录结构 二.运行效果 三.代码实现 首先,自定义一个类IncomingPhone继承RelativeLayout public IncomingPhone(Context context, AttributeSet attrs) { super(context, attrs); mContext = context; TextView textView = new Tex

随机推荐