android手机上实现歌词同步
最近在做一款android手机上的音乐播放器,学习到了很多东西,像是Fragment,ActionBar的使用等等,这里就先介绍一下歌词同步的实现问题。
歌词同步的实现思路很简单:获取歌词文件LRC中的时间和歌词内容,然后在指定的时间内播放相应的内容。获取不难,难就在于如何在手机屏幕上实现歌词的滚动。
先上效果图:
先从最基本的读取歌词文件开始:
public class LrcHandle { private List<String> mWords = new ArrayList<String>(); private List<Integer> mTimeList = new ArrayList<Integer>(); //处理歌词文件 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 List<String> getWords() { return mWords; } public List<Integer> getTime() { 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(timeHandler(str.substring(1,
str.length() - 1)));
}
}
}
一般歌词文件的格式大概如下:
[ar:艺人名]
[ti:曲名]
[al:专辑名]
[by:编者(指编辑LRC歌词的人)]
[offset:时间补偿值] 其单位是毫秒,正值表示整体提前,负值相反。这是用于总体调整显示快慢的。
但也不一定,有时候并没有前面那些ar:等标识符,所以我们这里也提供了另一种解析方式。
歌词文件中的时间格式则比较统一:[00:00.50]等等,00:表示分钟,00.表示秒数,.50表示毫秒数,当然,我们最后是要将它们转化为毫秒数处理才比较方便。
处理完歌词文件并得到我们想要的数据后,我们就要考虑如何在手机上滚动显示我们的歌词并且与我们得到的时间同步了。
先是布局文件:
<?xml version="1.0" encoding="utf-8"?> <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" /> </RelativeLayout>
WordView是自定义的TextView,它继承自TextView:
public class WordView extends TextView { private List<String> mWordsList = new ArrayList<String>(); 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<Integer> 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(); } }
歌词的显示需要重新开启一个线程,因为主线程是播放歌曲的。
代码很简单,功能也很简单,最主要的是多多尝试,多多修改,就能明白代码的原理了。
因为本人是菜鸟,讲得并不好,更多是贴出源码好让大家可以方便运行查看效果。