要实现桌面歌词,第一步必须做到将view置于屏幕顶端,实现这个效果可以用window系统自带的winmanager来实现,具体实现代码:
WindowManager wm;//创建浮动窗口盛放歌词
public void createFloatView(int paddingBottom) {
wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
view = LayoutInflater.from(this).inflate(R.layout.floatlrc,null,false);//拿到桌面歌词的布局
desk =(LrcView2)view.findViewById(R.id.lrc);//自定义的盛放桌面歌词的textview
final LinearLayout gone = (LinearLayout)view.findViewById(R.id.lrcgone);
final ImageView iv = (ImageView)view.findViewById(R.id.iv);
//设置监听点击关闭按钮关闭桌面歌词
iv.setOnClickListener(new View.OnClickListener() {
@Override
publicvoidonClick(View v) {
removeFloatView();
lrcIsShow=true;
}
});
//定义布局参数
final WindowManager.LayoutParams params = new WindowManager.LayoutParams();
params.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;// 所有程序窗口的“基地”窗口,其他应用程序窗口都显示在它上面。params.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL//设置浮动窗口下面的组件可以被点击
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
params.format = PixelFormat.TRANSLUCENT;// 不设置这个弹出框的透明遮罩显示为黑色params.width = WindowManager.LayoutParams.MATCH_PARENT;
params.height = WindowManager.LayoutParams.WRAP_CONTENT;;
params.gravity = Gravity.TOP | Gravity.LEFT;
int screenWidth = getResources().getDisplayMetrics().widthPixels;
int screenHeight = getResources().getDisplayMetrics().heightPixels;
params.x = screenWidth;
params.y = screenHeight - paddingBottom;
view.setBackgroundColor(Color.TRANSPARENT);
view.setVisibility(View.VISIBLE);
//重写onTouchListener,达到可拖动的目的,只可以上下拖动
view.setOnTouchListener(new View.OnTouchListener() {
// 触屏监听float lastX, lastY;
int oldOffsetX, oldOffsetY;
int tag = 0;// 悬浮球 所需成员变量
@Override
public boolean onTouch(View v, MotionEvent event) {
final int action = event.getAction();
float x = event.getX();
float y = event.getY();
if (tag == 0) {
oldOffsetX = params.x; // 偏移量
oldOffsetY = params.y; // 偏移量
}
if (action == MotionEvent.ACTION_DOWN) {
lastX = x;
lastY = y;
} elseif (action == MotionEvent.ACTION_MOVE) {
params.x += (int) (x - lastX) / 3; // 减小偏移量,防止过度抖动params.y += (int) (y - lastY) / 3; // 减小偏移量,防止过度抖动
tag = 1;
wm.updateViewLayout(v, params);//更新位置
} elseif (action == MotionEvent.ACTION_UP) {
//判断为点击后做的事
int newOffsetX = params.x;
int newOffsetY = params.y;
// 只要按钮一动位置不是很大,就认为是点击事件if (Math.abs(oldOffsetX - newOffsetX) <= 20
&& Math.abs(oldOffsetY - newOffsetY) <= 20) {
if (gone.getVisibility() == View.GONE) {
gone.setVisibility(View.VISIBLE);
} else {
gone.setVisibility(View.GONE);
}
v.setFocusable(false);
} else {
tag = 0;
}
}
return true;
}
});
initLrc();
wm.addView(view, params);
}
第二步就是对布局中自定义的textview(desk)做的事了,initLrc()就是为desk绑定监听事件,首先呢需要自定义接口初始化歌词以及监听歌词的变化
publicinterfaceOnLrcChangedListener { publicvoidonLrcLoaded(List<LrcModel> lrcModels); publicvoidonLyricSentenceChanged(int index); }
第三步:从网络或者本地数据库中拿到lrc歌词,将歌词处理成我们需要的,一行一行的格式,定义LrcDownloadManager类做这些事情,
package com.example.user.myapplication; /** * Created by user on 2016/9/13. */public class LrcDownLoadManager { private LrcDao lrcDao; private List<LrcModel> lrcModels;
//可能不止一个地方需要歌词信息,因此定义一个集合存放监听器,需要的时候遍历就好 private List<OnLrcChangedListener> onLrcChangedListeners = new ArrayList<>(); private OnLrcChangedListener lrcChangedListener; publicvoid setOnLrcChangedListener(OnLrcChangedListener lrcChangedListener){ onLrcChangedListeners.add(lrcChangedListener); } public LrcDownLoadManager(Context context) { lrcModels = new ArrayList<LrcModel>(); lrcDao = new LrcDao(context); }
//从本地数据库加载格式为lrc的歌词,如果没有就从网络中加载 publicvoid getSongInfo(final BenDiMusic music){ List<Lrc> lrcs = lrcDao.queryForName(music.getName()); if(lrcs.size()!=0){ String[] lines = lrcs.get(0).getContent().split("\\n"); lrcModels.clear(); for(String l:lines) { parseLrc(l); }
for(OnLrcChangedListener l:onLrcChangedListeners){ if(l!=null){ l.onLrcLoaded(lrcModels); } } }else{ if(1==1){ VolleyUtil.get().setCallBack(new CallBack() { @Override publicvoid onSuccess(String response) { lrcModels.clear(); try { Log.i("response", music.getName()); JSONObject jsonObject = newJSONObject(response); JSONArray array = jsonObject.getJSONArray("song"); for(int i = 0;i<array.length();i++){ JSONObject object = array.getJSONObject(i); if(object.getString("artistname").equals(music.getCuthor())){ music.setId(object.getString("songid")); break; } } getLrc(music.getId(),music.getName()); } catch (JSONException e) { lrcModels.clear(); e.printStackTrace(); return; } } @Override publicvoid onError(VolleyError error) { lrcModels.clear(); } }).setUrl("http://tingapi.ting.baidu.com/v1/restserver/ting?format=json&calback=&" + "from=webapp_music&method=baidu.ting.search.catalogSug&query="+music.getName()).builder() .setPriority(Request.Priority.HIGH) .start(); } } } publicvoid getLrc(String id, finalString name){ VolleyUtil.get().setCallBack(new CallBack() { @Override publicvoid onSuccess(String response) { lrcModels.clear(); try { JSONObject jsonObject = newJSONObject(response); if(jsonObject.getString("lrcContent")==null){ return; } Lrc lrc = new Lrc(); lrc.setContent(jsonObject.getString("lrcContent")); lrc.setName(name); lrcDao.addBook(lrc); String[] lines = jsonObject.getString("lrcContent").split("\\n"); lrcModels.clear(); for(String l:lines) { parseLrc(l); } for(OnLrcChangedListener l:onLrcChangedListeners){ if(lrcModels.size()==0&&l!=null){ lrcModels.clear(); l.onLrcLoaded(lrcModels); } if(l!=null){ l.onLrcLoaded(lrcModels); } } } catch (JSONException e) { e.printStackTrace(); return; } } @Override publicvoid onError(VolleyError error) { } }).setUrl("http://tingapi.ting.baidu.com/v1/restserver/ting?" + "method=baidu.ting.song.lry&songid="+id) .builder().setPriority(Request.Priority.HIGH).start(); }
//将歌词每行的文字提取出来放入集合 publicvoid parseLrc(Stringline){ String reg="\\[(\\d{2}:\\d{2}\\.\\d{2})\\]"; Pattern pattern=Pattern.compile(reg); Matcher matcher=pattern.matcher(line); while(matcher.find()){ String time=matcher.group(); LrcModel lModel=new LrcModel(); lModel.setCurrentTime(parseTime(time)); lModel.setCurrentContent(line.substring(time.length())); lrcModels.add(lModel); } } //每行歌词的播放时间提取出来 public Integer parseTime(String time){ String temp=time.substring(1,time.length()-1); String[] s = temp.split(":"); intmin = Integer.parseInt(s[0]); String[] ss = s[1].split("\\."); int sec = Integer.parseInt(ss[0]); int mill = Integer.parseInt(ss[1]); returnmin * 60 * 1000 + sec * 1000 + mill * 10; } //通过当前播放进度判断播放位置,返回当前行在集合中的索引位置 publicint lrcIndex(int duration,int currentTime) { int index = 0; if (currentTime < duration) { for (int i = 0; i < lrcModels.size(); i++) { if (i < lrcModels.size() - 1) { if (currentTime < lrcModels.get(i).getCurrentTime() && i == 0) { index = i; } if ((currentTime > lrcModels.get(i).getCurrentTime())&& currentTime < lrcModels.get(i+1).getCurrentTime()) { index = i; } } if ((i == lrcModels.size() - 1)&& currentTime > lrcModels.get(i).getCurrentTime()) { index = i; } } } return index; } }
第四步当然是开启桌面歌词的同时为desk添加监听器了,我是在服务中为desk添加的监听器,创建浮动窗口也是在服务中封装好的方法,提供一个调用的方法,
public void initLrc(){ setLrcChangedListener(new OnLrcChangedListener() { @Override public void onLrcLoaded(List<LrcModel> lrcModels){ desk.setLrcList(lrcModels); } @Override public void onLyricSentenceChanged(int index){ desk.setIndex(index); desk.invalidate(); } });
最主要的就是要在服务中实例化LrcDownloadManager的一个对象,调用加载歌词的方法,并完成歌词的制作(制作成我们需要的格式),这里定义了一个set方法添加监听器是为了方便除服务外地方使用这个功能,做完这些还有最重要的一步,就是更新歌词了,利用handler的定时机制更新当前播放进度,public void setLrcChangedListener(OnLrcChangedListener lrcChangedListener){ onLrcChangedListeners.add(lrcChangedListener); if(this.lrcDownLoadManager!=null){ this.lrcDownLoadManager.setOnLrcChangedListener(lrcChangedListener); loadLrc(); } } publicvoidloadLrc(){ //下载歌词 lrcDownLoadManager.getSongInfo(mdata.get(POSITION)); }
private Handler mServiceHandler = new Handler(){
@Overridepublicvoid handleMessage(Message msg) {
super.handleMessage(msg);
if(mdata.get(POSITION).getTime()!=0){
if(msg.what==MSG_TYPE_PROGRESS){//更新进度条,与桌面歌词功能无关
if(mState==State.PLAYING){
for(OnMusicPlayStateListener l:musicPlayStateListeners){
l.onProgressChanged(player.getCurrentPosition());
}
//定时每隔100毫秒,更新当前播放位置的索引
for(OnLrcChangedListener l:onLrcChangedListeners){
if(lrcDownLoadManager!=null){
if(l!=null)
l.onLyricSentenceChanged(lrcDownLoadManager
.lrcIndex((int) mdata.get(POSITION).getTime()
, player.getCurrentPosition()));
}
}
mServiceHandler.sendEmptyMessageDelayed(MSG_TYPE_PROGRESS, 100);//隔500毫秒发song
}
}
}
}
};
最后在添一个移除桌面 歌词的方法
public void removeFloatView(){
if (wm != null && view != null) {
wm.removeViewImmediate(view);
// wm.removeView(view);//不要调用这个,WindowLeaked
view = null;
wm = null;
}<pre name="code" class="java">public float getShaderSpeed(){
float speed = getCharacterWidth(infos.get(index).getCurrentContent(),60)/infos.get(index).getCurrentTime()/2;
return speed;
}
}
如果要实现歌词逐字渲染的效果,还需要再做些工作,因为我的desk(放歌词的textview)的宽度是match_parent,所以再计算渲染速度的时候就不能用这个宽度,需要写一个方法返回当前文本的宽度,下面的是我自定义盛放歌词的view的全部代码
package com.example.user.myapplication.myScroview;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.TextView;
import com.example.user.myapplication.LrcModel;
import java.util.ArrayList;
import java.util.List;
/**
* Created by user on 2016/9/30.
*/
public class LrcView2 extends TextView {
private int indexChanged = 0;
boolean b = true;
private float width; //歌词视图宽度
private float height; //歌词视图高度
private Paint currentPaint; //当前画笔对象
private Paint notCurrentPaint; //非当前画笔对象
private float textHeight = 70; //文本高度
private float textMaxSize = 60;
private float textSize = 55; //文本大小
private int index = 0; //list集合下标
private List<LrcModel> infos; //歌词信息
private float float1 = 0.0f;
private float float2 = 0.01f;
private boolean whichLine = true;判断是第一行歌词还是第二行歌词
private boolean whatLine = true;//判断是否需要切换歌词
private int nextIndex;
public LrcView2(Context context) {
super(context);
init();
}
public LrcView2(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public LrcView2(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public void setLrcList(List<LrcModel> infos) {
this.infos.clear();
this.infos.addAll(infos);
}
private void init() {
infos = new ArrayList<>();
setFocusable(true); //设置可对焦
//显示歌词部分
currentPaint = new Paint();
currentPaint.setAntiAlias(true); //设置抗锯齿,让文字美观饱满
currentPaint.setTextAlign(Paint.Align.CENTER);//设置文本对齐方式
//非高亮部分
notCurrentPaint = new Paint();
notCurrentPaint.setAntiAlias(true);
notCurrentPaint.setTextAlign(Paint.Align.CENTER);
}
/**
* 绘画歌词
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (canvas == null) {
return;
}
currentPaint.setColor(Color.argb(210, 251, 248, 29));
notCurrentPaint.setColor(Color.argb(140, 255, 255, 255));
notCurrentPaint.setTextAlign(Paint.Align.CENTER);
currentPaint.setTextAlign(Paint.Align.CENTER);
currentPaint.setTextSize(textMaxSize);
currentPaint.setTypeface(Typeface.SERIF);
notCurrentPaint.setTextSize(textSize);
notCurrentPaint.setTypeface(Typeface.DEFAULT);
//如果index的大小改变重置float
if(indexChanged!=index){
indexChanged=index;
float1 = 0;
float2 = 0;
}
//设置渲染速度
if(infos.size()!=0){
if(float2 < 1.0){
float1 += getShaderSpeed();
float2 += getShaderSpeed();
}
}
Log.i("width", float1 + "");
if(infos.size()!=0){
Shader shader = new LinearGradient(getBeginX(),0, getEndX(), 0, new int[] { Color.YELLOW,Color.GREEN},new float[]{float1, float2},Shader.TileMode.CLAMP);
currentPaint.setShader(shader);
//第一个参数是渲染在X轴的起始位置,第二个是Y轴的起始位置,第三个是X轴的结束位置,第四个是Y轴的结束位置,第五个参数传入一组颜色,数组中的第一个颜色就是渲染后的颜色,第六个参数就是渲染的位置,1.0f表示全部渲染,最后一个参数类似于一些特效吧,<div style="">//可以看出渲染歌词的关键点就是第六个参数了,传入的位置不同渲染的位置就不同,因此我们只需要不停的调用ondraw方法,每次改变float的位置即可,</div>
}
float tempY = height / 2;
tempY = tempY + textHeight;
try {
setText("");
if(infos.size()!=0){//判断当前有没有歌词信息
if(infos.get(index).getCurrentContent().equals("")){
//当前时间段没有歌词
canvas.drawText("......", width /2, height/2 , currentPaint);
}else{
canvas.drawText(infos.get(index)
.getCurrentContent(), width/2 , height /2, currentPaint);
canvas.drawText(infos.get(index+1)
.getCurrentContent(), width/2 , tempY , notCurrentPaint);
}
}else{
canvas.drawText("暂无歌词", width /2, height/2 , currentPaint);
}
} catch (Exception e) {
setText("");
setText("正在加载歌词...");
}
}
/**
* 当view大小改变的时候调用的方法
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.width = w;
this.height = h;
}
//返回渲染的速度
public float getShaderSpeed(){
long time;
if(index==0){
time = infos.get(index).getCurrentTime();
}else{
time = infos.get(index).getCurrentTime()-infos.get(index-1).getCurrentTime();
}
float speed = (float)1.0/time*10;
return speed;
}
//返回渲染起始点x位置
public float getBeginX(){
return width/2-getCharacterWidth(infos.get(index).getCurrentContent(),60)/2;
}
//返回渲染结束点x位置
public float getEndX(){
return width/2+getCharacterWidth(infos.get(index).getCurrentContent(),60)/2;
}
//获取当前歌词文本的宽度
public float getCharacterWidth(String text,float size){
if(text==null||text.equals("")){
return 0;
}
Paint paint = new Paint();
paint.setTextSize(size);
float text_width= paint.measureText(text);//得到总体长度
return text_width;
}
}