Android自定义View——QQ音乐均衡器

相信很多经常使用QQ音乐的人(当然这类APP应该都有类似的功能)都知道均衡器这种东西,它可以将音乐的风格改为爵士、流行、古典、电子等等,这里我们选择自定义调节这里的UI(其实都一样,非自定义只是固定值而已),如下图所示:


Android自定义View——QQ音乐均衡器_第1张图片
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;ix&&
                    pointsArray[i].y-mRadius*1.5y) {
                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

完整代码地址:
https://github.com/Geek-L/AndroidProject/blob/master/CustomView/app/src/main/java/com/zyl/customview/view/QQMusicEqualizerView.java

你可能感兴趣的:(Android自定义View——QQ音乐均衡器)