淘特cms
当前位置:淘特CMS->帮助中心->常见问题
android里教你如何制作一个音乐同步显示歌词的应用
  • 作者:
  • 日期:2016/4/12 11:05:27
  • 出处:淘特CMS
  • 点击:

最近在做一款android手机上的英语听力播放器,学习到了很多东西,像是Fragment,ActionBar的使用等等,这里就先介绍一下歌词同步的实现问题。

歌词同步的实现思路很简单:获取歌词文件LRC中的时间和歌词内容,然后在指定的时间内播放相应的内容。获取不难,难就在于如何在手机屏幕上实现歌词的滚动。

先上效果图:


我先准备一个歌词文件,先大概看一看里面的内容:

[ti:兄弟干杯] 
[ar:秦博] 
[t_time:(03:20)] 
[00:00.00] 兄弟干杯
[00:02.50] 作词:秦博
[00:05.00] 作曲:周宏涛
[00:07.50] 演唱:秦博
[00:10.00] 编曲:龙奔
[00:12.50] RAP词:刘闯 黄永军
[00:15.00] LRC BY :吉时雨
[00:17.50] QQ:132 7269 041
[00:20.00] www.cnLyric.com
[00:25.18] 男人在社会 酒一定要学会
[00:29.25] 烦了累了失恋了 只要你来陪
[00:33.46] 酒这东西咱别说那贱与贵
[00:37.62] 酸甜苦辣倒进杯 忘了是与非
[00:41.79] 现在的社会 美女是一大堆
[00:45.87] 不富不帅不理咱 不跟咱约会
[00:50.14] 人要活着就别让那心太累
[00:54.26] 金钱名利淡如水 不如啊醉一回
[00:58.52] 来来来 兄弟 干了这一杯
[01:02.62] 谁大谁小无所谓 有酒今朝醉
[01:06.80] 来来来 兄弟 干了这一杯
[01:10.94] 出门在外不容易 辛酸讲给谁
[01:15.14] 来来来 兄弟 干了这一杯
[01:19.29] 别管天南和地北 此时最陶醉
[01:23.50] 来来来 兄弟 干了这一杯
[01:27.65] 酒逢知己千杯少 人生醉几回
[01:31.95] RAP:深夜街头来买醉 究竟为了谁
[01:36.02] 看破红尘的卑微 口是与心非
[01:40.14] 种种挫折无所谓 勇敢来面对
[01:44.30] 不枉人间走一回 男儿不流泪
[01:48.60] 现在的社会 美女是一大堆
[01:52.75] 不富不帅不理咱 不跟咱约会
[01:56.88] 人要活着就别让那心太累
[02:01.03] 金钱名利淡如水 不如啊醉一回
[02:05.30] 来来来 兄弟 干了这一杯
[02:09.39] 谁大谁小无所谓 有酒今朝醉
[02:13.58] 来来来 兄弟 干了这一杯
[02:17.77] 出门在外不容易 辛酸讲给谁
[02:21.94] 来来来 兄弟 干了这一杯
[02:26.11] 别管天南和地北 此时最陶醉
[02:30.28] 来来来 兄弟 干了这一杯
[02:34.42] 酒逢知己千杯少 人生醉几回
[02:38.65] 来来来 兄弟 干了这一杯
[02:42.79] 谁大谁小无所谓 有酒今朝醉
[02:46.96] 来来来 兄弟 干了这一杯
[02:51.16] 出门在外不容易 辛酸讲给谁
[02:55.34] 来来来 兄弟 干了这一杯
[02:59.46] 别管天南和地北 此时最陶醉
[03:03.63] 来来来 兄弟 干了这一杯
[03:07.83] 酒逢知己千杯少 人生醉几回
看到了,每段歌词前面都有一个开始时间,那一段的时间就是这一段的结束时间,那原理明白了,就开始准备代码。

先从最基本的读取歌词文件开始:

Public class LrcHandle {
    private ListmWords = new ArrayList();
    private ListmTimeList = new ArrayList();

    //处理歌词文件
    public void readLRC(String path) {
        File file = new File(path);

        try {
            FileInputStream fileInputStream = new FileInputStream(file);
            InputStreamReader inputStreamReader = new InputStreamReader(
                    fileInputStream, "utf-8");
            BufferedReader bufferedReader = new BufferedReader(
                    inputStreamReader);
            String s = "";
            while ((s = bufferedReader.readLine()) != null) {
                addTimeToList(s);
                if ((s.indexOf("[ar:") != -1) || (s.indexOf("[ti:") != -1)
                        || (s.indexOf("[by:") != -1)) {
                    s = s.substring(s.indexOf(":") + 1, s.indexOf("]"));
                } else {
                    String ss = s.substring(s.indexOf("["), s.indexOf("]") + 1);
                    s = s.replace(ss, "");
                }
                mWords.add(s);
            }

            bufferedReader.close();
            inputStreamReader.close();
            fileInputStream.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            mWords.add("没有歌词文件,赶紧去下载");
        } catch (IOException e) {
            e.printStackTrace();
            mWords.add("没有读取到歌词");
        }
    }
   public ListgetWords() {
        return mWords;
   }

    public ListgetTime() {
        return mTimeList;
    }

    // 分离出时间
    private int timeHandler(String string) {
       string = string.replace(".", ":");
       String timeData[] = string.split(":");
// 分离出分、秒并转换为整型
        int minute = Integer.parseInt(timeData[0]);
        int second = Integer.parseInt(timeData[1]);
        int millisecond = Integer.parseInt(timeData[2]);

        // 计算上一行与下一行的时间转换为毫秒数
        int currentTime = (minute * 60 + second) * 1000 + millisecond * 10;

        return currentTime;
    }

   private void addTimeToList(String string) {
        Matcher matcher = Pattern.compile(
                "\\[\\d{1,2}:\\d{1,2}([\\.:]\\d{1,2})?\\]").matcher(string);
        if (matcher.find()) {
            String str = matcher.group();
            mTimeList.add(new LrcHandle().timeHandler(str.substring(1,
                    str.length() - 1)));
        }

    }
}

 一般歌词文件的格式大概如下:

[ar:艺人名]

[ti:曲名]

[al:专辑名]

[by:编者(指编辑LRC歌词的人)]

[offset:时间补偿值] 其单位是毫秒,正值表示整体提前,负值相反。这是用于总体调整显示快慢的。

但也不一定,有时候并没有前面那些ar:等标识符,所以我们这里也提供了另一种解析方式。

歌词文件中的时间格式则比较统一:[00:00.50]等等,00:表示分钟,00.表示秒数,.50表示毫秒数,当然,我们最后是要将它们转化为毫秒数处理才比较方便。

处理完歌词文件并得到我们想要的数据后,我们就要考虑如何在手机上滚动显示我们的歌词并且与我们得到的时间同步了。

先是布局文件:


<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=".MainActivity" >
<Button
android:id="@+id/button"
android:layout_width="60dip"
android:layout_height="60dip"
android:text="@string/停止" />
<com.example.slidechange.WordView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/button" />

WordView是自定义的TextView,它继承自TextView:

public class WordView extends TextView {
private List mWordsList = new ArrayList();
private Paint mLoseFocusPaint;
private Paint mOnFocusePaint;
private float mX = 0;
private float mMiddleY = 0;
private float mY = 0;
private static final int DY = 50;
private int mIndex = 0;
public WordView(Context context) throws IOException {
super(context);
init();
}
public WordView(Context context, AttributeSet attrs) throws IOException {
super(context, attrs);
init();
}
public WordView(Context context, AttributeSet attrs, int defStyle)
throws IOException {
super(context, attrs, defStyle);
init();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(Color.BLACK);
Paint p = mLoseFocusPaint;
p.setTextAlign(Paint.Align.CENTER);
Paint p2 = mOnFocusePaint;
p2.setTextAlign(Paint.Align.CENTER);
canvas.drawText(mWordsList.get(mIndex), mX, mMiddleY, p2);
int alphaValue = 25;
float tempY = mMiddleY;
for (int i = mIndex - 1; i >= 0; i--) {
tempY -= DY;
if (tempY < 0) {
break;
}
p.setColor(Color.argb(255 - alphaValue, 245, 245, 245));
canvas.drawText(mWordsList.get(i), mX, tempY, p);
alphaValue += 25;
}
alphaValue = 25;
tempY = mMiddleY;
for (int i = mIndex + 1, len = mWordsList.size(); i < len; i++) {
tempY += DY;
if (tempY > mY) {
break;
}
p.setColor(Color.argb(255 - alphaValue, 245, 245, 245));
canvas.drawText(mWordsList.get(i), mX, tempY, p);
alphaValue += 25;
}
mIndex++;
}
@Override
protected void onSizeChanged(int w, int h, int ow, int oh) {
super.onSizeChanged(w, h, ow, oh);
mX = w * 0.5f;
mY = h;
mMiddleY = h * 0.3f;
}
@SuppressLint("SdCardPath")
private void init() throws IOException {
setFocusable(true);
LrcHandle lrcHandler = new LrcHandle();
lrcHandler.readLRC("/sdcard/陪我去流浪.lrc");
mWordsList = lrcHandler.getWords();
mLoseFocusPaint = new Paint();
mLoseFocusPaint.setAntiAlias(true);
mLoseFocusPaint.setTextSize(22);
mLoseFocusPaint.setColor(Color.WHITE);
mLoseFocusPaint.setTypeface(Typeface.SERIF);
mOnFocusePaint = new Paint();
mOnFocusePaint.setAntiAlias(true);
mOnFocusePaint.setColor(Color.YELLOW);
mOnFocusePaint.setTextSize(30);
mOnFocusePaint.setTypeface(Typeface.SANS_SERIF);
}
}

最主要的是覆盖TextView的onDraw()和onSizeChanged()。

在onDraw()中我们重新绘制TextView,这就是实现歌词滚动实现的关键。歌词滚动的实现思路并不复杂:将上一句歌词向上移动,当前歌词字体变大,颜色变黄突出显示。

我们需要设置位移量DY = 50。颜色和字体大小我们可以通过设置Paint来实现。

我们注意到,在我实现的效果中,距离当前歌词越远的歌词,就会变透明,这个可以通过p.setColor(Color.argb(255 - alphaValue, 245, 245, 245))来实现。

接着就是主代码:

public class MainActivity extends Activity {
private WordView mWordView;
private List mTimeList;
private MediaPlayer mPlayer;
@SuppressLint("SdCardPath")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mPlayer.stop();
finish();
}
});
mWordView = (WordView) findViewById(R.id.text);
mPlayer = new MediaPlayer();
mPlayer.reset();
LrcHandle lrcHandler = new LrcHandle();
try {
lrcHandler.readLRC("/sdcard/陪我去流浪.lrc");
mTimeList = lrcHandler.getTime();
mPlayer.setDataSource("/sdcard/陪我去流浪.mp3");
mPlayer.prepare();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
}
final Handler handler = new Handler();
mPlayer.start();
new Thread(new Runnable() {
int i = 0;
@Override
public void run() {
while (mPlayer.isPlaying()) {
handler.post(new Runnable() {
@Override
public void run() {
mWordView.invalidate();
}
});
try {
Thread.sleep(mTimeList.get(i + 1) - mTimeList.get(i));
} catch (InterruptedException e) {
}
i++;
if (i == mTimeList.size() - 1) {
mPlayer.stop();
break;
}
}
}
}).start();
}
}

歌词的显示需要重新开启一个线程,因为主线程是播放歌曲的。

代码很简单,功能也很简单,最主要的是多多尝试,多多修改,就能明白代码的原理了。

最新评论
用 户:
内 容:
验证码: