美文网首页
Android自定义View——QQ音乐均衡器

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

作者: 奥利奥真好吃 | 来源:发表于2018-01-14 15:40 被阅读0次

    相信很多经常使用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

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

    相关文章

      网友评论

          本文标题:Android自定义View——QQ音乐均衡器

          本文链接:https://www.haomeiwen.com/subject/glpkoxtx.html