編輯:關於Android編程
最近有個項目有關於播放音樂時候,關於歌詞有以下幾個功能:
1、實現歌詞同步滾動的功能,即歌曲播放到哪句歌詞,就高亮地顯示出正在播放的這個歌詞;
2、實現上下拖動歌詞時候,可以拖動播放器的進度。即可以不停地上下拖動歌詞,當手指離開屏幕時候 即從當前拖動到的歌詞位置播放。
3、實現歌詞的字體大小可以進行縮放的功能。即雙指在屏幕進行縮放操作時,歌詞的字體大小也進行相應的縮放操作。
下面我將這幾個功能做成一個demo來展示給大家。首先來看看這個demo的具體實現效果,如下面幾幅圖所示。
圖1、同步滾動歌詞
圖2、上下拖動歌詞1
圖3、上下拖動歌詞2
圖4、縮放歌詞
圖5、歌詞顯示(較大字體)
圖6、歌詞顯示(較小字體)
圖7、歌詞滾動時候,高亮地畫出正滾動到的歌詞內容以及歌詞的開始時間,並該句歌詞下面畫出一條直線
lrc是英文lyric(歌詞)的縮寫,被用做歌詞文件的擴展名。以lrc為擴展名的歌詞文件可以在各類數碼播放器中同步顯示。
先來看一份標准的LRC歌詞文件,下面展示的是王力宏的《依然愛你》的lrc歌詞的內容
[ti:依然愛你]
[ar:王力宏]
[al:火力全開 新歌+精選]
[by:歐陽鵬]
[00:01.17]一閃一閃亮晶晶 留下歲月的痕跡
[00:07.29]我的世界的重心 依然還是你
[00:13.37]一年一年又一年 飛逝盡在一轉眼
[00:20.29]唯一永遠不改變 是不停的改變
[00:27.14]我不像從前的自己 你也有點不像你
[00:33.36]但在我眼中你的笑 依然的美麗
[00:39.53]這次只能往前走 一個方向順時鐘
[00:46.12]不知道還要多久 所以要讓你懂
[00:51.82]我依然愛你 就是唯一的退路
[00:57.36]我依然珍惜 時時刻刻的幸福
[01:04.65]你每個呼吸 每個動作 每個表情
[01:11.43]到最後一定會依然愛你
[01:18.08]依然愛你 依然愛你
[01:25.58]我不像從前的自己 你也有點不像你
[01:31.52]但在我眼中你的笑 依然的美麗
[01:37.61]這次只能往前走 一個方向順時鐘
[01:44.42]不知道還要多久 所以要讓你懂
[01:50.18]我依然愛你 就是唯一的退路
[01:55.65]我依然珍惜 時時刻刻的幸福
[02:02.84]你每個呼吸 每個動作 每個表情
[02:09.77]到最後一定會依然愛你
[02:15.61]
[02:17.61]lrc制作:http://blog.csdn.net/ouyang_peng 歐陽鵬
[02:25.61]
[02:31.06]依然愛你 依然愛你
[02:36.63]
[02:42.32]我依然愛你 或許是命中注定
[02:47.70]多年之後 任何人都無法代替
[02:54.57]那些時光 是我這一輩子 最美好
[03:01.84]那些回憶 依然無法忘記
[03:07.88]我依然愛你 就是唯一的退路
[03:13.95]我依然珍惜 時時刻刻的幸福
[03:21.32]你每個呼吸 每個動作 每個表情
[03:28.20]到最後一定會依然愛你
[03:34.76]你每個呼吸 每個動作 每個表情
[03:42.04]到永遠一定會依然愛你
[03:53.28]
[04:01.28]
lrc歌詞文本中含有兩類標簽:一是標識標簽 ,二是時間標簽。
標識標簽,其格式為“[標識名:值]”,主要包含以下預定義的標簽:
[ar:歌手名] [ti:歌曲名] [al:專輯名] [by:編輯者(指lrc歌詞的制作人)] [offset:時間補償值] (其單位是毫秒,正值表示整體提前,負值相反。這是用於總體調整顯示快慢的,但多數的MP3可能不會支持這種標簽)。
時間標簽,形式為“[mm:ss]”或“[mm:ss.ff]”(分鐘數:秒數.毫秒數),數字須為非負整數,
比如”[12:34.50]”是有效的,而”[0x0C:-34.50]”無效。
時間標簽需位於某行歌詞中的句首部分,一行歌詞可以包含多個時間標簽
(比如歌詞中的迭句部分)。當歌曲播放到達某一時間點時,MP3就會尋找對應的時間標簽並顯示標簽後面的歌詞文本,這樣就完成了“歌詞同步”的功能。
例如下面的這首 草蜢的《失戀戰線聯盟》,就是一行歌詞包含了多個時間標簽。
[ti:失戀戰線聯盟]
[ar:草蜢]
[al:]
[00:00.00]草蜢-失戀戰線聯盟
[00:08.78]編輯:小婧
[01:43.33][00:16.27]她總是只留下電話號碼
[01:46.97][00:19.81]從不肯讓我送她回家
[01:50.61][00:23.43]聽說你也曾經愛上過她
[01:54.15][00:27.07]曾經也同樣無法自拔
[01:57.78][00:30.72]你說你學不會假裝潇灑
[02:01.41][00:34.36]卻叫我別太早放棄她
[02:05.05][00:37.99]把過去傳說成一段神話
[02:08.70][00:41.59]然後笑你是一樣的傻
[02:12.01][00:45.11]我們那麼在乎她
[02:14.15][00:47.01]卻被她全部抹殺
[02:15.96][00:48.87]越談她越相信永遠得不到回答
[02:19.57][00:52.49]到底她怎麼想
[02:21.35][00:54.28]應該繼續在這麼
[02:23.37][00:56.36]還是說穿跑了吧
[02:26.89][00:59.80]找一個承認失戀的方法
[02:30.48][01:03.41]讓心情好好地放個假
[02:34.14][01:07.00]當你我不小心又想起她
[02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
[02:48.69]
[01:33.58]編輯:小婧
[01:43.33][00:16.27]她總是只留下電話號碼
上面這行歌詞表示:在 [00:16.27] 這個時間點播放 “她總是只留下電話號碼” 這句歌詞,
在 [01:43.33] 這個時間點再一個播放 “她總是只留下電話號碼” 這句歌詞。
其實可以把上面這行歌詞拆分為下面兩句歌詞:
[00:16.27]她總是只留下電話號碼
[01:43.33]她總是只留下電話號碼
/**
* 從assets目錄下讀取歌詞文件內容
* @param fileName
* @return
*/
public String getFromAssets(String fileName){
try {
InputStreamReader inputReader = new InputStreamReader( getResources().getAssets().open(fileName) );
BufferedReader bufReader = new BufferedReader(inputReader);
String line="";
String result="";
while((line = bufReader.readLine()) != null){
if(line.trim().equals(""))
continue;
result += line + "\r\n";
}
return result;
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
例如:從assets目錄下讀取test.lrc歌詞文件內容,則可以調用上面的getFromAssets(String fileName)方法得到歌詞的文本內容,如下所示:
String lrc = getFromAssets("test.lrc");
首先封裝一個表示每行歌詞內容的實體類LrcRow,該類由三個屬性,分別為:
strTime、time、content。
strTime表示該行歌詞要開始播放的時間,格式如下:[02:34.14]例如一行歌詞內容為:[02:34.14]當你我不小心又想起她 , 解析該行歌詞後的實體類LrcRow的屬性如下所示:
time表示將strTime轉換為long型之後的數值
例如將strTime為[02:34.14]格式轉換154014(154014=02 * 60 * 1000 + 34 * 1000+14)
content表示該行歌詞的內容,如:當你我不小心又想起她
代碼如下:
package com.oyp.lrc.view.impl;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* 歌詞行
* 包括該行歌詞的時間,歌詞內容
*/
public class LrcRow implements Comparable{
public final static String TAG = "LrcRow";
/** 該行歌詞要開始播放的時間,格式如下:[02:34.14] */
public String strTime;
/** 該行歌詞要開始播放的時間,由[02:34.14]格式轉換為long型,
* 即將2分34秒14毫秒都轉為毫秒後 得到的long型值:time=02*60*1000+34*1000+14
*/
public long time;
/** 該行歌詞的內容 */
public String content;
public LrcRow(){}
public LrcRow(String strTime,long time,String content){
this.strTime = strTime;
this.time = time;
this.content = content;
// Log.d(TAG,"strTime:" + strTime + " time:" + time + " content:" + content);
}
@Override
public String toString() {
return "[" + strTime + " ]" + content;
}
/**
* 讀取歌詞的每一行內容,轉換為LrcRow,加入到集合中
*/
public static List createRows(String standardLrcLine){
/**
一行歌詞只有一個時間的 例如:徐佳瑩 《我好想你》
[01:15.33]我好想你 好想你
一行歌詞有多個時間的 例如:草蜢 《失戀戰線聯盟》
[02:34.14][01:07.00]當你我不小心又想起她
[02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
**/
try{
if(standardLrcLine.indexOf("[") != 0 || standardLrcLine.indexOf("]") != 9 ){
return null;
}
//[02:34.14][01:07.00]當你我不小心又想起她
//找到最後一個 ‘]’ 的位置
int lastIndexOfRightBracket = standardLrcLine.lastIndexOf("]");
//歌詞內容就是 ‘]’ 的位置之後的文本 eg: 當你我不小心又想起她
String content = standardLrcLine.substring(lastIndexOfRightBracket + 1, standardLrcLine.length());
//歌詞時間就是 ‘]’ 的位置之前的文本 eg: [02:34.14][01:07.00]
/**
將時間格式轉換一下 [mm:ss.SS][mm:ss.SS] 轉換為 -mm:ss.SS--mm:ss.SS-
即:[02:34.14][01:07.00] 轉換為 -02:34.14--01:07.00-
*/
String times = standardLrcLine.substring(0,lastIndexOfRightBracket + 1).replace("[", "-").replace("]", "-");
//通過 ‘-’ 來拆分字符串
String arrTimes[] = times.split("-");
List listTimes = new ArrayList();
for(String temp : arrTimes){
if(temp.trim().length() == 0){
continue;
}
/** [02:34.14][01:07.00]當你我不小心又想起她
*
上面的歌詞的就可以拆分為下面兩句歌詞了
[02:34.14]當你我不小心又想起她
[01:07.00]當你我不小心又想起她
*/
LrcRow lrcRow = new LrcRow(temp, timeConvert(temp), content);
listTimes.add(lrcRow);
}
return listTimes;
}catch(Exception e){
Log.e(TAG,"createRows exception:" + e.getMessage());
return null;
}
}
/**
* 將解析得到的表示時間的字符轉化為Long型
*/
private static long timeConvert(String timeString){
//因為給如的字符串的時間格式為XX:XX.XX,返回的long要求是以毫秒為單位
//將字符串 XX:XX.XX 轉換為 XX:XX:XX
timeString = timeString.replace('.', ':');
//將字符串 XX:XX:XX 拆分
String[] times = timeString.split(":");
// mm:ss:SS
return Integer.valueOf(times[0]) * 60 * 1000 +//分
Integer.valueOf(times[1]) * 1000 +//秒
Integer.valueOf(times[2]) ;//毫秒
}
/**
* 排序的時候,根據歌詞的時間來排序
*/
public int compareTo(LrcRow another) {
return (int)(this.time - another.time);
}
}
該LrcRow的List createRows(String standardLrcLine)方法 ,將循環地一行一行的去讀取歌詞的內容。然後對每一行的歌詞進行解析,每解析出一個時間標簽[XX:XX.XX]則new出一個LrcRow對象,然後加入到歌詞行List集合中去。
該LrcRow類實現Comparable接口,用來進行解析之後的排序操作,排序按時間從小到大排序。
定義一個ILrcBuilder接口,接口有一個List getLrcRows(String rawLrc)方法,該方法用來解析歌詞,得到LrcRow的集合
package com.oyp.lrc.view;
import com.oyp.lrc.view.impl.LrcRow;
import java.util.List;
/**
* 解析歌詞,得到LrcRow的集合
*/
public interface ILrcBuilder {
List getLrcRows(String rawLrc);
}
DefaultLrcBuilder實現ILrcBuilder接口,List getLrcRows(String rawLrc)方法會循環地讀取歌詞的每一行,然後調用LrcRow類的List createRows(String standardLrcLine)方法,得到解析每一行歌詞之後的LrcRow集合,再將每一行得到LrcRow集合中得到的LrcRow實體加入一個總 的到LrcRow集合rows中去,然後將rows集合根據歌詞行的時間排序,得到排序後的LrcRow集合,該集合就是最終的解析歌詞後的內容了。
代碼如下:
package com.oyp.lrc.view.impl;
import android.util.Log;
import com.oyp.lrc.view.ILrcBuilder;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 解析歌詞,得到LrcRow的集合
*/
public class DefaultLrcBuilder implements ILrcBuilder {
static final String TAG = "DefaultLrcBuilder";
public List getLrcRows(String rawLrc) {
Log.d(TAG,"getLrcRows by rawString");
if(rawLrc == null || rawLrc.length() == 0){
Log.e(TAG,"getLrcRows rawLrc null or empty");
return null;
}
StringReader reader = new StringReader(rawLrc);
BufferedReader br = new BufferedReader(reader);
String line = null;
List rows = new ArrayList();
try{
//循環地讀取歌詞的每一行
do{
line = br.readLine();
/**
一行歌詞只有一個時間的 例如:徐佳瑩 《我好想你》
[01:15.33]我好想你 好想你
一行歌詞有多個時間的 例如:草蜢 《失戀戰線聯盟》
[02:34.14][01:07.00]當你我不小心又想起她
[02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
**/
Log.d(TAG,"lrc raw line: " + line);
if(line != null && line.length() > 0){
//解析每一行歌詞 得到每行歌詞的集合,因為有些歌詞重復有多個時間,就可以解析出多個歌詞行來
List lrcRows = LrcRow.createRows(line);
if(lrcRows != null && lrcRows.size() > 0){
for(LrcRow row : lrcRows){
rows.add(row);
}
}
}
}while(line != null);
if( rows.size() > 0 ){
// 根據歌詞行的時間排序
Collections.sort(rows);
if(rows!=null&&rows.size()>0){
for(LrcRow lrcRow:rows){
Log.d(TAG, "lrcRow:" + lrcRow.toString());
}
}
}
}catch(Exception e){
Log.e(TAG,"parse exceptioned:" + e.getMessage());
return null;
}finally{
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
reader.close();
}
return rows;
}
}
例如:通過下面代碼來調用ILrcBuilder解析歌詞,
//從assets目錄下讀取歌詞文件內容
String lrc = getFromAssets("test.lrc");
//解析歌詞構造器
ILrcBuilder builder = new DefaultLrcBuilder();
//解析歌詞返回LrcRow集合
List rows = builder.getLrcRows(lrc);
草蜢的《失戀戰線聯盟》,lrc原始內容如下:
[ti:失戀戰線聯盟]
[ar:草蜢]
[al:]
[00:00.00]草蜢-失戀戰線聯盟
[00:08.78]編輯:小婧
[01:43.33][00:16.27]她總是只留下電話號碼
[01:46.97][00:19.81]從不肯讓我送她回家
[01:50.61][00:23.43]聽說你也曾經愛上過她
[01:54.15][00:27.07]曾經也同樣無法自拔
[01:57.78][00:30.72]你說你學不會假裝潇灑
[02:01.41][00:34.36]卻叫我別太早放棄她
[02:05.05][00:37.99]把過去傳說成一段神話
[02:08.70][00:41.59]然後笑你是一樣的傻
[02:12.01][00:45.11]我們那麼在乎她
[02:14.15][00:47.01]卻被她全部抹殺
[02:15.96][00:48.87]越談她越相信永遠得不到回答
[02:19.57][00:52.49]到底她怎麼想
[02:21.35][00:54.28]應該繼續在這麼
[02:23.37][00:56.36]還是說穿跑了吧
[02:26.89][00:59.80]找一個承認失戀的方法
[02:30.48][01:03.41]讓心情好好地放個假
[02:34.14][01:07.00]當你我不小心又想起她
[02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
[02:48.69]
[01:33.58]編輯:小婧
讀取該歌詞內容,過程中的打印日志為:
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [ti:失戀戰線聯盟]
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [ar:草蜢]
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [al:]
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [00:00.00]草蜢-失戀戰線聯盟
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [00:08.78]編輯:小婧
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:43.33][00:16.27]她總是只留下電話號碼
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:46.97][00:19.81]從不肯讓我送她回家
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:50.61][00:23.43]聽說你也曾經愛上過她
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:54.15][00:27.07]曾經也同樣無法自拔
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:57.78][00:30.72]你說你學不會假裝潇灑
03-06 00:41:15.352 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:01.41][00:34.36]卻叫我別太早放棄她
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:05.05][00:37.99]把過去傳說成一段神話
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:08.70][00:41.59]然後笑你是一樣的傻
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:12.01][00:45.11]我們那麼在乎她
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:14.15][00:47.01]卻被她全部抹殺
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:15.96][00:48.87]越談她越相信永遠得不到回答
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:19.57][00:52.49]到底她怎麼想
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:21.35][00:54.28]應該繼續在這麼
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:23.37][00:56.36]還是說穿跑了吧
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:26.89][00:59.80]找一個承認失戀的方法
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:30.48][01:03.41]讓心情好好地放個假
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:34.14][01:07.00]當你我不小心又想起她
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:45.69][02:42.20][02:37.69][01:10.60]就在記憶裡畫一個叉
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [02:48.69]
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: [01:33.58]編輯:小婧
03-06 00:41:15.362 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrc raw line: null
解析歌詞後遍歷List集合的打印日志為:
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:00.00 ]草蜢-失戀戰線聯盟
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:08.78 ]編輯:小婧
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:16.27 ]她總是只留下電話號碼
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:19.81 ]從不肯讓我送她回家
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:23.43 ]聽說你也曾經愛上過她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:27.07 ]曾經也同樣無法自拔
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:30.72 ]你說你學不會假裝潇灑
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:34.36 ]卻叫我別太早放棄她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:37.99 ]把過去傳說成一段神話
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:41.59 ]然後笑你是一樣的傻
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:45.11 ]我們那麼在乎她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:47.01 ]卻被她全部抹殺
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:48.87 ]越談她越相信永遠得不到回答
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:52.49 ]到底她怎麼想
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:54.28 ]應該繼續在這麼
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:56.36 ]還是說穿跑了吧
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[00:59.80 ]找一個承認失戀的方法
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:03.41 ]讓心情好好地放個假
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:07.00 ]當你我不小心又想起她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:10.60 ]就在記憶裡畫一個叉
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:33.58 ]編輯:小婧
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:43.33 ]她總是只留下電話號碼
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:46.97 ]從不肯讓我送她回家
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:50.61 ]聽說你也曾經愛上過她
03-06 00:41:15.372 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:54.15 ]曾經也同樣無法自拔
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[01:57.78 ]你說你學不會假裝潇灑
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:01.41 ]卻叫我別太早放棄她
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:05.05 ]把過去傳說成一段神話
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:08.70 ]然後笑你是一樣的傻
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:12.01 ]我們那麼在乎她
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:14.15 ]卻被她全部抹殺
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:15.96 ]越談她越相信永遠得不到回答
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:19.57 ]到底她怎麼想
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:21.35 ]應該繼續在這麼
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:23.37 ]還是說穿跑了吧
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:26.89 ]找一個承認失戀的方法
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:30.48 ]讓心情好好地放個假
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:34.14 ]當你我不小心又想起她
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:37.69 ]就在記憶裡畫一個叉
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:42.20 ]就在記憶裡畫一個叉
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:45.69 ]就在記憶裡畫一個叉
03-06 00:41:15.382 5265-5265/com.oyp.lrc D/DefaultLrcBuilder: lrcRow:[02:48.69 ]
即,草蜢的《失戀戰線聯盟》的lrc歌詞解析完後的內容如下:
[00:00.00 ]草蜢-失戀戰線聯盟
[00:08.78 ]編輯:小婧
[00:16.27 ]她總是只留下電話號碼
[00:19.81 ]從不肯讓我送她回家
[00:23.43 ]聽說你也曾經愛上過她
[00:27.07 ]曾經也同樣無法自拔
[00:30.72 ]你說你學不會假裝潇灑
[00:34.36 ]卻叫我別太早放棄她
[00:37.99 ]把過去傳說成一段神話
[00:41.59 ]然後笑你是一樣的傻
[00:45.11 ]我們那麼在乎她
[00:47.01 ]卻被她全部抹殺
[00:48.87 ]越談她越相信永遠得不到回答
[00:52.49 ]到底她怎麼想
[00:54.28 ]應該繼續在這麼
[00:56.36 ]還是說穿跑了吧
[00:59.80 ]找一個承認失戀的方法
[01:03.41 ]讓心情好好地放個假
[01:07.00 ]當你我不小心又想起她
[01:10.60 ]就在記憶裡畫一個叉
[01:33.58 ]編輯:小婧
[01:43.33 ]她總是只留下電話號碼
[01:46.97 ]從不肯讓我送她回家
[01:50.61 ]聽說你也曾經愛上過她
[01:54.15 ]曾經也同樣無法自拔
[01:57.78 ]你說你學不會假裝潇灑
[02:01.41 ]卻叫我別太早放棄她
[02:05.05 ]把過去傳說成一段神話
[02:08.70 ]然後笑你是一樣的傻
[02:12.01 ]我們那麼在乎她
[02:14.15 ]卻被她全部抹殺
[02:15.96 ]越談她越相信永遠得不到回答
[02:19.57 ]到底她怎麼想
[02:21.35 ]應該繼續在這麼
[02:23.37 ]還是說穿跑了吧
[02:26.89 ]找一個承認失戀的方法
[02:30.48 ]讓心情好好地放個假
[02:34.14 ]當你我不小心又想起她
[02:37.69 ]就在記憶裡畫一個叉
[02:42.20 ]就在記憶裡畫一個叉
[02:45.69 ]就在記憶裡畫一個叉
[02:48.69 ]
下面是解析歌詞前後的對比圖
至此,歌詞解析完畢!<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxociAvPg0KPGgxIGlkPQ=="三顯示lrc歌詞內容">三、顯示LRC歌詞內容
ILrcViewListener接口,該接口定義了一個onLrcSeeked方法用來監聽用戶上下拖動歌詞的動作定義了一個方法
onLrcSeeked(int newPosition, LrcRow row)當歌詞被用戶上下拖動的時候回調該方法
package com.oyp.lrc.view;
import com.oyp.lrc.view.impl.LrcRow;
/**
* 歌詞拖動時候的監聽類
*/
public interface ILrcViewListener {
/**
* 當歌詞被用戶上下拖動的時候回調該方法
*/
void onLrcSeeked(int newPosition, LrcRow row);
}
ILrcView接口接口,定義了三個方法
setLrc(List lrcRows)調用該方法設置要展示的歌詞行集合
seekLrcToTime(long time)
音樂播放的時候調用該方法滾動歌詞,高亮正在播放的那句歌詞
setListener(ILrcViewListener l)
調用該方法設設置歌詞拖動時候的監聽類,用以回調ILrcViewListener的onLrcSeeked(int newPosition, LrcRow row)方法
package com.oyp.lrc.view;
import com.oyp.lrc.view.impl.LrcRow;
import java.util.List;
/**
* 展示歌詞的接口
*/
public interface ILrcView {
/**
* 設置要展示的歌詞行集合
*/
void setLrc(List lrcRows);
/**
* 音樂播放的時候調用該方法滾動歌詞,高亮正在播放的那句歌詞
*/
void seekLrcToTime(long time);
/**
* 設置歌詞拖動時候的監聽類
*/
void setListener(ILrcViewListener l);
}
自定義一個LrcView,該LrcView繼承android.view.View對象,實現了ILrcView接口。該自定義LrcView可以實現了同步顯示歌詞,拖動歌詞,縮放歌詞等功能。
首先來說說顯示歌詞的實現思路,要顯示歌詞即把歌詞的內容繪制出來,可以分以下三步來繪制歌詞:
第1步:高亮地畫出正在播放的那句歌詞
第2步:畫出正在播放的那句歌詞的上面可以展示出來的歌詞
第3步:畫出正在播放的那句歌詞的下面的可以展示出來的歌詞
重寫onDraw(Canvas canvas)方法,在方法中按照上面的思路來繪制者三部分的歌詞。代碼如下:
@Override
protected void onDraw(Canvas canvas) {
final int height = getHeight(); // height of this view
final int width = getWidth(); // width of this view
//當沒有歌詞的時候
if (mLrcRows == null || mLrcRows.size() == 0) {
if (mLoadingLrcTip != null) {
// draw tip when no lrc.
mPaint.setColor(mHignlightRowColor);
mPaint.setTextSize(mLrcFontSize);
mPaint.setTextAlign(Align.CENTER);
canvas.drawText(mLoadingLrcTip, width / 2, height / 2 - mLrcFontSize, mPaint);
}
return;
}
int rowY = 0; // vertical point of each row.
final int rowX = width / 2;
int rowNum = 0;
/**
* 分以下三步來繪制歌詞:
*
* 第1步:高亮地畫出正在播放的那句歌詞
* 第2步:畫出正在播放的那句歌詞的上面可以展示出來的歌詞
* 第3步:畫出正在播放的那句歌詞的下面的可以展示出來的歌詞
*/
// 1、 高亮地畫出正在要高亮的的那句歌詞
String highlightText = mLrcRows.get(mHignlightRow).content;
int highlightRowY = height / 2 - mLrcFontSize;
mPaint.setColor(mHignlightRowColor);
mPaint.setTextSize(mLrcFontSize);
mPaint.setTextAlign(Align.CENTER);
canvas.drawText(highlightText, rowX, highlightRowY, mPaint);
// 上下拖動歌詞的時候 畫出拖動要高亮的那句歌詞的時間 和 高亮的那句歌詞下面的一條直線
if (mDisplayMode == DISPLAY_MODE_SEEK) {
// 畫出高亮的那句歌詞下面的一條直線
mPaint.setColor(mSeekLineColor);
//該直線的x坐標從0到屏幕寬度 y坐標為高亮歌詞和下一行歌詞中間
canvas.drawLine(mSeekLinePaddingX, highlightRowY + mPaddingY, width - mSeekLinePaddingX, highlightRowY + mPaddingY, mPaint);
// 畫出高亮的那句歌詞的時間
mPaint.setColor(mSeekLineTextColor);
mPaint.setTextSize(mSeekLineTextSize);
mPaint.setTextAlign(Align.LEFT);
canvas.drawText(mLrcRows.get(mHignlightRow).strTime, 0, highlightRowY, mPaint);
}
// 2、畫出正在播放的那句歌詞的上面可以展示出來的歌詞
mPaint.setColor(mNormalRowColor);
mPaint.setTextSize(mLrcFontSize);
mPaint.setTextAlign(Align.CENTER);
rowNum = mHignlightRow - 1;
rowY = highlightRowY - mPaddingY - mLrcFontSize;
//只畫出正在播放的那句歌詞的上一句歌詞
// if (rowY > -mLrcFontSize && rowNum >= 0) {
// String text = mLrcRows.get(rowNum).content;
// canvas.drawText(text, rowX, rowY, mPaint);
// }
//畫出正在播放的那句歌詞的上面所有的歌詞
while( rowY > -mLrcFontSize && rowNum >= 0){
String text = mLrcRows.get(rowNum).content;
canvas.drawText(text, rowX, rowY, mPaint);
rowY -= (mPaddingY + mLrcFontSize);
rowNum --;
}
// 3、畫出正在播放的那句歌詞的下面的可以展示出來的歌詞
rowNum = mHignlightRow + 1;
rowY = highlightRowY + mPaddingY + mLrcFontSize;
//只畫出正在播放的那句歌詞的下一句歌詞
// if (rowY < height && rowNum < mLrcRows.size()) {
// String text2 = mLrcRows.get(rowNum).content;
// canvas.drawText(text2, rowX, rowY, mPaint);
// }
//畫出正在播放的那句歌詞的所有下面的可以展示出來的歌詞
while( rowY < height && rowNum < mLrcRows.size()){
String text = mLrcRows.get(rowNum).content;
canvas.drawText(text, rowX, rowY, mPaint);
rowY += (mPaddingY + mLrcFontSize);
rowNum ++;
}
}
為了實現同步顯示功能的功能,則需要不停地將自定義的LrcView進行重繪。首先當MediaPlayer開始播放的時候,同步的啟動一個TimerTask來進行歌詞的滾動操作。如代碼所示:
mPlayer.setOnPreparedListener(new OnPreparedListener() {
//准備完畢
public void onPrepared(MediaPlayer mp) {
mp.start();
if(mTimer == null){
mTimer = new Timer();
mTask = new LrcTask();
mTimer.scheduleAtFixedRate(mTask, 0, mPalyTimerDuration);
}
}
});
上面代碼的意思是,當MediaPlayer開始播放的時候,啟動一個定時器Timer,然後通過這個定時器每隔mPalyTimerDuration時間來執行一次LrcTask任務。LrcTask的代碼如下:
/**
* 展示歌曲的定時任務
*/
class LrcTask extends TimerTask{
@Override
public void run() {
//獲取歌曲播放的位置
final long timePassed = mPlayer.getCurrentPosition();
MainActivity.this.runOnUiThread(new Runnable() {
public void run() {
//滾動歌詞
mLrcView.seekLrcToTime(timePassed);
}
});
}
};
上面的代碼是:首先獲取MediaPlayer的播放進度值,然後調用了LrcView的seekLrcToTime(long time)方法進行歌詞同步滾動,LrcView的seekLrcToTime(long time)方法的實現代碼如下:
/**
* 播放的時候調用該方法滾動歌詞,高亮正在播放的那句歌詞
* @param time
*/
public void seekLrcToTime(long time) {
if (mLrcRows == null || mLrcRows.size() == 0) {
return;
}
if (mDisplayMode != DISPLAY_MODE_NORMAL) {
return;
}
Log.d(TAG, "seekLrcToTime:" + time);
for (int i = 0; i < mLrcRows.size(); i++) {
LrcRow current = mLrcRows.get(i);
LrcRow next = i + 1 == mLrcRows.size() ? null : mLrcRows.get(i + 1);
/**
* 正在播放的時間大於current行的歌詞的時間而小於next行歌詞的時間, 設置要高亮的行為current行
* 正在播放的時間大於current行的歌詞,而current行為最後一句歌詞時,設置要高亮的行為current行
*/
if ((time >= current.time && next != null && time < next.time)
|| (time > current.time && next == null)){
seekLrc(i, false);
return;
}
}
}
上面代碼意思是,首先通過傳入進來的MediaPlayer的播放進度值,來判斷需要高亮地歌詞行LrcRow是哪一行,然後調用seekLrc(int position, boolean cb)方法來進行歌詞重繪操作。seekLrc(int position, boolean cb)方法的實現如下所示:
/**
* 設置要高亮的歌詞為第幾行歌詞
*
* @param position 要高亮的歌詞行數
* @param cb 是否是手指拖動後要高亮的歌詞
*/
public void seekLrc(int position, boolean cb) {
if (mLrcRows == null || position < 0 || position > mLrcRows.size()) {
return;
}
LrcRow lrcRow = mLrcRows.get(position);
mHignlightRow = position;
invalidate();
//如果是手指拖動歌詞後
if (mLrcViewListener != null && cb) {
//回調onLrcSeeked方法,將音樂播放器播放的位置移動到高亮歌詞的位置
mLrcViewListener.onLrcSeeked(position, lrcRow);
}
}
上面方法是將要高亮的歌詞行設置為目前正在播放的歌詞行,然後重繪LrcView。
要實現拖動歌詞的功能,可以分為以下幾步來實現
1、給LrcView注冊一個ILrcViewListener監聽接口。
下面是LrcView注冊ILrcViewListener監聽的具體實現。
//設置自定義的LrcView上下拖動歌詞時監聽
mLrcView.setListener(new ILrcViewListener() {
//當歌詞被用戶上下拖動的時候回調該方法,從高亮的那一句歌詞開始播放
public void onLrcSeeked(int newPosition, LrcRow row) {
if (mPlayer != null) {
Log.d(TAG, "onLrcSeeked:" + row.time);
mPlayer.seekTo((int) row.time);
}
}
});
2、當歌詞進行拖動的時候,回調ILrcViewListener接口的onLrcSeeked(int newPosition, LrcRow row)方法。
如下面代碼所示:回調了onLrcSeeked(int newPosition, LrcRow row)方法。
/**
* 設置要高亮的歌詞為第幾行歌詞
*
* @param position 要高亮的歌詞行數
* @param cb 是否是手指拖動後要高亮的歌詞
*/
public void seekLrc(int position, boolean cb) {
if (mLrcRows == null || position < 0 || position > mLrcRows.size()) {
return;
}
LrcRow lrcRow = mLrcRows.get(position);
mHignlightRow = position;
invalidate();
//如果是手指拖動歌詞後
if (mLrcViewListener != null && cb) {
//回調onLrcSeeked方法,將音樂播放器播放的位置移動到高亮歌詞的位置
mLrcViewListener.onLrcSeeked(position, lrcRow);
}
}
3、判斷手指在屏幕上的操作,來進行歌詞滾動的操作。
重寫onTouchEvent(MotionEvent event)方法,來判斷手指的操作是拖動歌詞還是縮放歌詞。
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mLrcRows == null || mLrcRows.size() == 0) {
return super.onTouchEvent(event);
}
switch (event.getAction()) {
//手指按下
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "down,mLastMotionY:" + mLastMotionY);
mLastMotionY = event.getY();
mIsFirstMove = true;
invalidate();
break;
//手指移動
case MotionEvent.ACTION_MOVE:
if (event.getPointerCount() == 2) {
Log.d(TAG, "two move");
doScale(event);
return true;
}
Log.d(TAG, "one move");
// single pointer mode ,seek
//如果是雙指同時按下,進行歌詞大小縮放,抬起其中一個手指,另外一個手指不離開屏幕地移動的話,不做任何處理
if (mDisplayMode == DISPLAY_MODE_SCALE) {
//if scaling but pointer become not two ,do nothing.
return true;
}
//如果一個手指按下,在屏幕上移動的話,拖動歌詞上下
doSeek(event);
break;
case MotionEvent.ACTION_CANCEL:
//手指抬起
case MotionEvent.ACTION_UP:
if (mDisplayMode == DISPLAY_MODE_SEEK) {
//高亮手指抬起時的歌詞並播放從該句歌詞開始播放
seekLrc(mHignlightRow, true);
}
mDisplayMode = DISPLAY_MODE_NORMAL;
invalidate();
break;
}
return true;
}
如上代碼所示,當一個手指移動的時候,則調用doSeek(MotionEvent event)方法來進行拖動歌詞的操作,doSeek(MotionEvent event)方法的具體實現代碼如下:
/**
* 處理單指在屏幕移動時,歌詞上下滾動
*/
private void doSeek(MotionEvent event) {
float y = event.getY();//手指當前位置的y坐標
float offsetY = y - mLastMotionY; //第一次按下的y坐標和目前移動手指位置的y坐標之差
//如果移動距離小於10,不做任何處理
if (Math.abs(offsetY) < mMinSeekFiredOffset) {
return;
}
//將模式設置為拖動歌詞模式
mDisplayMode = DISPLAY_MODE_SEEK;
int rowOffset = Math.abs((int) offsetY / mLrcFontSize); //歌詞要滾動的行數
Log.d(TAG, "move to new hightlightrow : " + mHignlightRow + " offsetY: " + offsetY + " rowOffset:" + rowOffset);
if (offsetY < 0) {
//手指向上移動,歌詞向下滾動
mHignlightRow += rowOffset;//設置要高亮的歌詞為 當前高亮歌詞 向下滾動rowOffset行後的歌詞
} else if (offsetY > 0) {
//手指向下移動,歌詞向上滾動
mHignlightRow -= rowOffset;//設置要高亮的歌詞為 當前高亮歌詞 向上滾動rowOffset行後的歌詞
}
//設置要高亮的歌詞為0和mHignlightRow中的較大值,即如果mHignlightRow < 0,mHignlightRow=0
mHignlightRow = Math.max(0, mHignlightRow);
//設置要高亮的歌詞為0和mHignlightRow中的較小值,即如果mHignlight > RowmLrcRows.size()-1,mHignlightRow=mLrcRows.size()-1
mHignlightRow = Math.min(mHignlightRow, mLrcRows.size() - 1);
//如果歌詞要滾動的行數大於0,則重畫LrcView
if (rowOffset > 0) {
mLastMotionY = y;
invalidate();
}
}
如上面代碼所示,當一個手指不停的在屏幕上移動時,將會不停地調用doSeek(MotionEvent event)方法來進行LrcView的重繪操作,從而實現了歌詞拖動的效果。
當手指離開屏幕的時候,即MotionEvent 為MotionEvent.ACTION_UP的時候,會調用seekLrc(int position, boolean cb)方法,從而回調ILrcViewListener接口的onLrcSeeked方法,來拖動MediaPlayer的播放進度值,從而達到了拖動歌詞後從最終高亮的歌詞開始重新播放歌詞的功能。如下代碼所示:
case MotionEvent.ACTION_UP:
if (mDisplayMode == DISPLAY_MODE_SEEK) {
//高亮手指抬起時的歌詞並播放從該句歌詞開始播放
seekLrc(mHignlightRow, true);
}
mDisplayMode = DISPLAY_MODE_NORMAL;
invalidate();
break;
如onTouchEvent(MotionEvent event)方法中所示,當兩個手指在屏幕上移動的時候,調用doScale(MotionEvent event)方法來做縮放歌詞的功能。
case MotionEvent.ACTION_MOVE:
if (event.getPointerCount() == 2) {
Log.d(TAG, "two move");
doScale(event);
return true;
}
Log.d(TAG, "one move");
// single pointer mode ,seek
//如果是雙指同時按下,進行歌詞大小縮放,抬起其中一個手指,另外一個手指不離開屏幕地移動的話,不做任何處理
if (mDisplayMode == DISPLAY_MODE_SCALE) {
//if scaling but pointer become not two ,do nothing.
return true;
}
//如果一個手指按下,在屏幕上移動的話,拖動歌詞上下
doSeek(event);
break;
doScale(MotionEvent event)方法的具體實現代碼如下所示:
/**
* 處理雙指在屏幕移動時的,歌詞大小縮放
*/
private void doScale(MotionEvent event) {
//如果歌詞的模式為:拖動歌詞模式
if (mDisplayMode == DISPLAY_MODE_SEEK) {
//如果是單指按下,在進行歌詞上下滾動,然後按下另外一個手指,則把歌詞模式從 拖動歌詞模式 變為 縮放歌詞模式
mDisplayMode = DISPLAY_MODE_SCALE;
Log.d(TAG, "change mode from DISPLAY_MODE_SEEK to DISPLAY_MODE_SCALE");
return;
}
// two pointer mode , scale font
if (mIsFirstMove) {
mDisplayMode = DISPLAY_MODE_SCALE;
invalidate();
mIsFirstMove = false;
//兩個手指的x坐標和y坐標
setTwoPointerLocation(event);
}
//獲取歌詞大小要縮放的比例
int scaleSize = getScale(event);
Log.d(TAG, "scaleSize:" + scaleSize);
//如果縮放大小不等於0,進行縮放,重繪LrcView
if (scaleSize != 0) {
setNewFontSize(scaleSize);
invalidate();
}
setTwoPointerLocation(event);
}
如上代碼所示,當兩個手指第一次放在屏幕上時候,調用setTwoPointerLocation(MotionEvent event)方法來記錄兩個手指的x坐標和y坐標,setTwoPointerLocation(MotionEvent event)方法代碼如下所示:
/**
* 設置當前兩個手指的x坐標和y坐標
*/
private void setTwoPointerLocation(MotionEvent event) {
mPointerOneLastMotion.x = event.getX(0);
mPointerOneLastMotion.y = event.getY(0);
mPointerTwoLastMotion.x = event.getX(1);
mPointerTwoLastMotion.y = event.getY(1);
}
當兩個手指在屏幕上移動的時候,調用getScale(MotionEvent event)方法來對比兩個手指前後兩次的x坐標和y坐標,從而得到要縮放的比例scaleSize。getScale(MotionEvent event)方法具體實現如下所示:
/**
* 獲取歌詞大小要縮放的比例
*/
private int getScale(MotionEvent event) {
Log.d(TAG, "scaleSize getScale");
float x0 = event.getX(0);
float y0 = event.getY(0);
float x1 = event.getX(1);
float y1 = event.getY(1);
float maxOffset = 0; // max offset between x or y axis,used to decide scale size
boolean zoomin = false;
//第一次雙指之間的x坐標的差距
float oldXOffset = Math.abs(mPointerOneLastMotion.x - mPointerTwoLastMotion.x);
//第二次雙指之間的x坐標的差距
float newXoffset = Math.abs(x1 - x0);
//第一次雙指之間的y坐標的差距
float oldYOffset = Math.abs(mPointerOneLastMotion.y - mPointerTwoLastMotion.y);
//第二次雙指之間的y坐標的差距
float newYoffset = Math.abs(y1 - y0);
//雙指移動之後,判斷雙指之間移動的最大差距
maxOffset = Math.max(Math.abs(newXoffset - oldXOffset), Math.abs(newYoffset - oldYOffset));
//如果x坐標移動的多一些
if (maxOffset == Math.abs(newXoffset - oldXOffset)) {
//如果第二次雙指之間的x坐標的差距大於第一次雙指之間的x坐標的差距則是放大,反之則縮小
zoomin = newXoffset > oldXOffset ? true : false;
}
//如果y坐標移動的多一些
else {
//如果第二次雙指之間的y坐標的差距大於第一次雙指之間的y坐標的差距則是放大,反之則縮小
zoomin = newYoffset > oldYOffset ? true : false;
}
Log.d(TAG, "scaleSize maxOffset:" + maxOffset);
if (zoomin) {
return (int) (maxOffset / 10);//放大雙指之間移動的最大差距的1/10
} else {
return -(int) (maxOffset / 10);//縮小雙指之間移動的最大差距的1/10
}
}
當通過getScale(MotionEvent event)方法獲得了縮放比scaleSize後,調用setNewFontSize(int scaleSize)來設置歌詞的新的字體大小,然後重繪LrcView,從而實現了縮放歌詞的功能。
setNewFontSize(int scaleSize)方法的具體實現如下所示:
/**
* 設置縮放後的字體大小
*/
private void setNewFontSize(int scaleSize) {
//設置歌詞縮放後的的最新字體大小
mLrcFontSize += scaleSize;
mLrcFontSize = Math.max(mLrcFontSize, mMinLrcFontSize);
mLrcFontSize = Math.min(mLrcFontSize, mMaxLrcFontSize);
//設置顯示高亮的那句歌詞的時間最新字體大小
mSeekLineTextSize += scaleSize;
mSeekLineTextSize = Math.max(mSeekLineTextSize, mMinSeekLineTextSize);
mSeekLineTextSize = Math.min(mSeekLineTextSize, mMaxSeekLineTextSize);
}
至此,縮放功能已經實現。
package com.oyp.lrc.view.impl;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.oyp.lrc.view.ILrcView;
import com.oyp.lrc.view.ILrcViewListener;
import java.util.List;
/**
* 自定義LrcView,可以同步顯示歌詞,拖動歌詞,縮放歌詞
*/
public class LrcView extends View implements ILrcView {
public final static String TAG = "LrcView";
/**
* 正常歌詞模式
*/
public final static int DISPLAY_MODE_NORMAL = 0;
/**
* 拖動歌詞模式
*/
public final static int DISPLAY_MODE_SEEK = 1;
/**
* 縮放歌詞模式
*/
public final static int DISPLAY_MODE_SCALE = 2;
/**
* 歌詞的當前展示模式
*/
private int mDisplayMode = DISPLAY_MODE_NORMAL;
/**
* 歌詞集合,包含所有行的歌詞
*/
private List mLrcRows;
/**
* 最小移動的距離,當拖動歌詞時如果小於該距離不做處理
*/
private int mMinSeekFiredOffset = 10;
/**
* 當前高亮歌詞的行數
*/
private int mHignlightRow = 0;
/**
* 當前高亮歌詞的字體顏色為黃色
*/
private int mHignlightRowColor = Color.YELLOW;
/**
* 不高亮歌詞的字體顏色為白色
*/
private int mNormalRowColor = Color.WHITE;
/**
* 拖動歌詞時,在當前高亮歌詞下面的一條直線的字體顏色
**/
private int mSeekLineColor = Color.CYAN;
/**
* 拖動歌詞時,展示當前高亮歌詞的時間的字體顏色
**/
private int mSeekLineTextColor = Color.CYAN;
/**
* 拖動歌詞時,展示當前高亮歌詞的時間的字體大小默認值
**/
private int mSeekLineTextSize = 15;
/**
* 拖動歌詞時,展示當前高亮歌詞的時間的字體大小最小值
**/
private int mMinSeekLineTextSize = 13;
/**
* 拖動歌詞時,展示當前高亮歌詞的時間的字體大小最大值
**/
private int mMaxSeekLineTextSize = 18;
/**
* 歌詞字體大小默認值
**/
private int mLrcFontSize = 23; // font size of lrc
/**
* 歌詞字體大小最小值
**/
private int mMinLrcFontSize = 15;
/**
* 歌詞字體大小最大值
**/
private int mMaxLrcFontSize = 35;
/**
* 兩行歌詞之間的間距
**/
private int mPaddingY = 10;
/**
* 拖動歌詞時,在當前高亮歌詞下面的一條直線的起始位置
**/
private int mSeekLinePaddingX = 0;
/**
* 拖動歌詞的監聽類,回調LrcViewListener類的onLrcSeeked方法
**/
private ILrcViewListener mLrcViewListener;
/**
* 當沒有歌詞的時候展示的內容
**/
private String mLoadingLrcTip = "Downloading lrc...";
private Paint mPaint;
public LrcView(Context context, AttributeSet attr) {
super(context, attr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setTextSize(mLrcFontSize);
}
public void setListener(ILrcViewListener l) {
mLrcViewListener = l;
}
public void setLoadingTipText(String text) {
mLoadingLrcTip = text;
}
@Override
protected void onDraw(Canvas canvas) {
final int height = getHeight(); // height of this view
final int width = getWidth(); // width of this view
//當沒有歌詞的時候
if (mLrcRows == null || mLrcRows.size() == 0) {
if (mLoadingLrcTip != null) {
// draw tip when no lrc.
mPaint.setColor(mHignlightRowColor);
mPaint.setTextSize(mLrcFontSize);
mPaint.setTextAlign(Align.CENTER);
canvas.drawText(mLoadingLrcTip, width / 2, height / 2 - mLrcFontSize, mPaint);
}
return;
}
int rowY = 0; // vertical point of each row.
final int rowX = width / 2;
int rowNum = 0;
/**
* 分以下三步來繪制歌詞:
*
* 第1步:高亮地畫出正在播放的那句歌詞
* 第2步:畫出正在播放的那句歌詞的上面可以展示出來的歌詞
* 第3步:畫出正在播放的那句歌詞的下面的可以展示出來的歌詞
*/
// 1、 高亮地畫出正在要高亮的的那句歌詞
String highlightText = mLrcRows.get(mHignlightRow).content;
int highlightRowY = height / 2 - mLrcFontSize;
mPaint.setColor(mHignlightRowColor);
mPaint.setTextSize(mLrcFontSize);
mPaint.setTextAlign(Align.CENTER);
canvas.drawText(highlightText, rowX, highlightRowY, mPaint);
// 上下拖動歌詞的時候 畫出拖動要高亮的那句歌詞的時間 和 高亮的那句歌詞下面的一條直線
if (mDisplayMode == DISPLAY_MODE_SEEK) {
// 畫出高亮的那句歌詞下面的一條直線
mPaint.setColor(mSeekLineColor);
//該直線的x坐標從0到屏幕寬度 y坐標為高亮歌詞和下一行歌詞中間
canvas.drawLine(mSeekLinePaddingX, highlightRowY + mPaddingY, width - mSeekLinePaddingX, highlightRowY + mPaddingY, mPaint);
// 畫出高亮的那句歌詞的時間
mPaint.setColor(mSeekLineTextColor);
mPaint.setTextSize(mSeekLineTextSize);
mPaint.setTextAlign(Align.LEFT);
canvas.drawText(mLrcRows.get(mHignlightRow).strTime, 0, highlightRowY, mPaint);
}
// 2、畫出正在播放的那句歌詞的上面可以展示出來的歌詞
mPaint.setColor(mNormalRowColor);
mPaint.setTextSize(mLrcFontSize);
mPaint.setTextAlign(Align.CENTER);
rowNum = mHignlightRow - 1;
rowY = highlightRowY - mPaddingY - mLrcFontSize;
//只畫出正在播放的那句歌詞的上一句歌詞
// if (rowY > -mLrcFontSize && rowNum >= 0) {
// String text = mLrcRows.get(rowNum).content;
// canvas.drawText(text, rowX, rowY, mPaint);
// }
//畫出正在播放的那句歌詞的上面所有的歌詞
while( rowY > -mLrcFontSize && rowNum >= 0){
String text = mLrcRows.get(rowNum).content;
canvas.drawText(text, rowX, rowY, mPaint);
rowY -= (mPaddingY + mLrcFontSize);
rowNum --;
}
// 3、畫出正在播放的那句歌詞的下面的可以展示出來的歌詞
rowNum = mHignlightRow + 1;
rowY = highlightRowY + mPaddingY + mLrcFontSize;
//只畫出正在播放的那句歌詞的下一句歌詞
// if (rowY < height && rowNum < mLrcRows.size()) {
// String text2 = mLrcRows.get(rowNum).content;
// canvas.drawText(text2, rowX, rowY, mPaint);
// }
//畫出正在播放的那句歌詞的所有下面的可以展示出來的歌詞
while( rowY < height && rowNum < mLrcRows.size()){
String text = mLrcRows.get(rowNum).content;
canvas.drawText(text, rowX, rowY, mPaint);
rowY += (mPaddingY + mLrcFontSize);
rowNum ++;
}
}
/**
* 設置要高亮的歌詞為第幾行歌詞
*
* @param position 要高亮的歌詞行數
* @param cb 是否是手指拖動後要高亮的歌詞
*/
public void seekLrc(int position, boolean cb) {
if (mLrcRows == null || position < 0 || position > mLrcRows.size()) {
return;
}
LrcRow lrcRow = mLrcRows.get(position);
mHignlightRow = position;
invalidate();
//如果是手指拖動歌詞後
if (mLrcViewListener != null && cb) {
//回調onLrcSeeked方法,將音樂播放器播放的位置移動到高亮歌詞的位置
mLrcViewListener.onLrcSeeked(position, lrcRow);
}
}
private float mLastMotionY;
/**
* 第一個手指的坐標
**/
private PointF mPointerOneLastMotion = new PointF();
/**
* 第二個手指的坐標
**/
private PointF mPointerTwoLastMotion = new PointF();
/**
* 是否是第一次移動,當一個手指按下後開始移動的時候,設置為true,
* 當第二個手指按下的時候,即兩個手指同時移動的時候,設置為false
*/
private boolean mIsFirstMove = false;
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mLrcRows == null || mLrcRows.size() == 0) {
return super.onTouchEvent(event);
}
switch (event.getAction()) {
//手指按下
case MotionEvent.ACTION_DOWN:
Log.d(TAG, "down,mLastMotionY:" + mLastMotionY);
mLastMotionY = event.getY();
mIsFirstMove = true;
invalidate();
break;
//手指移動
case MotionEvent.ACTION_MOVE:
if (event.getPointerCount() == 2) {
Log.d(TAG, "two move");
doScale(event);
return true;
}
Log.d(TAG, "one move");
// single pointer mode ,seek
//如果是雙指同時按下,進行歌詞大小縮放,抬起其中一個手指,另外一個手指不離開屏幕地移動的話,不做任何處理
if (mDisplayMode == DISPLAY_MODE_SCALE) {
//if scaling but pointer become not two ,do nothing.
return true;
}
//如果一個手指按下,在屏幕上移動的話,拖動歌詞上下
doSeek(event);
break;
case MotionEvent.ACTION_CANCEL:
//手指抬起
case MotionEvent.ACTION_UP:
if (mDisplayMode == DISPLAY_MODE_SEEK) {
//高亮手指抬起時的歌詞並播放從該句歌詞開始播放
seekLrc(mHignlightRow, true);
}
mDisplayMode = DISPLAY_MODE_NORMAL;
invalidate();
break;
}
return true;
}
/**
* 處理雙指在屏幕移動時的,歌詞大小縮放
*/
private void doScale(MotionEvent event) {
//如果歌詞的模式為:拖動歌詞模式
if (mDisplayMode == DISPLAY_MODE_SEEK) {
//如果是單指按下,在進行歌詞上下滾動,然後按下另外一個手指,則把歌詞模式從 拖動歌詞模式 變為 縮放歌詞模式
mDisplayMode = DISPLAY_MODE_SCALE;
Log.d(TAG, "change mode from DISPLAY_MODE_SEEK to DISPLAY_MODE_SCALE");
return;
}
// two pointer mode , scale font
if (mIsFirstMove) {
mDisplayMode = DISPLAY_MODE_SCALE;
invalidate();
mIsFirstMove = false;
//兩個手指的x坐標和y坐標
setTwoPointerLocation(event);
}
//獲取歌詞大小要縮放的比例
int scaleSize = getScale(event);
Log.d(TAG, "scaleSize:" + scaleSize);
//如果縮放大小不等於0,進行縮放,重繪LrcView
if (scaleSize != 0) {
setNewFontSize(scaleSize);
invalidate();
}
setTwoPointerLocation(event);
}
/**
* 處理單指在屏幕移動時,歌詞上下滾動
*/
private void doSeek(MotionEvent event) {
float y = event.getY();//手指當前位置的y坐標
float offsetY = y - mLastMotionY; //第一次按下的y坐標和目前移動手指位置的y坐標之差
//如果移動距離小於10,不做任何處理
if (Math.abs(offsetY) < mMinSeekFiredOffset) {
return;
}
//將模式設置為拖動歌詞模式
mDisplayMode = DISPLAY_MODE_SEEK;
int rowOffset = Math.abs((int) offsetY / mLrcFontSize); //歌詞要滾動的行數
Log.d(TAG, "move to new hightlightrow : " + mHignlightRow + " offsetY: " + offsetY + " rowOffset:" + rowOffset);
if (offsetY < 0) {
//手指向上移動,歌詞向下滾動
mHignlightRow += rowOffset;//設置要高亮的歌詞為 當前高亮歌詞 向下滾動rowOffset行後的歌詞
} else if (offsetY > 0) {
//手指向下移動,歌詞向上滾動
mHignlightRow -= rowOffset;//設置要高亮的歌詞為 當前高亮歌詞 向上滾動rowOffset行後的歌詞
}
//設置要高亮的歌詞為0和mHignlightRow中的較大值,即如果mHignlightRow < 0,mHignlightRow=0
mHignlightRow = Math.max(0, mHignlightRow);
//設置要高亮的歌詞為0和mHignlightRow中的較小值,即如果mHignlight > RowmLrcRows.size()-1,mHignlightRow=mLrcRows.size()-1
mHignlightRow = Math.min(mHignlightRow, mLrcRows.size() - 1);
//如果歌詞要滾動的行數大於0,則重畫LrcView
if (rowOffset > 0) {
mLastMotionY = y;
invalidate();
}
}
/**
* 設置當前兩個手指的x坐標和y坐標
*/
private void setTwoPointerLocation(MotionEvent event) {
mPointerOneLastMotion.x = event.getX(0);
mPointerOneLastMotion.y = event.getY(0);
mPointerTwoLastMotion.x = event.getX(1);
mPointerTwoLastMotion.y = event.getY(1);
}
/**
* 設置縮放後的字體大小
*/
private void setNewFontSize(int scaleSize) {
//設置歌詞縮放後的的最新字體大小
mLrcFontSize += scaleSize;
mLrcFontSize = Math.max(mLrcFontSize, mMinLrcFontSize);
mLrcFontSize = Math.min(mLrcFontSize, mMaxLrcFontSize);
//設置歌詞的最新字體大小
mSeekLineTextSize += scaleSize;
mSeekLineTextSize = Math.max(mSeekLineTextSize, mMinSeekLineTextSize);
mSeekLineTextSize = Math.min(mSeekLineTextSize, mMaxSeekLineTextSize);
}
/**
* 獲取歌詞大小要縮放的比例
*/
private int getScale(MotionEvent event) {
Log.d(TAG, "scaleSize getScale");
float x0 = event.getX(0);
float y0 = event.getY(0);
float x1 = event.getX(1);
float y1 = event.getY(1);
float maxOffset = 0; // max offset between x or y axis,used to decide scale size
boolean zoomin = false;
//第一次雙指之間的x坐標的差距
float oldXOffset = Math.abs(mPointerOneLastMotion.x - mPointerTwoLastMotion.x);
//第二次雙指之間的x坐標的差距
float newXoffset = Math.abs(x1 - x0);
//第一次雙指之間的y坐標的差距
float oldYOffset = Math.abs(mPointerOneLastMotion.y - mPointerTwoLastMotion.y);
//第二次雙指之間的y坐標的差距
float newYoffset = Math.abs(y1 - y0);
//雙指移動之後,判斷雙指之間移動的最大差距
maxOffset = Math.max(Math.abs(newXoffset - oldXOffset), Math.abs(newYoffset - oldYOffset));
//如果x坐標移動的多一些
if (maxOffset == Math.abs(newXoffset - oldXOffset)) {
//如果第二次雙指之間的x坐標的差距大於第一次雙指之間的x坐標的差距則是放大,反之則縮小
zoomin = newXoffset > oldXOffset ? true : false;
}
//如果y坐標移動的多一些
else {
//如果第二次雙指之間的y坐標的差距大於第一次雙指之間的y坐標的差距則是放大,反之則縮小
zoomin = newYoffset > oldYOffset ? true : false;
}
Log.d(TAG, "scaleSize maxOffset:" + maxOffset);
if (zoomin) {
return (int) (maxOffset / 10);//放大雙指之間移動的最大差距的1/10
} else {
return -(int) (maxOffset / 10);//縮小雙指之間移動的最大差距的1/10
}
}
/**
* 設置歌詞行集合
* @param lrcRows
*/
public void setLrc(List lrcRows) {
mLrcRows = lrcRows;
invalidate();
}
/**
* 播放的時候調用該方法滾動歌詞,高亮正在播放的那句歌詞
* @param time
*/
public void seekLrcToTime(long time) {
if (mLrcRows == null || mLrcRows.size() == 0) {
return;
}
if (mDisplayMode != DISPLAY_MODE_NORMAL) {
return;
}
Log.d(TAG, "seekLrcToTime:" + time);
for (int i = 0; i < mLrcRows.size(); i++) {
LrcRow current = mLrcRows.get(i);
LrcRow next = i + 1 == mLrcRows.size() ? null : mLrcRows.get(i + 1);
/**
* 正在播放的時間大於current行的歌詞的時間而小於next行歌詞的時間, 設置要高亮的行為current行
* 正在播放的時間大於current行的歌詞,而current行為最後一句歌詞時,設置要高亮的行為current行
*/
if ((time >= current.time && next != null && time < next.time)
|| (time > current.time && next == null)){
seekLrc(i, false);
return;
}
}
}
}
以上就是自定義LrcView的全部內容,下面將該自定義LrcView放在布局文件activity_main.xml中去顯示出來。
activity_main.xml的代碼如下所示:
然後通過MainActivity來加載該布局,並在MainActivity中播放音樂,MainActivity的代碼如下所示:
package com.oyp.lrc;
import android.app.Activity;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.os.Bundle;
import android.util.Log;
import com.oyp.lrc.view.ILrcBuilder;
import com.oyp.lrc.view.ILrcView;
import com.oyp.lrc.view.ILrcViewListener;
import com.oyp.lrc.view.impl.DefaultLrcBuilder;
import com.oyp.lrc.view.impl.LrcRow;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
public class MainActivity extends Activity {
public final static String TAG = "MainActivity";
//自定義LrcView,用來展示歌詞
ILrcView mLrcView;
//更新歌詞的頻率,每秒更新一次
private int mPalyTimerDuration = 1000;
//更新歌詞的定時器
private Timer mTimer;
//更新歌詞的定時任務
private TimerTask mTask;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//獲取自定義的LrcView
setContentView(R.layout.activity_main);
mLrcView=(ILrcView)findViewById(R.id.lrcView);
//從assets目錄下讀取歌詞文件內容
String lrc = getFromAssets("test.lrc");
//解析歌詞構造器
ILrcBuilder builder = new DefaultLrcBuilder();
//解析歌詞返回LrcRow集合
List rows = builder.getLrcRows(lrc);
//將得到的歌詞集合傳給mLrcView用來展示
mLrcView.setLrc(rows);
//開始播放歌曲並同步展示歌詞
beginLrcPlay();
//設置自定義的LrcView上下拖動歌詞時監聽
mLrcView.setListener(new ILrcViewListener() {
//當歌詞被用戶上下拖動的時候回調該方法,從高亮的那一句歌詞開始播放
public void onLrcSeeked(int newPosition, LrcRow row) {
if (mPlayer != null) {
Log.d(TAG, "onLrcSeeked:" + row.time);
mPlayer.seekTo((int) row.time);
}
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mPlayer != null) {
mPlayer.stop();
}
}
/**
* 從assets目錄下讀取歌詞文件內容
* @param fileName
* @return
*/
public String getFromAssets(String fileName){
try {
InputStreamReader inputReader = new InputStreamReader( getResources().getAssets().open(fileName) );
BufferedReader bufReader = new BufferedReader(inputReader);
String line="";
String result="";
while((line = bufReader.readLine()) != null){
if(line.trim().equals(""))
continue;
result += line + "\r\n";
}
return result;
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
MediaPlayer mPlayer;
/**
* 開始播放歌曲並同步展示歌詞
*/
public void beginLrcPlay(){
mPlayer = new MediaPlayer();
try {
mPlayer.setDataSource(getAssets().openFd("test.mp3").getFileDescriptor());
//准備播放歌曲監聽
mPlayer.setOnPreparedListener(new OnPreparedListener() {
//准備完畢
public void onPrepared(MediaPlayer mp) {
mp.start();
if(mTimer == null){
mTimer = new Timer();
mTask = new LrcTask();
mTimer.scheduleAtFixedRate(mTask, 0, mPalyTimerDuration);
}
}
});
//歌曲播放完畢監聽
mPlayer.setOnCompletionListener(new OnCompletionListener() {
public void onCompletion(MediaPlayer mp) {
stopLrcPlay();
}
});
//准備播放歌曲
mPlayer.prepare();
//開始播放歌曲
mPlayer.start();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 停止展示歌曲
*/
public void stopLrcPlay(){
if(mTimer != null){
mTimer.cancel();
mTimer = null;
}
}
/**
* 展示歌曲的定時任務
*/
class LrcTask extends TimerTask{
@Override
public void run() {
//獲取歌曲播放的位置
final long timePassed = mPlayer.getCurrentPosition();
MainActivity.this.runOnUiThread(new Runnable() {
public void run() {
//滾動歌詞
mLrcView.seekLrcToTime(timePassed);
}
});
}
};
}
下面是項目的結構圖。
模仿支付寶輸入效果,實現很簡單,就是畫個矩形框和圓形,其他的通過組合view來實現所有功能,雖然簡單但是封裝起來,方便以後使用,也分享一下,希望對別人也有點幫助。&nbs
Android Overview Screen – 概覽界面原文鏈接:http://developer.android.com/guide/component
Android中對組合模式的應用,可謂是泛濫成粥,隨處可見,那就是View和ViewGroup類的使用。在android UI設計,幾乎所有的widget和布局類都依靠這
阿法狗讓圍棋突然就被熱議了,鴻洋大神也順勢出了篇五子棋單機游戲的視頻,我看到了就像膜拜膜拜,就學習了一下,寫篇博客梳理一下自己的思路,加深一下印象視頻鏈接: