音乐播放器自定义控件歌词解析

本文介绍了一种自定义音乐歌词显示控件的实现方法,包括歌词文件解析、歌词类设计及自定义控件的绘制过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

源码地址https://github.com/helloworld107/ShangGuiGu321Meida.git
自定义控件音乐歌词源码分析
当前主流的歌词文件为lrc,和txt,先来看一下文件内容
[00:03.51]荣耀
[00:10.30]作词:高晓松
[00:12.30]作曲:钱雷
[00:12.37]演唱:王晓天
[00:20.03]
[00:21.82]你听远处的声声汽笛
[00:27.02]勾勒出梦境中的岛屿

首先:可以看出一句话对应一个时间点,两个时间点的间隔就是歌词停留的时间,不排队一些特殊情况的写法,这里先以最简单的情况讲解一下原理,我们发现每行歌词都有一样的属性,时间点,内容(为空就算“”),停留时间,所以就可以先定义一个歌词单行的类
public classLyric {
privateStringcontent;//内容
private longsleepTime;//停留时间
private longtimePoint;//时间点(时刻)
publicString getContent() {
returncontent;
}

public voidsetContent(String content) {
this.content= content;
}

public longgetSleepTime() {
returnsleepTime;
}

public voidsetSleepTime(longsleepTime) {
this.sleepTime= sleepTime;
}

public longgetTimePoint() {
returntimePoint;
}

public voidsetTimePoint(longtimePoint) {
this.timePoint= timePoint;
}
之后就要解析歌词文本,把内容转换成一行一行的Lyric,一行一行又是什么?想到了吧,就是list<lyric>,一个这样的集合就是要一首歌,读取文件转换非常复杂,所以专门写了一个工具类

public classLyricUtils {

privateArrayList<Lyric>mLyrics;一首歌词
private booleanisExistsLyric;是否存在歌词
public booleanisExistsLyric() {
returnisExistsLyric;
}

publicArrayList<Lyric> getLyrics() {
returnmLyrics;
}

public voidsetLyrics(ArrayList<Lyric> lyrics) {
mLyrics= lyrics;
}

读取歌曲文件,顺便转换成我们要的歌词
public voidreadLyricFile(File file) {
先看在不在,再说做什么
if((file == null) || !file.exists()) {
isExistsLyric=false;
mLyrics=null;
}else{
isExistsLyric=true;
//读流吧
mLyrics=newArrayList<>();
try{
果断一行一行的读啊
BufferedReader buffer =newBufferedReader(newInputStreamReader(newFileInputStream(file),getCharset(file)));
String content ="";
while((content = buffer.readLine()) !=null) {
//解析歌词并且加到集合中喔
parseLyric(content);

}
buffer.close();
}catch(FileNotFoundException e) {
isExistsLyric=false;
e.printStackTrace();
}catch(IOException e) {
e.printStackTrace();
}
}
从上到下读取后应该就是正确的顺序,这个方法很可能多次一举,不写影响也不大
if(mLyrics!=null) {
Collections.sort(mLyrics,newComparator<Lyric>() {
@Override
public intcompare(Lyric o1, Lyric o2) {
if(o1.getTimePoint() < o2.getTimePoint()) {
return-1;
}else if(o1.getTimePoint() > o2.getTimePoint()) {
return1;
}else{
return0;
}
}
});
}

//3.计算每句高亮显示的时间(停留时间)
//后一句减前面一句
if(mLyrics!=null) {
for(inti = 0; i <mLyrics.size(); i++) {
intnext = i + 1;
if(next < mLyrics.size()) {
longsleepTime = mLyrics.get(next).getTimePoint() -mLyrics.get(i).getTimePoint();
mLyrics.get(i).setSleepTime(sleepTime);

}

}
}

}

/**
*解析一行歌词
*简单情况[02:04.12]我在这里欢笑
*/
private voidparseLyric(String content) {

//判断有几句歌词
intcountTag = getCountTag(content);
//代表正常的一句话
if(countTag != -1) {
intpost2 = content.indexOf("]");
intpost1 = content.indexOf("[");
截取对于边界保留谁也是个大坑,对于两个边界是前包后不包,对于只截取一个边界是不包,真的很坑,这种东西谁也记不住吧,到时候写个测试类测试一下就好
String timePoint = content.substring(post1 +1, post2),
String lyricContent = content.substring(post2 +1);
Lyric lyric =newLyric();
//显然我们需要long
lyric.setTimePoint(strTime2LongTime(timePoint));
lyric.setContent(lyricContent);
mLyrics.add(lyric);
}
}

这里的坑好多,对于[10:10.10]如果从左边分割就是两个数组,如果是从右边分割就是一个数组
但是会把该句作为只含一个元素的数组,所以长度为0根本不可能存在,至少为一
private intgetCountTag(String line) {
intresult = -1;
if(line != null) {
String[] right = line.split("\\]");
result = right.length;
}
//减去1正好判断精准无误
returnresult - 1;
}

/**
*String类型是时间转换成long类型
*@paramstrTime02:04.12
*/
private longstrTime2LongTime(String strTime) {
longresult = -1;
//切割
String[] left = strTime.split(":");
String[] right = left[1].split("\\.");一定要注意"."一定要加双斜杠,系统会识别冲突当做下一级的意思,这是个大坑,慎入,包括其他容易冲突的字符,都要加上双斜杠
longmin = Long.parseLong(left[0]);
longsecond = Long.parseLong(right[0]);
longmills = Long.parseLong(right[1]);//毫秒
result = min *60* 1000+ second *1000+ mills *10;
returnresult;
}

/**
*断文件编码 这个非常复杂,就不说了
*
*@paramfile文件
*@return编码:GBK,UTF-8,UTF-16LE
*/
publicString getCharset(File file) {
String charset ="GBK";
byte[] first3Bytes = new byte[3];
try{
booleanchecked = false;
BufferedInputStream bis =newBufferedInputStream(
newFileInputStream(file));
bis.mark(0);
intread = bis.read(first3Bytes,0,3);
if(read == -1)
returncharset;
if(first3Bytes[0] == (byte)0xFF&& first3Bytes[1] == (byte)0xFE) {
charset ="UTF-16LE";
checked =true;
}else if(first3Bytes[0] == (byte)0xFE
&& first3Bytes[1] == (byte)0xFF) {
charset ="UTF-16BE";
checked =true;
}else if(first3Bytes[0] == (byte)0xEF
&& first3Bytes[1] == (byte)0xBB
&& first3Bytes[2] == (byte)0xBF) {
charset ="UTF-8";
checked =true;
}
bis.reset();
if(!checked) {
intloc = 0;
while((read = bis.read()) != -1) {
loc++;
if(read >= 0xF0)
break;
if(0x80<= read && read <= 0xBF)
break;
if(0xC0<= read && read <= 0xDF) {
read = bis.read();
if(0x80<= read && read <= 0xBF)
continue;
else
break;
}else if(0xE0<= read && read <= 0xEF) {
read = bis.read();
if(0x80<= read && read <= 0xBF) {
read = bis.read();
if(0x80<= read && read <= 0xBF) {
charset ="UTF-8";
break;
}else
break;
}else

break;
}
}
}
bis.close();
}catch(Exception e) {
e.printStackTrace();
}
returncharset;
}
}

最后终于可以开始写自定义歌词控件啦
能实现的方法很多,这里使用的是textview实际上显示也并不是主布局显示多个该控件,而是在这个大控件里面不断的画textview,反而textview纯粹变成了一个容器,目测换成其他控件改动也不会大
public classShowLyricViewextendsTextView {
privatePaintmPaint;正在唱的歌词为红色
privatePaintmWhitePaint;没读到的歌词为白色
privateArrayList<Lyric>mLyrics=newArrayList<Lyric>();歌词
private intmIndex=0;//当前歌词坐标
//控件宽高
private intmWeidth;
private intmHeight;
//假定的控件行高
private intmLineHeight;
private floatmTimePoint;时间点
private floatmSleepTime;停留时间
//当前播放进度
private floatcurrentPositon;

publicArrayList<Lyric> getLyrics() {
returnmLyrics;
}

public voidsetLyrics(ArrayList<Lyric> lyrics) {
mLyrics= lyrics;
}

publicShowLyricView(Context context) {
this(context,null);
}

publicShowLyricView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}

publicShowLyricView(Context context, AttributeSet attrs,intdefStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}

private voidinitView(Context context) {
//歌词当然需要画笔啦
//高亮画笔画笔
mPaint=newPaint();
mPaint.setColor(Color.GREEN);
mPaint.setAntiAlias(true);
mPaint.setTextSize(CommonUtils.dip2px(context, 16));这句方法是为了做适配,详情看攻击类
mPaint.setTextAlign(Paint.Align.CENTER);
//普通文本画笔
mWhitePaint=newPaint();
mWhitePaint.setColor(Color.WHITE);
mWhitePaint.setAntiAlias(true);
mWhitePaint.setTextSize(CommonUtils.dip2px(context,16));这句方法是为了做适配
mWhitePaint.setTextAlign(Paint.Align.CENTER);
//定义行高
mLineHeight=CommonUtils.dip2px(context,18);
/* //我们先假设模拟一些数据
for (int i = 0; i < 100; i++) {
Lyric lyric = new Lyric();
lyric.setContent(i + "我爱你啦啦啦" + i);
lyric.setTimePoint(1000 * i);
lyric.setSleepTime(1500 + i);
mLyrics.add(lyric);
}*/
}

@Override
protected voidonDraw(Canvas canvas) {
super.onDraw(canvas);

if(mLyrics!=null&&mLyrics.size() >0) {

//往上推移的效果
floatplush = 0;
if(mSleepTime==0){
plush=0;
}else{
是不是想起了高中几何的等比例公式??
//这一句所花的时间 :休眠时间 mSleepTime=移动的距离 plush: 总距离(行高)mLineHeight
//移动的距离 = (这一句所花的时间 :休眠时间)*总距离(行高)
//屏幕的的坐标 = 行高 +移动的距离
plush=((currentPositon-mTimePoint)/mSleepTime)*mLineHeight;
canvas.translate(0,-plush);
}


String content =mLyrics.get(mIndex).getContent();
//绘制当前歌词 注意我们以控件中间线为标准,上下绘制
canvas.drawText(content,mWeidth/2,mHeight/2,mPaint);
//绘制前面歌词
floattemp=mHeight/2;
for(inti = mIndex-1; i >=0; i--) {
content =mLyrics.get(i).getContent();
temp=temp-mLineHeight;
//防止越界
if(temp<0){
break;
}
canvas.drawText(content,mWeidth/2,temp,mWhitePaint);
}

//绘制后面歌词i
temp=mHeight/2;
for(inti = mIndex+1; i <mLyrics.size(); i++) {
content =mLyrics.get(i).getContent();
temp=temp+mLineHeight;
//防止越界
if(temp>mHeight){
break;
}
canvas.drawText(content,mWeidth/2,temp,mWhitePaint);
}
}else{
canvas.drawText("没有歌词",mWeidth/2,mHeight/2,mPaint);
}
}

//可以测出当前控件的宽高值
@Override
protected voidonSizeChanged(intw,inth,intoldw,intoldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWeidth= w;
mHeight= h;
}

//设置显示的位置其实就是找到那一行,重新执行ondraw
//找的方法是通过夹逼两个时刻的中间值,注意参数为时刻
public voidsetLyricPosition(floatcurrentPositon){
//没有就不判断啦
this.currentPositon= currentPositon;
if(mLyrics==null||mLyrics.size()==0) {
return;
}
for(inti = 1; i <mLyrics.size(); i++) {

if(currentPositon<mLyrics.get(i).getTimePoint()){
//既然这里减去1,那就应该从1开始循环,不要手贱的习惯性从0开始,分分钟钟没有结果,坑
intpreTime=i-1;
if(currentPositon>=mLyrics.get(preTime).getTimePoint()){
//当前增长播放的歌曲,千万不要混为一弹
mIndex=preTime;
//同时设置时间和时刻
mTimePoint=mLyrics.get(preTime).getTimePoint();
mSleepTime=mLyrics.get(preTime).getSleepTime();
}

}
}
invalidate();//主线程重绘
// postInvalidate();//子线程重绘
}
}
之后在布局文件引用拿到数据就可显示了,是不是很简单呀(*^__^*) 嘻嘻……

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值