源码地址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();//子线程重绘
}
}
之后在布局文件引用拿到数据就可显示了,是不是很简单呀(*^__^*) 嘻嘻……