相信很多经常使用QQ音乐的人(当然这类APP应该都有类似的功能)都知道均衡器这种东西,它可以将音乐的风格改为爵士、流行、古典、电子等等,这里我们选择自定义调节这里的UI(其实都一样,非自定义只是固定值而已),如下图所示:
IMG_0801.PNG
刚开始的时候我以为整个UI界面是一个整体的自定义View(尽管也可以这样做,但是首先绘制比较复杂,而且不符合软件工程低耦合的要求),后来用hierarchyviewer查看的时候发现并不是这样的,自定义View只有中间的平衡器,其它文字都是在textview上面做显示。所以这里我们只做中间的自定义View平衡器部分。
在不考虑文字显示的情况下,问题就简单了:一共10条线将整个View宽度11等分,从-12到12db将View高度26等分,因为只考虑整数显示,在滑动的过程中对Y坐标考虑四舍五入。(试想如果要是将上下的文字显示空间考虑在内,一个坐标问题就会很头痛了)然后是绘画过程,细看上图你会发现绘制的时候先后顺序很重要,我这里选择先画10个结点构成的连线,然后画每个结点,最后才是10条竖线:
private Paint mPaint; //画布的主绘笔
private Paint nodePaint; //画每个结点圆的画笔
private Paint connectPaint; //画连线的画笔
private int mWidth,mHeight; //View的宽高
private PointF[] pointsArray; //将12个结点的坐标做存储,第一个和最后一个设定超出视图外
private final int STATE_NONE=0; //View第一个绘制时设定的状态
private final int STATE_TOUCH_DOWN=1; //正在TouchDown
private final int STATE_TOUCH_MOVE=2; //正在TouchMove
private final int STATE_TOUCH_UP=3; //正在TouchUp
private int STATE_NOW=STATE_NONE;
private int[] decibelArray; //将PointF坐标转换为要显示的整数
private float mRadius; //结点圆的半径
private float step; //View的高度被等分的步长
private void refreshView(Canvas canvas,int stepSize){
float[] points=new float[]{pointsArray[0].x,pointsArray[0].y,pointsArray[1].x,pointsArray[1].y,
pointsArray[1].x,pointsArray[1].y,pointsArray[2].x,pointsArray[2].y,
pointsArray[2].x,pointsArray[2].y,pointsArray[3].x,pointsArray[3].y,
pointsArray[3].x,pointsArray[3].y,pointsArray[4].x,pointsArray[4].y,
pointsArray[4].x,pointsArray[4].y,pointsArray[5].x,pointsArray[5].y,
pointsArray[5].x,pointsArray[5].y,pointsArray[6].x,pointsArray[6].y,
pointsArray[6].x,pointsArray[6].y,pointsArray[7].x,pointsArray[7].y,
pointsArray[7].x,pointsArray[7].y,pointsArray[8].x,pointsArray[8].y,
pointsArray[8].x,pointsArray[8].y,pointsArray[9].x,pointsArray[9].y,
pointsArray[9].x,pointsArray[9].y,pointsArray[10].x,pointsArray[10].y,
pointsArray[10].x,pointsArray[10].y,pointsArray[11].x,pointsArray[11].y};
//感觉这个还挺好用的,就是参数麻烦点
canvas.drawLines(points,connectPaint);
for(int i=1;i<=10;i++){
float cx = stepSize * i, cy = pointsArray[i].y;
if(i==index&&STATE_NOW!=STATE_TOUCH_UP) {
mRadius=50; //TouchDown和Move的过程中结点被放大
}else{
mRadius=40;
}
canvas.drawCircle(cx, cy, mRadius, nodePaint); //外部圆
canvas.drawCircle(cx, cy, mRadius - 6 , connectPaint); //再次画圆变环
mPaint.setColor(ContextCompat.getColor(mContext, R.color.equalizer_node_over_line));
mPaint.setStrokeWidth(6);
canvas.drawLine(cx, cy + mRadius + 3, stepSize * i, mHeight, mPaint); //画上半部分的黑线
mPaint.setColor(ContextCompat.getColor(mContext, R.color.equalizer_node_last_line));
canvas.drawLine(cx, cy - mRadius - 3, stepSize * i, 0, mPaint);
} //画下半部分的绿线
}
我这里设定了几个状态主要是为了提高代码的复用性,每次绘制的内部并不会少,不知道有没有局部绘制的方法。
在重写Touch方法前先考虑一下怎样判断当然手指是否落在正确的结点位置,知道圆的半径,知道手指落点的坐标,很简单的坐标判定:
/**
* 查出当前正在操作的是哪个结点
* @param x
* @param y
* @return
*/
private int findTheIndex(float x,float y){
int result=0;
for(int i=1;i<pointsArray.length;i++){
//为了方便用户操作,这里我们不会严格按结点的半径来判断用户touch落点,这里我设定为1.5倍的半径范围都是合法的
if(pointsArray[i].x-mRadius*1.5<x&&pointsArray[i].x+mRadius*1.5>x&&
pointsArray[i].y-mRadius*1.5<y&&pointsArray[i].y+mRadius*1.5>y) {
result = i;
break;
}
}
return result;
}
然后我们要将Y坐标正确转换为-12到12之间的整数,Y坐标越大,这个整数越小:
/**
* 将坐标转换为-12到12之间的数字
* @param y
* @return
*/
private int getTheDecibel(float y){
//因为在做纵向等分的时候头尾都可能是不够一整份的,所以针对这两个特殊点做特殊处理(防止逆向转换为Y坐标的时候坐标点超出View的高度)
if(y==getHeight()-40)
return -12;
else if(y==40f)
return 12;
else
return 13-Math.round(y/step);
}
最后的onTouch事件就好说了
private int mLastY=0;
private int index=0;
@Override
public boolean onTouchEvent(MotionEvent event) {
int action=event.getAction();
int x=(int)event.getX(),y=(int)event.getY();
switch (action){
case MotionEvent.ACTION_DOWN:{
index=findTheIndex(x,y);
if(index!=0){
STATE_NOW=STATE_TOUCH_DOWN;
invalidate();
}
break;
}
case MotionEvent.ACTION_MOVE:{
float deltaY=y-mLastY;
if(index!=0){
STATE_NOW=STATE_TOUCH_MOVE;
pointsArray[index].y+=deltaY;
if(y<=40)
pointsArray[index].y=40; //防止结点圆部分超出视图
if(y>=mHeight-40)
pointsArray[index].y=mHeight-40; //防止结点圆部分超出视图
decibelArray[index-1]=getTheDecibel(pointsArray[index].y);
invalidate();
listener.updateDecibel(decibelArray);
}
break;
}
//要说的是用户的手指UP完全可能会超出View之外,还有就是结点的Y坐标在这里一定要是step步长的整数倍,说白了就是做一次强制调整
case MotionEvent.ACTION_UP:{
if(index!=0){
STATE_NOW=STATE_TOUCH_UP;
if(decibelArray[index-1]!=0&&decibelArray[index-1]!=-12&&
decibelArray[index-1]!=12) {
float lastY = step * (decibelArray[index-1] + 13);
pointsArray[index].y = lastY;
}else if(decibelArray[index-1]==0)
pointsArray[index].y=step*13;
invalidate();
}
break;
}
default:
break;
}
mLastY=y;
return true;
}
以上自定义View基本完成,那么要显示在另外的TextView上应该怎样做呢?毫无疑问也是简单的方法:接口回调函数!接口回调函数!!接口回调函数!!!刚开始我居然直接给它传了一个TextView进去,这绝对是搞笑呢,没有这么做的。另外可以根据需要增加一些set、get方法,例如切换为非自定义模式的平衡器时,可以增加如下方法:
public int[] getDecibelArray() {
return decibelArray;
}
public void setDecibelArray(int[] decibelArray) {
this.decibelArray = decibelArray;
invalidate();
}
接口是个好东西
private updateDecibelListener listener;
interface updateDecibelListener{
void updateDecibel(int[] decibels);
}
public void setUpdateDecibelListener(updateDecibelListener listener){
this.listener=listener;
}
使用也很简单,Activity里面直接来实现这个接口即可,这里也贴一下代码好了:
public class MainActivity extends AppCompatActivity implements QQMusicEqualizerView.updateDecibelListener{
QQMusicEqualizerView mView;
TextView tv1,tv2,tv3,tv4,tv5,tv6,tv7,tv8,tv9,tv10;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
public void initView(){
WindowManager wm=getWindowManager();
DisplayMetrics dm=new DisplayMetrics();
wm.getDefaultDisplay().getMetrics(dm);
int width=dm.widthPixels;
float step=width/11f;
float padding=step/2;
mView=findViewById(R.id.testview);
int[] array=new int[]{3,1,2,7,-6,-4,-9,11,-10,5};
mView.setDecibelArray(array);
mView.setUpdateDecibelListener(this);
LinearLayout linearLayout=findViewById(R.id.linearlayout1);
linearLayout.setPadding((int)padding,0,(int)padding,0);
tv1=findViewById(R.id.textView1);
tv2=findViewById(R.id.textView2);
tv3=findViewById(R.id.textView3);
tv4=findViewById(R.id.textView4);
tv5=findViewById(R.id.textView5);
tv6=findViewById(R.id.textView6);
tv7=findViewById(R.id.textView7);
tv8=findViewById(R.id.textView8);
tv9=findViewById(R.id.textView9);
tv10=findViewById(R.id.textView10);
setDecibel(array);
}
public void setDecibel(int[] array){
tv1.setText(array[0]+"db");
tv2.setText(array[1]+"db");
tv3.setText(array[2]+"db");
tv4.setText(array[3]+"db");
tv5.setText(array[4]+"db");
tv6.setText(array[5]+"db");
tv7.setText(array[6]+"db");
tv8.setText(array[7]+"db");
tv9.setText(array[8]+"db");
tv10.setText(array[9]+"db");
}
@Override
public void updateDecibel(int[] decibels) {
setDecibel(decibels);
}
这么多TextView,一个个的findViewById、setText也是够麻烦的,要知道我这都简化了,QQ音乐那个界面共有21个TextView,不能也这么干吧,不知道有什么简便方法没。
最后上图来看一下效果吧:
渣渣模拟器.gif
网友评论