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

源码地址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 class Lyric {
private String content ; // 内容
private long sleepTime ; // 停留时间
private long timePoint ; // 时间点 ( 时刻)
public String getContent() {
return content ;
}

public void setContent(String content) {
this . content = content;
}

public long getSleepTime() {
return sleepTime ;
}

public void setSleepTime( long sleepTime) {
this . sleepTime = sleepTime;
}

public long getTimePoint() {
return timePoint ;
}

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

public class LyricUtils {

private ArrayList<Lyric> mLyrics ;一首歌词
private boolean isExistsLyric ;是否存在歌词
public boolean isExistsLyric() {
return isExistsLyric ;
}

public ArrayList<Lyric> getLyrics() {
return mLyrics ;
}

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

读取歌曲文件,顺便转换成我们要的歌词
public void readLyricFile(File file) {
先看在不在,再说做什么
if ((file == null ) || !file.exists()) {
isExistsLyric = false ;
mLyrics = null ;
} else {
isExistsLyric = true ;
// 读流吧
mLyrics = new ArrayList<>();
try {
果断一行一行的读啊
BufferedReader buffer = new BufferedReader( new InputStreamReader( new FileInputStream(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 , new Comparator<Lyric>() {
@Override
public int compare(Lyric o1, Lyric o2) {
if (o1.getTimePoint() < o2.getTimePoint()) {
return - 1 ;
} else if (o1.getTimePoint() > o2.getTimePoint()) {
return 1 ;
} else {
return 0 ;
}
}
});
}

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

}

}
}

}

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

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

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

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

/**
* 断文件编码 这个非常复杂,就不说了
*
* @param file 文件
* @return 编码: GBK,UTF-8,UTF-16LE
*/
public String getCharset(File file) {
String charset = "GBK" ;
byte [] first3Bytes = new byte [ 3 ];
try {
boolean checked = false ;
BufferedInputStream bis = new BufferedInputStream(
new FileInputStream(file));
bis.mark( 0 );
int read = bis.read(first3Bytes, 0 , 3 );
if (read == - 1 )
return charset;
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) {
int loc = 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();
}
return charset;
}
}

最后终于可以开始写自定义歌词控件啦
能实现的方法很多,这里使用的是textview实际上显示也并不是主布局显示多个该控件,而是在这个大控件里面不断的画textview,反而textview纯粹变成了一个容器,目测换成其他控件改动也不会大
public class ShowLyricView extends TextView {
private Paint mPaint ;正在唱的歌词为红色
private Paint mWhitePaint ;没读到的歌词为白色
private ArrayList<Lyric> mLyrics = new ArrayList<Lyric>();歌词
private int mIndex = 0 ; // 当前歌词坐标
// 控件宽高
private int mWeidth ;
private int mHeight ;
// 假定的控件行高
private int mLineHeight ;
private float mTimePoint ;时间点
private float mSleepTime ;停留时间
// 当前播放进度
private float currentPositon ;

public ArrayList<Lyric> getLyrics() {
return mLyrics ;
}

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

public ShowLyricView(Context context) {
this (context, null );
}

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

public ShowLyricView(Context context, AttributeSet attrs, int defStyleAttr) {
super (context, attrs, defStyleAttr);
initView(context);
}

private void initView(Context context) {
// 歌词当然需要画笔啦
// 高亮画笔画笔
mPaint = new Paint();
mPaint .setColor(Color. GREEN );
mPaint .setAntiAlias( true );
mPaint .setTextSize(CommonUtils. dip2px (context, 16));这句方法是为了做适配,详情看攻击类
mPaint .setTextAlign(Paint.Align. CENTER );
// 普通文本画笔
mWhitePaint = new Paint();
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 void onDraw(Canvas canvas) {
super .onDraw(canvas);

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

// 往上推移的效果
float plush = 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 );
// 绘制前面歌词
float temp= mHeight / 2 ;
for ( int i = 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 ( int i = 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 void onSizeChanged( int w, int h, int oldw, int oldh) {
super .onSizeChanged(w, h, oldw, oldh);
mWeidth = w;
mHeight = h;
}

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

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

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

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值