基本功能:
1.播放音乐跟歌词同步
2.根据手势,移动歌词
3.音乐和拖动条同步
点击下载地址
下面贴出核心的歌词视图代码:
/** * 歌词视图 */ public class LRCView extends View implements android.view.GestureDetector.OnGestureListener { private String Tag = "LRCView"; private Context context; // 手势监听类 private GestureDetector gesDet; private List<LrcBean> lrcList; private LrcBean lrcBean; private Paint curPaint; private Paint nearPaint; // 当前行数 private int curLine = 0; // 字体大小 private int wSize = 0; // 行间隔 private int wInterval = 15; // 当前时间 private long curTime = 0; // 移动歌词 private boolean moveTouch = false; public LRCView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // TODO Auto-generated constructor stub this.context = context; init(); } public LRCView(Context context, AttributeSet attrs) { super(context, attrs); // TODO Auto-generated constructor stub this.context = context; init(); } public LRCView(Context context) { super(context); // TODO Auto-generated constructor stub this.context = context; init(); } public List<LrcBean> getLrcList() { return lrcList; } public void setLrcList(List<LrcBean> lrcList) { this.lrcList = lrcList; } public int getwSize() { return wSize; } public void setwSize(int wSize) { this.wSize = wSize; // 重新修改字体大小 间距 curPaint.setTextSize(wSize); nearPaint.setTextSize(wSize > 15 ? wSize - 5 : 6); wInterval = wSize > 15 ? wSize - 10 : 6; } public int getCurLine() { return curLine; } public void setCurLine(int curLine) { this.curLine = curLine; } public boolean isMoveTouch() { return moveTouch; } public void setMoveTouch(boolean moveTouch) { this.moveTouch = moveTouch; } public void init() { curPaint = new Paint(); // 设置文本对其 居中 curPaint.setTextAlign(Paint.Align.CENTER); // 设置颜色 curPaint.setColor(Color.GREEN); // 抗锯齿 curPaint.setAntiAlias(true); // 设定使用图像抖动处理 curPaint.setDither(true); // 透明度 curPaint.setAlpha(180); // 字体大小 curPaint.setTextSize(wSize); nearPaint = new Paint(); nearPaint.setTextAlign(Paint.Align.CENTER); nearPaint.setColor(Color.BLUE); nearPaint.setAntiAlias(true); nearPaint.setDither(true); nearPaint.setAlpha(180); nearPaint.setTextSize(wSize > 15 ? wSize - 5 : 6); gesDet = new GestureDetector(this); } @Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub super.onDraw(canvas); // 判空处理 ,当触摸屏幕滑动手势的时候 停止播放 if (null == lrcList) { return; } // 当前正在唱的行 lrcBean = lrcList.get(curLine); canvas.drawText(lrcBean.getLrcSentence(), this.getWidth() / 2, this.getHeight() / 2, curPaint); // 当移动歌词时 显示线条 long cur = Utils.getCurTimeMillions(); if (moveTouch) { float[] arr = Utils.getLinesArr(getWidth(), getHeight()); canvas.drawLines(arr, 0, arr.length, curPaint); // 暂停播放 ((MainActivity) context).setPausePlayer(); } else { // 播放 ((MainActivity) context).setStartPlayer(); } // 一半的行数,跟顶和底留一行的空隙 int half = (int) (getHeight() / 2 / (wSize + wInterval)); // 绘制上下的行 for (int i = 1; i < half; i++) { if (curLine - i >= 0) { lrcBean = lrcList.get(curLine - i); canvas.drawText(lrcBean.getLrcSentence(), getWidth() / 2, getHeight() / 2 - (wSize + wInterval) * i, nearPaint); } if (curLine + i < lrcList.size()) { lrcBean = lrcList.get(curLine + i); canvas.drawText(lrcBean.getLrcSentence(), getWidth() / 2, getHeight() / 2 + (wSize + wInterval) * i, nearPaint); } } } @Override public boolean onTouchEvent(MotionEvent event) { // TODO Auto-generated method stub // 将事件交给手势管理 gesDet.onTouchEvent(event); return true; } // 手指按下 @Override public boolean onDown(MotionEvent e) { // TODO Auto-generated method stub return false; } // 手指飞速滑动 /** * e1:初次触发 e2:每次触发 x ,y:滑动的移动速度(上滑为 负,而歌词行数为正) */ @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // TODO Auto-generated method stub // 隐藏拖动条 ((MainActivity) context).setVisibSB(false); // 行变化量 int line = 0; // 行数变化量限制 line = (int) (velocityY / 3); // 最大的变化行数 为5 if (Math.abs(line) > 5) { line = -Math.abs(line) / line * 5; } Log.d(Tag, "Fling:" + line); if (curLine + line >= 0 && curLine + line < lrcList.size()) { ((MainActivity) context).setCurLine(line); } curTime = Utils.getCurTimeMillions(); return false; } // 手指滑动 100s返回一个状态 一般手指距离 10-30 /** * e1:初次触发 e2:每次触发 x ,y:两次滑动的间距(start-end) */ @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // TODO Auto-generated method stub ((MainActivity) context).setVisibSB(false); int line = 0; // 行数变化量限制 line = (int) (distanceY / 3); // 最大的变化行数 为5 if (Math.abs(line) > 5) { line = Math.abs(line) / line * 5; } Log.d(Tag, "Scroll:" + line); if (curLine + line >= 0 && curLine + line < lrcList.size()) { ((MainActivity) context).setCurLine(line); } curTime = Utils.getCurTimeMillions(); return false; } // 手指单机抬起 @Override public boolean onSingleTapUp(MotionEvent e) { // TODO Auto-generated method stub if (((MainActivity) context).isVisibSB()) { ((MainActivity) context).setVisibSB(false); } else { ((MainActivity) context).setVisibSB(true); } return false; } @Override public void onLongPress(MotionEvent e) { // TODO Auto-generated method stub } @Override public void onShowPress(MotionEvent e) { // TODO Auto-generated method stub } }
下面是运行效果:
正常运行:
单击屏幕(会显示拖动条):
手势滑动(会显示线条):
在活动中开启了个死循环的线程,不断的去刷新这个歌词视图组件,因此组件内就是实现在特定的时间做出响应就可以了,主要是在onDraw()中绘制,对手势的处理是交给GestureDetector类进行处理的(直接在onTouchEvent()中写逻辑处理,在灵敏度和滑动屏幕的效果上,不是很好)。
在GestureDetector的onFling()中得到的速度和GestureDetector的onScroll()得到的距离,在正负关系上是相反的,在变化上要一致。而且这两个方法执行起来,没有太大的差别(实际上两点触摸点的数据,不同的侧重),基本是交替运行的。
// 行数变化量限制 line = (int) (velocityY / 3); // 最大的变化行数 为5 if (Math.abs(line) > 5) { line = -Math.abs(line) / line * 5; }
手势滑动的时候,有不同的速度和距离,在适当的数据范围内(要排除那些数值偏大或者偏小的值),我们就可以认为手势移动了多少行,这个没有固定,多实验几次,觉得合理就可以了。
计算得到绘制直接的数组,从而在手势滑动的时候,绘制当前行下面的线。
// 向上滚动 这个是根据歌词的时间长短来滚动,整体上移 canvas.translate(0, -plus); // 先画当前行,之后再画他的前面和后面,这样就保持当前行在中间的位置 try { canvas.drawText(Sentencelist.get(index).getContent(), width / 2, height / 2, CurrentPaint); float tempY = height / 2; // 画出本句之前的句子 for (int i = index - 1; i >= 0; i--) { // Sentence sen = list.get(i); // 向上推移 tempY = tempY - TextHeight; if (tempY < 0) { break; } canvas.drawText(Sentencelist.get(i).getContent(), width / 2, tempY, NotCurrentPaint); } tempY = height / 2; // 画出本句之后的句子 for (int i = index + 1; i < Sentencelist.size(); i++) { // 往下推移 tempY = tempY + TextHeight; if (tempY > height) { break; } canvas.drawText(Sentencelist.get(i).getContent(), width / 2, tempY, NotCurrentPaint); }显然,对于歌词的移动,也可以通过动画移动来实现,这里没采用这种方法,权当提供一条思路和方法。
说明:
1.我的手机改了系统字体的,所以是这种效果,android原生系统是不带这种字体的。