自定義音樂播放器的歌詞顯示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

相關文章
相關標籤/搜索