自定义音乐播放器的歌词显示view

网易云音乐是我最经常使用的一个软件。不只界面美观,功能还不错(这不是打广告哈)。今天,我就来利用网易云音乐现成的歌词文件来制做一个自定义的歌词显示view。效果以下。android

效果看完,下面解释撸代码的时候了。

读取歌词文件

我使用的歌词文件时网易云音乐的歌词文件,结构以下截图:

能够比较明显的看出这是一个json数据。其中【00:00.00】表示的是【分:秒.毫秒】的形式。知道歌词结构后就开始解析数据了。

歌词解析

先将歌词文件存放到android studio下的assets文件夹下,固然放到手机内存中也行,读获得就行。下面是读取文件的代码。
try {
            //读取assets文件夹下名字为86357的文件
            InputStream lrycis = getResources().getAssets().open("86357");
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(lrycis));
            //结束标记符
            boolean eof = false;
            //临时保存的行内容
            String line = null;
            StringBuffer stringBuffer = new StringBuffer();
            while (!eof){
                line = bufferedReader.readLine();
                if (line == null){
                    eof = true;
                }else {
                    stringBuffer.append(line);
                }
            }
            //操做完成后记得关闭输入流,释放内存
            bufferedReader.close();
            lrycis.close();
            Gson gson = new Gson();
            //将歌词装换成对应的beam类,方便获取内容
            SongLyric songLyric = gson.fromJson(stringBuffer.toString(),SongLyric.class);
            //每个\n表示一行歌词,根据这个结构能够解析出全部的行
            String[] lyricArray = songLyric.getLyric().split("\n");
            lryList = new ArrayList<>();
            for (int i=0; i<lyricArray.length; i++){
                String ly = lyricArray[i];
                //获取分钟
                String min = lyricArray[i].substring(1,ly.indexOf(":"));
                //获取秒钟
                String second = lyricArray[i].substring(ly.indexOf(":")+1,ly.indexOf("."));
                //获取毫秒
                String minSecond = lyricArray[i].substring(ly.indexOf(".")+1,ly.indexOf("]"));
                //获取歌词
                String strLy = ly.substring(ly.indexOf("]")+1);
                //计算歌词起点的毫秒数
                int allTime = (Integer.parseInt(min)*60*1000+Integer.parseInt(second)*1000+Integer.parseInt(minSecond));
                //将歌词和起点装到集合中去,用歌词不经常使用到的%做为分隔符
                lryList.add(allTime+"%"+strLy);
            }
            musicLrycisView.setLys(lryList);
        } catch (IOException e) {
            e.printStackTrace();
            Log.e("日志","错误日志:"+e.getLocalizedMessage());
        }
复制代码

读取完文本后须要利用gson把文本转换成一个beam类,经过beam类获取到歌词的具体内容。为了在使用歌词比较容易获取到歌词对应的时间点,在后面统一把时间装换成毫秒了。时间的解析就是经过是【分:秒.毫】这个结构来解析的。解析完后将全部歌词保存到集合中去。读取文件属于耗时操做,放到子线程操做好点。json

自定义歌词view

逻辑梳理:自定义view因具有点击快进,歌词与音频同步,选中歌词部分颜色高亮这三个功能。这三个功能实现前提是歌词能够正常显示出来(废话)。有了这四个步骤后开始构造自定义viewcanvas

  • 歌词正常显示
    这个步骤比较容易实现。先自定义一个view,继承自AppCompatTextView。而后在类里面新建一个方法,updateTimeByIndex(int index,int type),index为歌词所在索引,后面快进,倒退及歌词自动同步音频时会用到,type表示操做类型,类型包括快进和倒退两种。定义好重写view的onSizeChanged(int w, int h, int oldw, int oldh)方法,在这里获取view的高度的一半,用于使歌词选中部分始终保持在view的中间位置。都写好后开始进入正题。讲太多没用,先给代码。
@Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        halfHeight = this.getHeight()/2;
        Log.e("日志","高度为:"+halfHeight);
    }
复制代码
/**
     * 更新歌词显示,相比与上面的方法,此方法时在快进或倒退时使用的。
     * @param index 当前歌词在歌词集合中的位置
     */
    public void updateTimeByIndex(int index){
        //当type为-1时,不容许外部赋值
        if (this.type != -1){
            defaultMove = halfHeight-paint.measureText("1")*3f*index;
            invalidate();
        }
    }
复制代码
@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.translate(0,defaultMove);

        //经过行数计算歌词总高度
        lryHeight = lys.size() * paint.measureText("1")*3f;
        //计算每行高度,测量“1”目的是获取单个字符的高度,由于单个字符占用的空间时正方形。
        singleHeight = paint.measureText("1")*3f;
        index = Math.abs((int)((defaultMove - halfHeight)/singleHeight));
        if (index > lys.size()-1){
            index = lys.size()-1;
        }


        for (int i=0;i<lys.size(); i++){

            if (i != index){
                paint.setColor(unselectLrcTextColor);
            }else {
                paint.setColor(selectLrcTextColor);

                rect.set( (int)(getWidth()/2-paint.measureText(lys.get(i).split("%")[1].trim())),
                        (int)((paint.measureText("1")*(i)*3f- AppUtils.dip2px(getContext(),7))),
                        (int)(getWidth()/2+paint.measureText(lys.get(i).split("%")[1].trim())),
                        (int)(paint.measureText("1")*(i)*3f+paint.measureText("1")+AppUtils.dip2px(getContext(),7))
                );

            }
            //getWidth()/2-paint.measureText(lys.get(i).split("%")[1].trim())/2使文字居中显示
            canvas.drawText(lys.get(i).split("%")[1].trim(),getWidth()/2-paint.measureText(lys.get(i).split("%")[1].trim())/2,paint.measureText("1")*(i)*3f,paint);
        }
    }
复制代码

正常显示歌词直接使用for语句循环遍历一下传入的歌词集合就行。for循环完后看到的效果多是下面的效果。 bash


开时部分没有被移动到屏幕中间,这时候上面代码的 halfHeight就派上用场了,这个值时view高度的通常,绘制内容时可使用这个值使总体内容下移半个view高度。

  • 选中歌词高亮
    这部分主要经过比较index来实现。当循环遍历的i值与计算出来的index值相同时,说明此处时歌词应该显示高亮的位置,这是改变一下颜色就能够了。代码就是上面贴出代码中onDraw方法中的for循环部分。app

  • 歌词与音频同步
    这里的实现是在歌曲音频时间发生改变时调用一次updateTimeByIndex方法进行同步操做。updateTimeByIndex中的index其实是和ondraw方法中的index是相同的。为何我还要在ondraw方法中重写计算index呢?由于在拖动歌词进行快进倒退时,我只能够经过计算方式去获取index,为了两边统一,因此我直接使用的index是在ondraw方法中计算出来的index。updateTimeByIndex中的index只用于计算歌词应该下滑的距离。下面贴出外部调用updateTimeByIndex方法的代码。ide

public void setMusicLry(int position){
        //旧的上一步歌曲时间减去如今的歌曲时间大于2秒,被认为是快进了,快进执行快进操做
        int lryIndex = 0;
        for (int i=0;i<lryList.size();i++){
            if (position > Integer.parseInt(lryList.get(i).split("%")[0]) && i+1 > lryList.size()-1){
                lryIndex = lryList.size() - 1;
                break;
            }else if (position > Integer.parseInt(lryList.get(i).split("%")[0]) && position < Integer.parseInt(lryList.get(i+1).split("%")[0])){
                lryIndex = i;
                break;
            }
        }
            musicLrycisView.updateTimeByIndex(lryIndex); }
        //设置当行当前歌词的显示
        lycText.setText(lryList.get(lryIndex).split("%")[1]);
    }
复制代码

position是歌曲当前的时间(单位:毫秒)。先在for循环中找出在此时间段内的歌词,而后调用updateTimeByIndex方法进行同步操做。post

  • 实现拖动进行倒退,快进功能。
/**
     * 设置手势,上划快进,下拉倒退。
     * @param event
     * @return
     */
    private float downY;
    private long clickTime = 0;
    private boolean isClick = false;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                clickTime = System.currentTimeMillis();
                downY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                //活动后设置歌词为不可同步状态,直到3秒后同步设置为可同步状态。此目的是为了防止户刚滑动到某处时外部对歌词view从新赋值后当前歌词有跳转会当前歌词而不是用户滑动歌词的地方
                float tempMove = defaultMove + (event.getY() - downY)/ AppUtils.dip2px(getContext(),6);
                if (tempMove < halfHeight){
                    defaultMove = defaultMove + (event.getY() - downY)/ AppUtils.dip2px(getContext(),6);
                }else if (tempMove > halfHeight){
                    defaultMove = halfHeight;
                }else if (tempMove == halfHeight){
                    return false;
                }
                //用户手指移动5dp内可认为是点击,固然,是不是点击还得结合点击时间判断
                if (Math.abs(downY - event.getY())> AppUtils.dip2px(getContext(),5)){
                    isClick = false;
                    type = -1;
                }else {
                    isClick = true;
                }
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                Log.e("日志","是否点击:"+isClick);
                //当用户手指重点击屏幕到手指离开屏幕时间小于200毫秒时,被认为时点击时间。写这个方法目的时解决重写onTouchEvent后整个view的点击失效问题。
                if (System.currentTimeMillis() - clickTime <= 200 &&  isClick && type != -1){
                    clickTime = 0;
                    performClick();
                    type = 0;
                    isClick = false;
                    downY = 0;
                    return false;
                }
                //保证是点击事件才执行,否者用户在手指移动到矩阵内并抬手就快进了。
                if (rect.contains((int)event.getX(),(int) (index*singleHeight)) && System.currentTimeMillis() - clickTime <= 200 && isClick && type == -1){
                    if (clickLryListen != null){
                        if (index+1 > lys.size()-1){
                            clickLryListen.sendToProgress(Integer.parseInt(lys.get(lys.size()-1).split("%")[0]));
                        }else if (index+1 < lys.size()-1){
                            clickLryListen.sendToProgress(Integer.parseInt(lys.get(index).split("%")[0]));
                        }
                        //Log.e("日志","矩阵内容:"+rect.left+","+rect.right+","+rect.top+","+rect.bottom);
                        //Log.e("日志","点击位点为:"+(int)(event.getY()+Math.abs(defaultMove))+",行数:"+index);
                    }else {
                        Log.e("日志","请先调用setClickLrcListen(int time)初始化监听器");
                    }
                }
                //这些值使用后必定要恢复默认值
                isClick = false;
                clickTime = 0;
                downY = 0;
                //在未设置mssage时,message默认值为0,下面语句目的是清除以前设置的全部延时任务
                //三秒后自动改成可同步歌曲进度状态
                handler.removeMessages(0);
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        type = 0;
                    }
                },2000);
                break;
        }
        return true;
    }
复制代码

当用户手指接触屏幕瞬间须要记录下点击的坐标及点击发生时间,这两个值分别时downY和clickTime,downY用于计算手指滑动距离,clickTime经过用户手指抬起时间计算用户点击屏幕总时间,用于判断触摸屏幕时时想滑动屏幕仍是进行点击操做。isClick值为true时表示用户的动做可能时点击事件,为false时动做确定不是点击事件。isClick判断依据时MotionEvent.ACTION_MOVE时用户手指移动的距离决定的。当用户手指滑动距离小于5dp内时,能够认为用户多是想进行点击操做,是否是还须要结合用户点击屏幕的时间及手指离开屏幕的时间的时间差来共同决定。在滑动事件中,咱们须要先将type赋值为-1,避免在拖动过程当中外部调用updateTimeByIndex方法进行歌词同步操做。在滑动事件中,咱们还须要为defaultMove赋值,目的是给ondraw中计算出具体的index,这个index就是被选中歌词的索引。

ui

在MotionEvent.ACTION_UP事件中,咱们须要判断用户操做时拖动仍是点击,当isClick为true且手指停留在屏幕上的事件小于200毫秒时,能够认为这是一个点击事件。这里我还多加了一个type的判断,type为-1时,表示歌词状态还处于不可同步状态,此时点击view,将会是执行快进,倒退功能。若是type为0,表示view处于可同步状态,此时点击屏幕就和平时的setOnClickListen()后设置的点击响应同样。 clickLryListen.sendToProgress(Integer.parseInt(lys.get(lys.size()-1).split("%")[0]));这句话就是调用外部的方法进行快进倒退用的。执行完这些操做后,后面的值必须回复默认,不然影响下功能的实现。固然也要将view的状态设置为可同步状态。我设定的时间时手指离开屏幕后两秒后自动恢复view为可同步歌词状态。this

结束语

这个自定义view其实仍是挺简单的,只是提及来有点复杂,上面说的也有点乱,因此看起来也有点烦,不过若是仔细看完后,本身编写出这个view应该也是没问题的。spa

相关文章
相关标签/搜索