闲来无事,撸了个自定义了个均衡器 EqualizerView,遵循测量-布局-绘制三部曲,最后加上触摸交互动作。本控件支持手机端和TV端使用,可应用在音乐播放器中。
效果图:

代码:

package com.sjl.equalizerview;import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;import java.util.HashMap;
import java.util.Map;/*** 自定义EQ均衡器,支持手机和TV端** @author Kelly* @version 1.0.0* @filename EqualizerView.java* @time 2020/5/12 10:35* @copyright(C) 2020 song*/
public class EqualizerView extends View {private static final String TAG = "EqualizerView";private int mWidth, mHeight;private int mMinHeight = 600;private float circleTextSize;private int circleTextColor;/*** 圆圈半径大小*/private float circleRadius;/*** x轴文本数字*/private float xTextSize;private int xSelectColor, xUnSelectColor;/*** 左右边距*/private int marginLR;/*** dB宽度*/private int mDbSize;/*** dB X轴步长*/private int xAxialStep;/*** 画笔*/private Paint mPaint;/*** X轴值,dB*/private int[] yAxialVal = new int[]{5, 0, -5, -10};/*** Y轴值,Hz*/private int[] xAxialVal = new int[]{100, 500, 1500, 5000, 10000};/*** 当前选中的Hz Bar,-1表示未选中*/private int currentSelectBarIndex = -1;/*** dB条数量*/private int maxDbBarNum = 0;private Map<Integer, Integer> dBAndHzMap = new HashMap<>();private float startY;  //在屏幕上滑动调节dB时,开始的Y轴值private float touchRange;  //屏幕的高,因为涉及到横竖屏切换,到时候会取小的值/*** 当前点击选中的Hz对应的dB值*/private int currentDb = 0;public EqualizerView(Context context) {this(context, null);}public EqualizerView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public EqualizerView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(context, attrs);}private void init(Context context, AttributeSet attrs) {TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.EqualizerView);circleTextSize = a.getDimension(R.styleable.EqualizerView_evCircleTextSize, dip2px(getContext(), 8));circleTextColor = a.getColor(R.styleable.EqualizerView_evCircleTextColor, Color.WHITE);circleRadius = a.getDimension(R.styleable.EqualizerView_svCircleRadius, 20);xTextSize = a.getDimension(R.styleable.EqualizerView_evXTextSize, dip2px(getContext(), 8));xSelectColor = a.getColor(R.styleable.EqualizerView_evXSelectColor, Color.parseColor("#81AA81"));xUnSelectColor = a.getColor(R.styleable.EqualizerView_evXUnSelectColor, Color.parseColor("#636363"));marginLR = a.getInt(R.styleable.EqualizerView_evLRMargin, 50);a.recycle();mPaint = new Paint();//设置画笔的颜色mPaint.setColor(Color.WHITE);//设置抗锯齿mPaint.setAntiAlias(true);initXY(xAxialVal, yAxialVal, new int[]{0, 0, 0, 0, 0});//下面使回调onkeydown事件setFocusableInTouchMode(true); //确保能接收到触屏事件setFocusable(true); //确保我们的View能获得输入焦点}private void initXY(int[] xAxialVal, int[] yAxialVal, int[] defaultYVal) {if (xAxialVal.length != defaultYVal.length) {throw new IllegalArgumentException("Y坐标值个数不匹配X坐标个数");}int y = yAxialVal[0] - yAxialVal[yAxialVal.length - 1];maxDbBarNum = y + 1;for (int i = 0; i < xAxialVal.length; i++) {dBAndHzMap.put(i, calculateDbBum(defaultYVal[i]) + 1);}}/*** 计算db数量块** @param dbValue* @return*/private int calculateDbBum(int dbValue) {int up = yAxialVal[0];int down = yAxialVal[yAxialVal.length - 1];int val;if (dbValue >= down && dbValue <= up) {val = dbValue - down;} else {throw new IllegalArgumentException("dB 值越界:" + dbValue);}return val;}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);int width;int height;int widthSize = MeasureSpec.getSize(widthMeasureSpec);int heightSize = MeasureSpec.getSize(heightMeasureSpec);int widthMode = MeasureSpec.getMode(widthMeasureSpec);int heightMode = MeasureSpec.getMode(heightMeasureSpec);if (widthMode == MeasureSpec.EXACTLY) {width = widthSize;} else {width = widthSize * 1 / 2;}if (heightMode == MeasureSpec.EXACTLY) {height = heightSize;} else {height = heightSize * 1 / 2;}if (height < mMinHeight) {//适配大屏问题height = mMinHeight;}setMeasuredDimension(width, height);}//计算高度宽度@Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);mWidth = getWidth();mHeight = getHeight();int size = xAxialVal.length + 1;mDbSize = mWidth / (size * 3);//dB块宽度xAxialStep = mWidth / size;Log.i(TAG, "mWidth:" + mWidth + ",mHeight:" + mHeight + ",xAxialStep:" + xAxialStep + ",mSize:" + mDbSize);}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);mPaint.setColor(Color.WHITE);mPaint.setTextSize(xTextSize);mPaint.setStyle(Paint.Style.FILL);float dB = mPaint.measureText("dB");//绘制dB单位canvas.drawText("dB", marginLR + circleRadius - dB / 2, marginLR, mPaint);int yStep = 150;int xStart = 20 + marginLR;int yTop = 20 + marginLR;//圆形距离顶部dB的开始距离int yStart = (int) (circleRadius + yTop);int startY = yStart, lastY = 0;//绘制Y轴坐标for (int i = 0; i < yAxialVal.length; i++) {mPaint.setStyle(Paint.Style.STROKE);//绘制圆弧canvas.drawCircle(xStart, yStart, circleRadius, mPaint);//40直径lastY = yStart;//绘制圆圈数字float v = mPaint.measureText(String.valueOf(yAxialVal[i]));mPaint.setStyle(Paint.Style.FILL);mPaint.setColor(circleTextColor);mPaint.setTextSize(circleTextSize);//绘制圆圈数字canvas.drawText(String.valueOf(yAxialVal[i]), marginLR + circleRadius - v / 2, getBaseLineY((int) (2 * circleRadius)) - circleRadius + yStart, mPaint);//绘制直线canvas.drawLine(marginLR + 2 * circleRadius, yStart, getWidth() - marginLR, yStart, mPaint);yStart += yStep;}int xAxialStart = (int) (marginLR + 80 + 2 * circleRadius);//dB块总高度int dBTotalHeight = lastY - startY;/*** 单个dB块高度*/int dBHeight = dBTotalHeight / (maxDbBarNum * 2 - 2);//绘制X轴坐标for (int i = 0; i < xAxialVal.length; i++) {int dbVal;if (i == currentSelectBarIndex) {//选中绿色dbVal = dBAndHzMap.get(currentSelectBarIndex);mPaint.setColor(xSelectColor);} else {//未选中的dbVal = dBAndHzMap.get(i);mPaint.setColor(xUnSelectColor);}//柱状dB绘制float v1 = xAxialStart;for (int j = 0; j < dbVal; j++) {//int left, int top, int right, int bottomint top = lastY + dBHeight / 2 - dBHeight - 2 * j * dBHeight;int bottom = lastY + dBHeight / 2 - 2 * j * dBHeight;canvas.drawRect(new Rect((int) (v1), top, (int) (mDbSize + v1), bottom), mPaint);}float v = mPaint.measureText(String.valueOf(xAxialVal[i]));mPaint.setStyle(Paint.Style.FILL);mPaint.setTextSize(xTextSize);//绘制底部文本int offsetX;if (v / 2 > mDbSize / 2) {//使得对准dB块中间offsetX = (int) -Math.abs(v / 2 - mDbSize / 2);} else {offsetX = (int) Math.abs(v / 2 - mDbSize / 2);}canvas.drawText(String.valueOf(xAxialVal[i]), v1 + offsetX, lastY + 2 * circleRadius, mPaint);xAxialStart += xAxialStep;}//绘制Hz单位mPaint.setColor(Color.WHITE);float v = mPaint.measureText("Hz");int lastX = xAxialStart - xAxialStep + mDbSize;//最左边dB的右侧坐标int hzOffset;if (getWidth() - marginLR - lastX - 50 > v) {hzOffset = getWidth() - marginLR - 50;} else {hzOffset = getWidth() - marginLR;}canvas.drawText("Hz", hzOffset, lastY + 2 * circleRadius, mPaint);}@Overridepublic boolean onTouchEvent(MotionEvent ev) {int x = (int) ev.getX();int y = (int) ev.getY();int left = (int) (marginLR + 80 + 2 * circleRadius);int top = 0;int length = xAxialVal.length;int right = left + mDbSize;int bottom = mHeight;switch (ev.getAction()) {case MotionEvent.ACTION_DOWN:startY = ev.getY();touchRange = Math.min(mWidth, mHeight);for (int i = 0; i < length; i++) {Rect rect = new Rect(left, top, right, bottom);left += xAxialStep;right = left + mDbSize;//精确点击位置if (rect.contains(x, y)) {currentSelectBarIndex = i;currentDb = dBAndHzMap.get(currentSelectBarIndex);  //获取滑动开始时的音量invalidate();break;}}break;case MotionEvent.ACTION_MOVE:updateDb(ev);break;case MotionEvent.ACTION_UP:break;default:break;}return true;}private void updateDb(MotionEvent ev) {float endY = ev.getY();     //滑动的距离float distance = startY - endY;  //相对滑动的距离float changeDb = (distance / touchRange) * maxDbBarNum;  //改变的dBint dB = (int) Math.min(Math.max(currentDb + changeDb, 0), maxDbBarNum);  //改变后的dBLog.i(TAG, "onTouchEvent, changeDb:" + changeDb + ",dB:" + dB);if (changeDb != 0 && dB > 0) {dBAndHzMap.put(currentSelectBarIndex, dB);invalidate();}}/*** 获取基线y轴坐标** @param circleR* @return*/public int getBaseLineY(int circleR) {Paint.FontMetrics fontMetrics = mPaint.getFontMetrics();float textTop = fontMetrics.top;float textBottom = fontMetrics.bottom;float contentBottom = circleR / 2;int baseLineY = (int) (contentBottom - textTop / 2 - textBottom / 2);return baseLineY;}/*** 根据手机的分辨率从 dp 的单位 转成为 px(像素)*/public static int dip2px(Context context, float dpValue) {final float scale = context.getResources().getDisplayMetrics().density;return (int) (dpValue * scale + 0.5f);}/*** 向做移动音量条*/public void moveLeft() {if (currentSelectBarIndex == 0) {return;}currentSelectBarIndex--;invalidate();}/*** 向右移动音量条*/public void moveRight() {if (currentSelectBarIndex == xAxialVal.length - 1) {return;}currentSelectBarIndex++;invalidate();}/*** 增加音量*/public void moveUp() {if (currentSelectBarIndex == -1) {return;}int defaultYVal = dBAndHzMap.get(currentSelectBarIndex);if (defaultYVal == maxDbBarNum) {return;}defaultYVal++;dBAndHzMap.put(currentSelectBarIndex, defaultYVal);invalidate();}/*** 降低音量*/public void moveDown() {if (currentSelectBarIndex == -1) {return;}int defaultYVal = dBAndHzMap.get(currentSelectBarIndex);if (defaultYVal == 1) {//保留最后一隔音量return;}defaultYVal--;dBAndHzMap.put(currentSelectBarIndex, defaultYVal);Log.i(TAG, "触发了moveDown");invalidate();}/*** 设置默认选中的柱状条** @param currentSelectBarIndex*/public void setCurrentSelectBarIndex(int currentSelectBarIndex) {this.currentSelectBarIndex = currentSelectBarIndex;invalidate();}/*** 整体修改** @param dbValue 指定值,不能超出y轴范围,可以为正负*/public void setDbVal(int dbValue) {int val = calculateDbBum(dbValue);for (int i = 0; i < xAxialVal.length; i++) {dBAndHzMap.put(i, val + 1);}invalidate();}/*** 重置*/public void reset() {setDbVal(0);}@Overridepublic boolean onKeyDown(int keyCode, KeyEvent event) {switch (keyCode) {case KeyEvent.KEYCODE_DPAD_UP://向上Log.e(TAG, "-----向上-----");moveUp();break;case KeyEvent.KEYCODE_DPAD_DOWN://向下Log.e(TAG, "-----向下-----");moveDown();break;case KeyEvent.KEYCODE_DPAD_LEFT://向左Log.e(TAG, "-----向左-----");moveLeft();break;case KeyEvent.KEYCODE_DPAD_RIGHT://向右Log.e(TAG, "-----向右-----");moveRight();break;case KeyEvent.KEYCODE_ENTER://确定Log.e(TAG, "-----确定-----");break;case KeyEvent.KEYCODE_BACK://返回Log.e(TAG, "-----返回-----");break;case KeyEvent.KEYCODE_HOME://房子Log.e(TAG, "-----房子-----");break;case KeyEvent.KEYCODE_MENU://菜单Log.e(TAG, "-----菜单-----");break;}return super.onKeyDown(keyCode, event);}/*** 设置数据** @param xVal        X坐标* @param yVal        y坐标* @param defaultYVal y坐标默认值*/public void setXYData(int[] xVal, int[] yVal, int[] defaultYVal) {this.xAxialVal = xVal;this.yAxialVal = yVal;int size = xVal.length + 1;mDbSize = mWidth / (size * 3);//dB块宽度xAxialStep = mWidth / size;initXY(xVal, yVal, defaultYVal);invalidate();}/*** 设置Y坐标值数据** @param defaultYVal y坐标默认值*/public void setYVal(int[] defaultYVal) {initXY(this.xAxialVal, this.yAxialVal, defaultYVal);invalidate();}/*** 获取d和Hz映射值** @return*/public Map<Integer, Integer> getDbAndHzMap() {int down = yAxialVal[yAxialVal.length - 1];Map<Integer, Integer> temp = new HashMap<>();for (Map.Entry<Integer, Integer> entry : dBAndHzMap.entrySet()) {Integer key = entry.getKey();Integer value = entry.getValue();int realVal = value + down - 1;temp.put(key, realVal);}return temp;}}

项目github地址:

https://github.com/kellysong/EqualizerView

Android自定义均衡器 EqualizerView相关推荐

  1. Android 自定义音乐播放器实现

    Android自定义音乐播放器 一:首先介绍用了哪些Android的知识点: 1 MediaPlayer工具来播放音乐 2 Handle.因为存在定时任务(歌词切换,动画,歌词进度条变换等)需要由Ha ...

  2. Android自定义ViewGroup基本步骤

    1.自定义属性,获取自定义属性,可参考 ​ Android自定义View基本步骤 ​ 2.onMeasure() 方法,for循环测量子View,根据子View的宽高来计算自己的宽 高 3.onDra ...

  3. Android自定义View —— TypedArray

    在上一篇中Android 自定义View Canvas -- Bitmap写到了TypedArray 这个属性 下面也简单的说一下TypedArray的使用 TypedArray 的作用: 用于从该结 ...

  4. Android 自定义View —— Canvas

    上一篇在android 自定义view Paint 里面 说了几种常见的Point 属性 绘制图形的时候下面总有一个canvas ,Canvas 是是画布 上面可以绘制点,线,正方形,圆,等等,需要和 ...

  5. android 自定义loading,Android自定义动画-StarLoadingView

    今天来分享第二个自定义loading的动画,起了个名字叫 蹦跶的星星 ,还是老规矩先介绍,后上图. 实现效果在最后,GIF有点大,手机流量慎重. 介绍 首先声明做这个动画的初衷是为了学习和分享,所以从 ...

  6. android自定义view获取控件,android 自定义控件View在Activity中使用findByViewId得到结果为null...

    转载:http://blog.csdn.net/xiabing082/article/details/48781489 1.  大家常常自定义view,,然后在xml 中添加该view 组件..如果在 ...

  7. android 自定义命名空间,Android自定义ActionBar实例

    本文实例讲述了android自定义actionbar的实现方法.分享给大家供大家参考.具体实现方法如下: android 3.0及以上已经有了actionbar的api,可以通过引入support p ...

  8. Android自定义View:ViewGroup(三)

    自定义ViewGroup本质是什么? 自定义ViewGroup本质上就干一件事--layout. layout 我们知道ViewGroup是一个组合View,它与普通的基本View(只要不是ViewG ...

  9. Android自定义视图四:定制onMeasure强制显示为方形

    这个系列是老外写的,干货!翻译出来一起学习.如有不妥,不吝赐教! Android自定义视图一:扩展现有的视图,添加新的XML属性 Android自定义视图二:如何绘制内容 Android自定义视图三: ...

最新文章

  1. 关于Python类属性与实例属性的讨论
  2. Spring【依赖注入】就是这么简单
  3. 延大计算机文化基础课程作业,基于项目学习的大学《计算机文化基础课》教学设计...
  4. C:#define用法
  5. [翻译]编写高性能 .NET 代码 第一章:性能测试与工具 -- 平均值 vs 百分比
  6. 《学习OpenCV》课后习题解答1
  7. android页面布局更改,使用setContentView的方式更换布局文件从而更换界面
  8. 改造Python中文拼音扩展库pypinyin补充自定义声母全过程
  9. 小沙的步伐(枚举+暴力)
  10. dup和dup2(摘 )
  11. java mysql 博客园_JAVA基础--MySQL
  12. VS 2017番茄插件安装破解教程:visual assist
  13. vue如何加载html字符串_VUE渲染后端返回含有script标签的html字符串示例
  14. 三星note9刷Android9,三星Note9官方韩版安卓9固件rom线刷刷机包:N960NKSU2CSE3
  15. Xshell5(远程终端工具)工具的安装使用 【免费】
  16. java大嘴鱼游戏代码_深海迷航零度之下全代码汇总 常用作弊码及使用方法
  17. cad文字宽度因子_CAD怎么设置中输入的文字宽度统一?
  18. html文档头部标记,HTML头部标记
  19. 支付宝第三方支付保证数据的安全性
  20. 《环球》杂志|“宇宙级”漏洞过后,一个技术总裁的忠告……

热门文章

  1. 头脑风暴算法BSO优化BP神经网络-matlab源码
  2. 3.Java中JVM, JRE和JDK的关系是什么?
  3. Web端打开本地可执行的exe程序
  4. Lottie—json文件解析
  5. 乐高 计算机泡泡龙教案,小班科学教案:泡泡龙的秘密
  6. STM32使用__attribute__后下载提示No Algorithm found for: xxxH - xxxH
  7. Okular – 轻巧快速的跨平台文档阅读器
  8. 一篇还不错的介绍make的文章
  9. 会覆盖本地_新服务进阶,阿里本地生活开启“三环阵营”
  10. 【关于实施携号转网后运营商控流失营服体系的探讨】