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