歌词显示实现
1.绘制单行居中文本
自定义一个显示歌词的 LryicView,歌词本身就是一个文本,所以在这里我们继承 TextView,它还有一个好处继承 TextView 之后不需要再去重写 onMeasure 方法,在 onDraw 方法中去绘制一个文本
LryicView.java
package com.itheima.medioplayer.ui.view;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.widget.TextView;
import com.itheima.medioplayer.R;
public class LryicView extends TextView {
private float hightlightSize;
private float normalSize;
private int hightLightColor;
private int normalColor;
private Paint paint;
public LryicView(Context context) {
super(context);
initView();
}
public LryicView(Context context, AttributeSet attrs) {
super(context, attrs);
initView();
}
public LryicView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView();
}
private void initView() {
hightlightSize = getResources().getDimension(R.dimen.lryic_hightlight_size);
normalSize = getResources().getDimension(R.dimen.lryic_normal_size);
hightLightColor = Color.GREEN;
normalColor = Color.WHITE;
paint = new Paint();
paint.setAntiAlias(true);//抗锯齿
paint.setTextSize(hightlightSize);
paint.setColor(hightLightColor);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text="正在加载歌词...";
canvas.drawText(text,0,0, paint);
}
}
在项目中的歌词布局中引用 View,重新 build即可以显示,但是文本显示的坐标是 view 的左上角,那么我们需要将文本显示的位置设置在 view 的中间,下面描述下怎么居中。
单行文本居中:X=View一半宽度-文本一半宽度, Y=View一半高度+文本一半高度,X、Y分别为宽和高
在onSizeChang中计算出View宽和高的一半, 通过paint.getTextBounds方法计算出文本的宽高的一半
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
halfViewW = w/2;
halfViewH = h/2;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
String text="正在加载歌词...";
Rect bounds=new Rect();
//计算 text 的宽和高
int halfTextW=bounds.width()/2;
int halfTextH=bounds.height()/2;
//计算 text 位置
int drawX=halfViewW-halfTextW;
int drawY=halfTextH+halfViewH;
canvas.drawText(text,drawX,drawY, paint);
}
1.2 绘制多行歌词
1.首先用 List 模拟歌词的数据并且记录高亮行的行数
private void initView() {
hightlightSize = getResources().getDimension(R.dimen.lryic_hightlight_size);
normalSize = getResources().getDimension(R.dimen.lryic_normal_size);
hightLightColor = Color.GREEN;
normalColor = Color.WHITE;
paint = new Paint();
paint.setAntiAlias(true);//抗锯齿
paint.setTextSize(hightlightSize);
paint.setColor(hightLightColor);
//高亮的行数
currentLine = 5;
//模拟初始化数据
lryics = new ArrayList<>();
for (int i = 0; i <30 ; i++) {
lryics.add(new Lryic(i * 2000, "当前正在播放行数为:"+i));
}
}
2.获取高亮行的位置
/** 绘制多行文本*/
private void drawMutiLineText(Canvas canvas) {
Lryic lryic=lryics.get(currentLine);
//获取高亮行 Y 的位置
Rect bounds=new Rect();
//计算 text 的宽和高
paint.getTextBounds(lryic.getContent(),0,lryic.getContent().length(),bounds);
int halfTextW=bounds.width()/2;
int halfTextH=bounds.height()/2;
int centerY=halfTextH+halfViewH;
}
3.按行绘制文本
for (int i = 0; i < lryics.size(); i++) {
if (currentLine==i){
paint.setColor(hightLightColor);
paint.setTextSize(hightlightSize);
}else{
paint.setColor(normalColor);
paint.setTextSize(normalSize);
}
4.y=居中行 y 的位置+(绘制行的位置-高亮行的行数)*行高
lineHeight=getResources().getDimensionPixelSize(R.dimen.lryic_line_height);
//y=居中行 Y 的位置+(绘制行的行数-高亮行的行数)*行号
int downY=centerY+(i-currentLine)*lineHeight;
5.x=水平居中的 x
drawHorizontalText(canvas,lryics.get(i).getContent(),downY);
1.3按行滚动歌词
1.在 LryicView 中提供一个滚动歌词的方法,说白了其实只要设置歌词高亮的位置就可以了。
高亮行:起始时间<=播放时间,下一行起始时间>播放时间
public void roll(int position,int duration){
int endPoint;
for (int i =1;i<lyrics.size();i++){
Lyric lyric = lyrics.get(i);
if (i==lyrics.size()-1){
endPoint = duration;
}else{
Lyric nextLryic=lyrics.get(i+1);
endPoint=nextLryic.getLrcTime();
}
if (lyric.getLrcTime()<=position&&endPoint>position){
current_line=i;
break;
}
}
invalidate();
}
2.在音乐播放界面中发消息让歌词滚动。在接收到准备完成的广播之后就让歌词开始滚动
private static final int UPDATE_LRYIC_ROLL = 1;
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case UPDATA_POSITION:
updataCurrentPosition();
seekbar.setProgress(binder.getCurrentPosition());
break;
case UPDATE_LRYIC_ROLL:
startRoll();
}
}
};
private void startRoll() {
lryicView.roll(binder.getCurrentPosition(),binder.getDuration());
handler.sendEmptyMessage(UPDATE_LRYIC_ROLL);
}
1.4 平滑滚动歌词
平滑滚动歌词算法 :
变化位置=居中行位置+偏移位置
偏移位置=移动百分比*行高
移动时间百分比=移动时间/可用时间
可用时间=下一段的时间-本段的时间
移动时间=已播放时间-起始时间
public void drawMutiLineText(Canvas canvas){
Lyric lyric =lyrics.get(current_line);
int endStartPoint;
if (current_line==lyrics.size()-1){
endStartPoint=mDuration;
}else{
Lyric nextLryic=lyrics.get(current_line+1);
endStartPoint=nextLryic.getLrcTime();
}
int moveTime=mPosition-lyric.getLrcTime();
int useTime=endStartPoint-lyric.getLrcTime();
float movePercent=moveTime/(float)useTime;
int offset= (int) (movePercent*lineHeight);
Rect bound = new Rect();
paint.getTextBounds(lyric.getLrcString(),0,lyric.getLrcString().length(),bound);
int halfTextW = bound.width()/2;
int halfTextH = bound.height()/2;
int centerY = halfTextH+halfViewH-offset;
int centerY = halfTextH+halfViewH;
for (int i = 0;i<lyrics.size();i++){
if (current_line == i){
paint.setColor(highlightColor);
}else {
paint.setColor(nomalColor);
}
int downY=centerY+(i-current_line)*lineHeight;
canvas.drawText(lyrics.get(i).getLrcString(),halfTextW,downY,paint);
}
}
1.5从文件中解析歌词
1.从文件中解析歌词。将歌词一行一行的读出来,并且根据歌词的格式解析成 List 集合,并将歌词排序
import com.itheima.medioplayer.bean.Lryic;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
public class LryicParser {
/** 从歌词文件中解析歌词列表*/
public static List<Lryic> parseLryicFromFile(File lryicFile){
List<Lryic> lryics=new ArrayList<>();
//数据可用性检查
if (lryicFile==null || !lryicFile.exists()){
lryics.add(new Lryic(0,"没有找到歌词文件"));
return lryics;
}
try {
BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(new
FileInputStream(lryicFile),"GBK"));
String line=bufferedReader.readLine();
while (line!=null){
List<Lryic> lineLryics=parserLine(line);
lryics.addAll(lineLryics);
line=bufferedReader.readLine();
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
//歌词排序
Collections.sort(lryics);
return lryics;
}
/** 解析一行歌词 [ 01:22.51][ 01:22.51]爱爱爱*/
private static List<Lryic> parserLine(String line) {
List<Lryic> lineLryic=new ArrayList<>();
// [ 01:22.51 [ 01:22.51 爱爱爱
String[] arr=line.split("]");
String content=arr[arr.length-1];
for (int i = 0; i < arr.length - 1; i++) {
int startPoint=parserPoint(arr[i]);
lineLryic.add(new Lryic(startPoint,content));
}
return lineLryic;
}
/** 解析一行歌词的时间 [ 01:22.51*/
private static int parserPoint(String s) {
int time=0;
String timeStr=s.substring(1);
String[] arr=timeStr.split(":");
String minStr=arr[0];
arr=arr[1].split("\\.");
String senStr=arr[0];
String mSenStr=arr[1];
time=Integer.parseInt(minStr)*60*1000+Integer.parseInt(senStr)*1000+Integer.parse
Int(mSenStr)*100;
return time;
}
}
Lryic.java 需要实现 Comparable 接口,实现 compareTo 方法
@Override
public int compareTo(Lryic lryic) {
return startPoint-lryic.getStartPoint();
}
在 LryicView 中提供从文件中获取歌词集合和设置当前高亮行的方法
public void setLryicFile(File lryicFile){
lryics=LryicParser.parseLryicFromFile(lryicFile);
currentLine=0;
}
在 onDraw 方法中绘制的时候,需要去判断集合是否有数据,没有数据的话就显示歌词正在加载中,如果有数据的话就显示歌词
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (lryics==null||lryics.size()==0){
//绘制单行居中
drawSingleLineText(canvas);
}else{
drawMutiLineText(canvas);
}
}
在接收准备的广播中的滚动歌词之前将歌词加载出来
private class AudioBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
//准备完成
//更新界面的按钮
updatePlayBtn();
//初始化歌曲名和歌手
AudioItem audioItem= (AudioItem) intent.getSerializableExtra("audioItem");
tv_name.setText(StringUtil.formatDisplayName(audioItem.getName()));
tv_artist.setText(audioItem.getArtist());
sk_position.setMax(binder.getDuration());
//更新播放进度
updateCurrentPosition();
//初始化播放模式
updatePlayModeBtn();
File file=new File(Environment.getExternalStorageDirectory(),"test/audio/"+
StringUtil.formatDisplayName(audioItem.getName())+".lrc");
lryicView.setLryicFile(file);
//开启歌词滚动更新
startRoll();
}
}
总结
到这一篇为止,我已经实现了主页面、音乐播放服务、通知栏和歌词,这时一个音乐播放器已经基本完成,但是我还有一些功能没有实现,没有关系,之后我会进行第二版的开发,现在还有其他项目在忙,所以只能暂时搁置,我相信第二版肯定会更好的,也希望大家可以多提意见,我会悉心听取的。