Android 音乐播放器的开发教程(九) 歌词的显示----- 小达

时间:2022-03-07 10:15:43

歌词的显示


        现在的播放器已经能够切歌咯,进度条也可以*的滑动,有没有觉得很爽滑~~~~Android 音乐播放器的开发教程(九) 歌词的显示----- 小达,今天就来介绍怎么将歌词显示到屏幕上面,歌词的文件形式有很多种,例如lrc,trc,krc,,我手机上面是天天动听的播放器,其歌词的形式为.trc的,所以今天我们以这个为例,lrc是最简单解析的,下面第一章图就是TRC的格式,第二张为LRC格式的歌词,不难发现,TRC就是在LRC基础上对每个字都加了时间,更加精确.

,Android 音乐播放器的开发教程(九) 歌词的显示----- 小达,Android 音乐播放器的开发教程(九) 歌词的显示----- 小达


        显示歌词的原理就是,自定义一个View,将歌词文件取出来,每行每行的显示出来,对比当前歌曲的播放进度与歌词前面标注的时间来判定已经播放到了哪句话, 将当前的那句话用不同的颜色标出来.


        这里小达用了一个专门显示歌词的Fragment,来存放那个自定义的View,Fragment在前面也有讲过,所以这里直接给出源代码.

fragment_play.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.dada.myapplication.PlayFragment">

<com.example.dada.myapplication.LyricView //显示歌词的自定义View
android:id="@+id/lrcShowView"
android:layout_width="match_parent"
android:layout_height="300dip" />

<ImageButton
android:id="@+id/dismiss_lyric_button" //回退到主Fragment的按钮
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:background="@drawable/arrow_down"/>

<ImageButton

android:id="@+id/my_favorite_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="#00000000"
android:layout_alignBottom="@+id/dismiss_lyric_button"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true" />

</RelativeLayout>

PlayFragment.xml

package com.example.dada.myapplication;

import android.app.Activity;
import android.os.Bundle;
import android.app.Fragment;
import android.os.Handler;
import android.os.Message;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.ListAdapter;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

public class PlayFragment extends Fragment {


private int duration;
private int index = 0;
private int currentTime;
private LyricView lyricView;
private Activity myActivity;
private LyricProgress lyricProgress;
private static String music_url = "";
private OnPlayFragmentInteractionListener mListener;
private List<LyricContent> lyricContents = new ArrayList<LyricContent>();


Runnable myRunnable = new Runnable(){
@Override
public void run() {
lyricView.setIndex(lyricIndex());
lyricView.invalidate(); //调用后自定义View会自动调用onDraw()方法来重新绘制歌词
            myHandler.postDelayed(myRunnable, 300);        }    };    Handler myHandler = new Handler(){                          //通过用myHandler来不断改变歌词的显示,达到同步的效果        @Override        public void handleMessage(Message msg) {            super.handleMessage(msg);        }    };    public static PlayFragment newInstance(String url) {        PlayFragment fragment = new PlayFragment();        Bundle args = new Bundle();        music_url = url;        args.putString("url",url);        fragment.setArguments(args);        return fragment;    }    public PlayFragment() {        // Required empty public constructor    }    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);    }    @Override    public View onCreateView(LayoutInflater inflater, ViewGroup container,                             Bundle savedInstanceState) {        myActivity = getActivity();        View rootView = inflater.inflate(R.layout.fragment_play, container, false);        initialize(rootView);             //自定义的初始化函数        return rootView;    }    protected void initialize(View v){        lyricView = (LyricView)v.findViewById(R.id.lrcShowView);        (v.findViewById(R.id.dismiss_lyric_button))                .setOnClickListener(new View.OnClickListener() {                    @Override                    public void onClick(View v) {                        mListener.onPlayFragmentInteraction(AppConstant.PlayerMsg.DISMISS_CLICK);                    }                });        initLyric(music_url);                          //初始化歌词        myHandler.post(myRunnable);    //将myRunnable传给handler来执行    }    // TODO: Rename method, update argument and hook method into UI event    public void onButtonPressed() {    }    @Override    public void onAttach(Activity activity) {        super.onAttach(activity);        try {            mListener = (OnPlayFragmentInteractionListener) activity;        } catch (ClassCastException e) {            throw new ClassCastException(activity.toString()                    + " must implement OnFragmentInteractionListener");        }    }    @Override    public void onDetach() {        super.onDetach();        mListener = null;    }    public interface OnPlayFragmentInteractionListener {                         //不要忘了在Activity中实现这个接口,重写这个里面包含的回调函数.        // TODO: Update argument type and name        public void onPlayFragmentInteraction(int message);    }    public void initLyric(String url) {                                                             //里面包含了一些即将要介绍的自定义类        lyricProgress = new LyricProgress();        lyricProgress.readLyric(url);        lyricContents = lyricProgress.getLyricList();        try{            lyricView.setMyLyricList(lyricContents);        }        catch(Exception e){            e.printStackTrace();        }        myHandler.post(myRunnable);    }    public int lyricIndex() {                                     //用来寻找当前歌曲播放的位置,返回当前歌词的索引值        int size = lyricContents.size();        if(PlayerService.mediaPlayer.isPlaying()) {            currentTime = PlayerService.mediaPlayer.getCurrentPosition();            duration = PlayerService.mediaPlayer.getDuration();        }        if(currentTime < duration) {            for (int i = 0; i < size; i++) {                if (i < size - 1) {                    if (currentTime < lyricContents.get(i).getLyricTime() && i==0) {                        index = i;                        break;                    }                    if (currentTime > lyricContents.get(i).getLyricTime()                            && currentTime < lyricContents.get(i + 1).getLyricTime()) {                        index = i;                        break;                    }                }                if (i == size - 1                        && currentTime > lyricContents.get(i).getLyricTime()) {                    index = i;                    break;                }            }        }        return index;    }}


上面定义了一个Fragment,这个Fragment在用户点击Activity中的控制台时,显示出来,所以需要给Activity最下面的布局文件添加一个按钮监听,代码如下:

 public void main_activity_bottom_layout_listener(View v){

String current_music_url = mp3Infos.get(music_position).getUrl();

PlayFragment playFragment = PlayFragment.newInstance(current_music_url);


FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.replace(R.id.fragment_layout, playFragment);
fragmentTransaction.addToBackStack(null);

fragmentTransaction.commit();

}


里面包含了自定义组件LyricView,下面是自定义组件的代码:

LyricView.java:

package com.example.dada.myapplication;


import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.widget.TextView;


import java.util.ArrayList;
import java.util.List;


public class LyricView extends TextView{
    private float width;
    private float height;
    private Paint currentPaint;             //用来描绘当前正在播放的那句歌词
    private Paint notCurrentPaint;          //用来描绘非当前歌词
    private float textHeight = 50;
    private float textSize = 30;
    private int index = 0;                  //当前歌词的索引


    /*
    观察歌词文件发现,每句话都对应着一个时间
    所以专门写一个类LyricContent.java
    后面马上介绍到,来存放时间和该时间对应的歌词
    然后再用一个List将很多这个类的实例包裹起来
    这样就能很好的将每句歌词和他们的时间对应起来
     */
    private List<LyricContent> myLyricList = null;        //每个LyricCOntent对应着一句话,这个List就是整个解析后的歌词文件


    public void setIndex(int index){
        this.index = index;
    }


    public void setMyLyricList(List<LyricContent> lyricList){
        this.myLyricList = lyricList;
    }


    public List<LyricContent> getMyLyricList(){
        return this.myLyricList;
    }


    public LyricView(Context context){
        super(context);
        init();
    }


    public LyricView(Context context,AttributeSet attributeSet){
        super(context,attributeSet);
        init();
    }


    public LyricView(Context context,AttributeSet attributeSet,int defSytle){
        super(context,attributeSet,defSytle);
        init();
    }


    private void init(){                            //初始化画笔
        setFocusable(true);


        currentPaint = new Paint();
        currentPaint.setAntiAlias(true);
        currentPaint.setTextAlign(Paint.Align.CENTER);


        notCurrentPaint = new Paint();
        notCurrentPaint.setAntiAlias(true);
        notCurrentPaint.setTextAlign(Paint.Align.CENTER);


    }
    
    /*
    onDraw()就是画歌词的主要方法了
    在PlayFragment中会不停地调用
    lyricView.invalidate();这个方法
    此方法写在了一个Runnable的run()函数中
    通过不断的给一个handler发送消息,不断的重新绘制歌词
    来达到歌词同步的效果
     */


    @Override
    protected void onDraw(Canvas canvas){
        super.onDraw(canvas);
        if(canvas == null){
            return ;
        }


        currentPaint.setColor(getResources().getColor(R.color.greenyellow));
        notCurrentPaint.setColor(getResources().getColor(R.color.rosybrown));


        currentPaint.setTextSize(40);
        currentPaint.setTypeface(Typeface.DEFAULT_BOLD);


        notCurrentPaint.setTextSize(textSize);
        notCurrentPaint.setTypeface(Typeface.DEFAULT);


        try{
            setText("");


            float tempY = height / 2;                                                                     //画出之前的句子
            for(int i =index - 1;i >= 0; i --){
                tempY -= textHeight;
                canvas.drawText(myLyricList.get(i).getLyricString(),width/2,tempY,notCurrentPaint);
            }
            canvas.drawText(myLyricList.get(index).getLyricString(),width/2,height/2,currentPaint);       //画出当前的句子


            tempY = height / 2;                                                                           //画出之后的句子
            for(int i =index + 1;i<myLyricList.size(); i ++){
                tempY += textHeight;
                canvas.drawText(myLyricList.get(i).getLyricString(),width/2,tempY,notCurrentPaint);
            }


        }
        catch(Exception e){
            setText("一丁点儿歌词都没找到,下载后再来找我把.......");
        }
    }


    @Override
    protected void onSizeChanged(int w,int h,int oldW,int oldH){
        super.onSizeChanged(w,h,oldW,oldH);
        this.width = w;
        this.height = h;
    }
}

上面提到过一个封装歌词的类,里面包含着歌词对应的时间以及内容,下面给出源代码,

LyricContent.java:

package com.example.dada.myapplication;


public class LyricContent {
private String lyricString; //歌词的内容
private int lyricTime; //歌词当前的时间

public String getLyricString(){
return this.lyricString;
}

public void setLyricString(String str){
this.lyricString = str;
}

public int getLyricTime(){
return this.lyricTime;
}

public void setLyricTime(int time){
this.lyricTime = time;
}
}

还需要一个类来将歌词解析出来,转换时间,解析格式等功能,

LyricProgress.java:

package com.example.dada.myapplication;


import android.os.PatternMatcher;
import android.provider.ContactsContract;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class LyricProgress {

private List<LyricContent> lyricList;
private LyricContent myLyricContent;

public LyricProgress(){
myLyricContent = new LyricContent();
lyricList = new ArrayList<LyricContent>();
}

public String readLyric(String path){ //从文件中读出歌词并解析的函数
StringBuilder stringBuilder = new StringBuilder();
path = path.replace("song","lyric"); //这个是针对天天动听的目录结构下手的,,,不知道有没有什么适合所有文件结构的方法呢..
File f = new File(path.replace(".mp3",".trc"));

try{
FileInputStream fis = new FileInputStream(f);
InputStreamReader isr = new InputStreamReader(fis,"utf-8");
BufferedReader br = new BufferedReader(isr);
String s= "";

while((s = br.readLine()) != null){
s = s.replace("[","");
s = s.replace("]","@"); //每一句话的分隔符

s = s.replaceAll("<[0-9]{3,5}>",""); //去掉每个字的时间标签,这里用到了正则表达式


String spiltLrcData[] = s.split("@");

if(spiltLrcData.length > 1){

myLyricContent.setLyricString(spiltLrcData[1]); //将每句话创建一个类的实例,歌词和对应时间赋值
int lycTime = time2Str(spiltLrcData[0]);
myLyricContent.setLyricTime(lycTime);
lyricList.add(myLyricContent);

myLyricContent = new LyricContent();
}
}
}
catch(FileNotFoundException e){
e.printStackTrace();
stringBuilder.append("一丁点儿歌词都没找到,下载后再来找我把.......");
}
catch(IOException e){
e.printStackTrace();
stringBuilder.append("没有读取到歌词.....");
}
return stringBuilder.toString();
}

public int time2Str(String timeStr){ //将分:秒:毫秒转化为长整型的数
timeStr = timeStr.replace(":",".");
timeStr = timeStr.replace(".","@");

String timeData[] = timeStr.split("@");

int min = Integer.parseInt(timeData[0]);
int sec = Integer.parseInt(timeData[1]);
int millSec = Integer.parseInt(timeData[2]);

int currentTime = (min * 60 + sec) * 1000 + millSec * 10;
return currentTime;
}

public List<LyricContent> getLyricList(){
return this.lyricList;
}
}

上面的注释不知道写的清楚不,如果还有什么问题看不懂的话,直接问我就好咯,这个歌词解析我用的方法好笨的感觉,,而且有些歌词还不能应对,比如KRC格式的歌词,(在显示歌词之前确保已经存在歌词文件,不然什么都木有的,,,),下面是做出来的效果:

Android 音乐播放器的开发教程(九) 歌词的显示----- 小达  ,嘿嘿,今天的歌词显示就讲到这里咯,到下篇博客的时候,小达将介绍哈通知栏的显示,也就是Notification的应用,可以在下拉的控制台上控制我们的播放器切歌和暂停哟~~~~Android 音乐播放器的开发教程(九) 歌词的显示----- 小达,88