Android 歌词显示

将相本无种,男儿当自强。这篇文章主要讲述Android 歌词显示相关的知识,希望能为你提供帮助。
 
一.概述 
项目中设计到歌词显示的问题,这一块之前没有涉及过,只是套用过一个开源的项目,效果还行,于是想到拿来稍作修改,以适应项目需求.
二.歌词控件先来看下这个自定义控件写的歌词控件吧:

public class LrcView extends View implements ILrcView { /** * 所有的歌词 ***/ private List< LrcRow> mLrcRows; /** * 无歌词数据的时候 显示的默认文字 **/ private static final String DEFAULT_TEXT = "*暂未获取到歌词*"; /** * 默认文字的字体大小 **/ private static final float SIZE_FOR_DEFAULT_TEXT = CommonUtils.dip2px(MyApplication.getContext(), 28); /** * 画高亮歌词的画笔 ***/ private Paint mPaintForHighLightLrc; /** * 高亮歌词的默认字体大小 ***/ private static final float DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC = CommonUtils.dip2px(MyApplication.getContext(), 32); /** * 高亮歌词当前的字体大小 ***/ private float mCurSizeForHightLightLrc = DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC; /** * 高亮歌词的默认字体颜色 **/ private static final int DEFAULT_COLOR_FOR_HIGHT_LIGHT_LRC = 0xffffffff; /** * 高亮歌词当前的字体颜色 **/ private int mCurColorForHightLightLrc = DEFAULT_COLOR_FOR_HIGHT_LIGHT_LRC; /** * 画其他歌词的画笔 ***/ private Paint mPaintForOtherLrc; /** * 其他歌词的默认字体大小 ***/ private static final float DEFAULT_SIZE_FOR_OTHER_LRC = CommonUtils.dip2px(MyApplication.getContext(), 28); /** * 其他歌词当前的字体大小 ***/ private float mCurSizeForOtherLrc = DEFAULT_SIZE_FOR_OTHER_LRC; /** * 其他歌词的默认字体颜色 **/ private static final int DEFAULT_COLOR_FOR_OTHER_LRC = 0x66ffffff; /** * 其他歌词当前的字体颜色 **/ private int mCurColorForOtherLrc = DEFAULT_COLOR_FOR_OTHER_LRC; /** * 画时间线的画笔 ***/ private Paint mPaintForTimeLine; /*** * 时间线的颜色 **/ private static final int COLOR_FOR_TIME_LINE = 0xff999999; /** * 时间文字大小 **/ private static final int SIZE_FOR_TIME = CommonUtils.dip2px(MyApplication.getContext(), 12); /** * 是否画时间线 **/ private boolean mIsDrawTimeLine = false; /** * 歌词间默认的行距 **/ private static final float DEFAULT_PADDING = CommonUtils.dip2px(MyApplication.getContext(), 17); /** * 歌词当前的行距 **/ private float mCurPadding = DEFAULT_PADDING; /** * 歌词的最大缩放比例 **/ public static final float MAX_SCALING_FACTOR = 1.5f; /** * 歌词的最小缩放比例 **/ public static final float MIN_SCALING_FACTOR = 0.5f; /** * 默认缩放比例 **/ private static final float DEFAULT_SCALING_FACTOR = 1.0f; /** * 歌词的当前缩放比例 **/ private float mCurScalingFactor = DEFAULT_SCALING_FACTOR; /** * 实现歌词竖直方向平滑滚动的辅助对象 **/ private Scroller mScroller; /*** * 移动一句歌词的持续时间 **/ private static final int DURATION_FOR_LRC_SCROLL = 500; /*** * 停止触摸时 如果View需要滚动 时的持续时间 **/ private static final int DURATION_FOR_ACTION_UP = 400; /** * 控制文字缩放的因子 **/ private float mCurFraction = 0; private int mTouchSlop; private Bitmap arrowBitmap; public LrcView(Context context) { super(context); init(context); }public LrcView(Context context, AttributeSet attrs) { super(context, attrs); init(context); }/** * 初始化画笔等 */ @Override public void init(Context context) { mScroller = new Scroller(getContext()); mPaintForHighLightLrc = new Paint(); //mPaintForHighLightLrc.setShadowLayer(5,0,0, Color.parseColor("#66ffffff")); mPaintForHighLightLrc.setColor(mCurColorForHightLightLrc); mPaintForHighLightLrc.setTextSize(mCurSizeForHightLightLrc); mPaintForHighLightLrc.setAntiAlias(true); mPaintForOtherLrc = new Paint(); mPaintForOtherLrc.setColor(mCurColorForOtherLrc); mPaintForOtherLrc.setTextSize(mCurSizeForOtherLrc); mPaintForOtherLrc.setAntiAlias(true); mPaintForTimeLine = new Paint(); mPaintForTimeLine.setColor(COLOR_FOR_TIME_LINE); mPaintForTimeLine.setTextSize(SIZE_FOR_TIME); mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inDensity = 30; options.inTargetDensity = 30; arrowBitmap = BitmapFactory.decodeResource(context.getResources(), R.raw.lrc_arrow, options); }private int mTotleDrawRow; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (mLrcRows == null || mLrcRows.size() == 0) { //画默认的显示文字 mPaintForOtherLrc.setTextSize(SIZE_FOR_DEFAULT_TEXT); float textWidth = mPaintForOtherLrc.measureText(DEFAULT_TEXT); float textX = (getWidth() - textWidth) / 2; canvas.drawText(DEFAULT_TEXT, textX, getHeight() / 2, mPaintForOtherLrc); return; } if (mTotleDrawRow == 0) { //初始化将要绘制的歌词行数 mTotleDrawRow = (int) (getHeight() / (mCurSizeForOtherLrc + mCurPadding)) + 4; } //因为不需要将所有歌词画出来 int minRaw = mCurRow - (mTotleDrawRow - 1) / 2; int maxRaw = mCurRow + (mTotleDrawRow - 1) / 2; minRaw = Math.max(minRaw, 0); //处理上边界 maxRaw = Math.min(maxRaw, mLrcRows.size() - 1); //处理下边界 //实现渐变的最大歌词行数 int count = Math.max(maxRaw - mCurRow, mCurRow - minRaw); if (count == 0) { return; } //两行歌词间字体颜色变化的透明度 int alpha = (0xFF - 0x11) / count; //画出来的第一行歌词的y坐标 float rowY = getHeight() / 2 + minRaw * (mCurSizeForOtherLrc + mCurPadding); for (int i = minRaw; i < = maxRaw; i++) {if (i == mCurRow) {//画高亮歌词 //因为有缩放效果,所有需要动态设置歌词的字体大小 float textSize = mCurSizeForOtherLrc + (mCurSizeForHightLightLrc - mCurSizeForOtherLrc) * mCurFraction; mPaintForHighLightLrc.setTextSize(textSize); String text = mLrcRows.get(i).getContent(); //获取到高亮歌词 float textWidth = mPaintForHighLightLrc.measureText(text); //用画笔测量歌词的宽度 if (textWidth > getWidth()) { //如果歌词宽度大于view的宽,则需要动态设置歌词的起始x坐标,以实现水平滚动 canvas.drawText(text, mCurTextXForHighLightLrc, rowY, mPaintForHighLightLrc); } else { //如果歌词宽度小于view的宽,则让歌词居中显示 float textX = (getWidth() - textWidth) / 2; canvas.drawText(text, textX, rowY, mPaintForHighLightLrc); } } else { if (i == mLastRow) {//画高亮歌词的上一句 //因为有缩放效果,所有需要动态设置歌词的字体大小 float textSize = mCurSizeForHightLightLrc - (mCurSizeForHightLightLrc - mCurSizeForOtherLrc) * mCurFraction; mPaintForOtherLrc.setTextSize(textSize); } else {//画其他的歌词 mPaintForOtherLrc.setTextSize(mCurSizeForOtherLrc); } String text = mLrcRows.get(i).getContent(); float textWidth = mPaintForOtherLrc.measureText(text); float textX = (getWidth() - textWidth) / 2; //如果计算出的textX为负数,将textX置为0(实现:如果歌词宽大于view宽,则居左显示,否则居中显示) textX = Math.max(textX, 0); //实现颜色渐变从0xFFFFFFFF 逐渐变为 0x11FFFFFF(颜色还是白色,只是透明度变化) int curAlpha = 255 - (Math.abs(i - mCurRow) - 1) * alpha; //求出当前歌词颜色的透明度 //mPaintForOtherLrc.setColor(0x1000000*curAlpha+0xffffff); canvas.drawText(text, textX, rowY, mPaintForOtherLrc); } //计算出下一行歌词绘制的y坐标 rowY += mCurSizeForOtherLrc + mCurPadding; }//画时间线和时间 if (mIsDrawTimeLine) { float y = getHeight() / 2 + getScrollY(); float x = getWidth(); canvas.drawBitmap(arrowBitmap, -20, y - 41, null); canvas.drawText(mLrcRows.get(mCurRow).getTimeStr().substring(0, 5), x - 105, y + 13, mPaintForTimeLine); canvas.drawLine(60, y, getWidth() - 110, y, mPaintForTimeLine); }}/** * 是否可拖动歌词 **/ private boolean canDrag = false; /** * 事件的第一次的y坐标 **/ private float firstY; /** * 事件的上一次的y坐标 **/ private float lastY; private float lastX; @Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: firstY = event.getRawY(); lastX = event.getRawX(); break; case MotionEvent.ACTION_MOVE: if (mLrcRows == null || mLrcRows.size() == 0) { return false; } if (!canDrag) { if (Math.abs(event.getRawY() - firstY) > mTouchSlop & & Math.abs(event.getRawY() - firstY) > Math.abs(event.getRawX() - lastX)) { canDrag = true; mIsDrawTimeLine = true; mScroller.forceFinished(true); stopScrollLrc(); mCurFraction = 1; } lastY = event.getRawY(); }if (canDrag) { float offset = event.getRawY() - lastY; //偏移量 if (getScrollY() - offset < 0) { if (offset > 0) { offset = offset / 3; } } else if (getScrollY() - offset > mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding) { if (offset < 0) { offset = offset / 3; } } scrollBy(getScrollX(), -(int) offset); lastY = event.getRawY(); int currentRow = (int) (getScrollY() / (mCurSizeForOtherLrc + mCurPadding)); currentRow = Math.min(currentRow, mLrcRows.size() - 1); currentRow = Math.max(currentRow, 0); seekTo(mLrcRows.get(currentRow).getTime(), false, false); return true; } lastY = event.getRawY(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (!canDrag) { if (onLrcClickListener != null) { onLrcClickListener.onClick(); } } else { if (onSeekToListener != null & & mCurRow != -1) { onSeekToListener.onSeekTo(mLrcRows.get(mCurRow).getTime()); } if (getScrollY() < 0) { smoothScrollTo(0, DURATION_FOR_ACTION_UP); } else if (getScrollY() > mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding) { smoothScrollTo((int) (mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding), DURATION_FOR_ACTION_UP); }canDrag = false; mIsDrawTimeLine = false; invalidate(); } break; } return true; }/** * 为LrcView设置歌词List集合数据 */ @Override public void setLrcRows(List< LrcRow> lrcRows) { reset(); this.mLrcRows = lrcRows; invalidate(); }/** * 当前高亮歌词的行号 **/ private int mCurRow = -1; /** * 上一次的高亮歌词的行号 **/ private int mLastRow = -1; @Override public void seekTo(int progress, boolean fromSeekBar, boolean fromSeekBarByUser) { if (mLrcRows == null || mLrcRows.size() == 0) { return; } //如果是由seekbar的进度改变触发 并且这时候处于拖动状态,则返回 if (fromSeekBar & & canDrag) { return; } for (int i = mLrcRows.size() - 1; i > = 0; i--) {if (progress > = mLrcRows.get(i).getTime()) { if (mCurRow != i) { mLastRow = mCurRow; mCurRow = i; log("mCurRow=i=" + mCurRow); if (fromSeekBarByUser) { if (!mScroller.isFinished()) { mScroller.forceFinished(true); } scrollTo(getScrollX(), (int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding))); } else { smoothScrollTo((int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)), DURATION_FOR_LRC_SCROLL); } //如果高亮歌词的宽度大于View的宽,就需要开启属性动画,让它水平滚动 float textWidth = mPaintForHighLightLrc.measureText(mLrcRows.get(mCurRow).getContent()); log("textWidth=" + textWidth + "getWidth()=" + getWidth()); if (textWidth > getWidth()) { if (fromSeekBarByUser) { mScroller.forceFinished(true); } log("开始水平滚动歌词:" + mLrcRows.get(mCurRow).getContent()); startScrollLrc(getWidth() - textWidth, (long) (mLrcRows.get(mCurRow).getTotalTime() * 0.6)); } invalidate(); } break; } }}/** * 控制歌词水平滚动的属性动画 ***/ private ValueAnimator mAnimator; /** * 开始水平滚动歌词 * * @param endX歌词第一个字的最终的x坐标 * @param duration 滚动的持续时间 */private void startScrollLrc(float endX, long duration) { if (mAnimator == null) { mAnimator = ValueAnimator.ofFloat(0, endX); mAnimator.addUpdateListener(updateListener); } else { mCurTextXForHighLightLrc = 0; mAnimator.cancel(); mAnimator.setFloatValues(0, endX); } mAnimator.setDuration(duration); //mAnimator.setStartDelay((long) (duration * 0.2)); //延迟执行属性动画 mAnimator.start(); }/** * 停止歌词的滚动 */ private void stopScrollLrc() { if (mAnimator != null) { mAnimator.cancel(); } mCurTextXForHighLightLrc = 0; }/** * 高亮歌词当前的其实x轴绘制坐标 **/ private float mCurTextXForHighLightLrc; /*** * 监听属性动画的数值值的改变 */ AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {@Override public void onAnimationUpdate(ValueAnimator animation) { mCurTextXForHighLightLrc = (Float) animation.getAnimatedValue(); log("mCurTextXForHighLightLrc=" + mCurTextXForHighLightLrc); invalidate(); } }; /** * 设置歌词的缩放比例 */ @Override public void setLrcScalingFactor(float scalingFactor) { mCurScalingFactor = scalingFactor; mCurSizeForHightLightLrc = DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC * mCurScalingFactor; mCurSizeForOtherLrc = DEFAULT_SIZE_FOR_OTHER_LRC * mCurScalingFactor; mCurPadding = DEFAULT_PADDING * mCurScalingFactor; mTotleDrawRow = (int) (getHeight() / (mCurSizeForOtherLrc + mCurPadding)) + 3; log("mRowTotal=" + mTotleDrawRow); scrollTo(getScrollX(), (int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding))); invalidate(); mScroller.forceFinished(true); }/** * 重置 */ @Override public void reset() { if (!mScroller.isFinished()) { mScroller.forceFinished(true); } mLrcRows = null; scrollTo(getScrollX(), 0); invalidate(); }/** * 平滑的移动到某处 * * @param dstY */ private void smoothScrollTo(int dstY, int duration) { int oldScrollY = getScrollY(); int offset = dstY - oldScrollY; mScroller.startScroll(getScrollX(), oldScrollY, getScrollX(), offset, duration); invalidate(); }@Override public void computeScroll() { if (!mScroller.isFinished()) { if (mScroller.computeScrollOffset()) { int oldY = getScrollY(); int y = mScroller.getCurrY(); if (oldY != y & & !canDrag) { scrollTo(getScrollX(), y); } mCurFraction = mScroller.timePassed() * 3f / DURATION_FOR_LRC_SCROLL; mCurFraction = Math.min(mCurFraction, 1F); invalidate(); } } }/** * 返回当前的歌词缩放比例 * * @return */ public float getmCurScalingFactor() { return mCurScalingFactor; }private OnSeekToListener onSeekToListener; public void setOnSeekToListener(OnSeekToListener onSeekToListener) { this.onSeekToListener = onSeekToListener; }public interface OnSeekToListener { void onSeekTo(int progress); }private OnLrcClickListener onLrcClickListener; public void setOnLrcClickListener(OnLrcClickListener onLrcClickListener) { this.onLrcClickListener = onLrcClickListener; }public interface OnLrcClickListener { void onClick(); }public void log(Object o) { Log.d("LrcView", o + ""); } }

* 在ViewGroup里面 scrollTo,scrollBy方法移动的是子View * 在View里面scrollTo,scrollBy方法移动的是View里面绘制的内容 * 要点: * 1:歌词的上下平移用什么实现? * 用Scroller实现,Scroller只是一个工具而已, * 真正实现滚动效果的还是View的scrollTo方法 * 2:歌词的水平滚动怎么实现? * 通过属性动画ValueAnimator控制高亮歌词绘制的x轴起始坐标

歌词与播放进度联动,只需要调用seekTo方法,原理是拿播放进度和歌词每一行前的时间作比较,只要播放进度超前,那么就滚动到歌词的相应时间上显示,理论上,只要歌词文本的时间是精确的,那么歌词就会随着
播放进度一直滚动.

触摸滑动和点击事件都是在onTouchEvent中处理的,原理是记录手指按下和抬起这段时间内Y方向的偏移量,把这个偏移量与每行文字的高度作比较,看滚动到歌词的哪个部分,然后再把播放进度调到对应的时间位置,这样
就实现了歌词进度与播放进度的完全绑定了.

当然,这些都是建立在歌词解析完成,并且获取到的前提之下,因此,歌词解析也是很重要的部分.

三.歌词解析下载的歌词文件,看过的都知道是一种时间刻度+歌词字符串的形式; 例如
Android 歌词显示

文章图片

 
这样的,既然如此,那么只需要按"["和"]"来截取字符串就可以了.来看下歌词解析类吧(歌词解析不少网友都分享过):
public class DefaultLrcParser implements ILrcParser { private static final DefaultLrcParser istance = new DefaultLrcParser(); public static final DefaultLrcParser getIstance() { return istance; }private DefaultLrcParser() { }/*** * 将歌词文件里面的字符串 解析成一个List< LrcRow> */ @Override public List< LrcRow> getLrcRows(String str) {if (TextUtils.isEmpty(str)) { return null; } BufferedReader br = new BufferedReader(new StringReader(str)); List< LrcRow> lrcRows = new ArrayList< > (); String lrcLine; try { while ((lrcLine = br.readLine()) != null) { List< LrcRow> rows = LrcRow.createRows(lrcLine); if (rows != null & & rows.size() > 0) { lrcRows.addAll(rows); } } Collections.sort(lrcRows); int len = lrcRows.size(); for (int i = 0; i < len - 1; i++) { lrcRows.get(i).setTotalTime(lrcRows.get(i + 1).getTime() - lrcRows.get(i).getTime()); } lrcRows.get(len - 1).setTotalTime(5000); } catch (Exception e) { e.printStackTrace(); return null; } finally { if (br != null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } }return lrcRows; }

【Android 歌词显示】歌词的实体类:
/** * 每行歌词的实体类,实现了Comparable接口,方便List< LrcRow> 的sort排序 * * @author Ligang2014/8/19 */ public class LrcRow implements Comparable< LrcRow> {/** * 开始时间 为00:10:00 ***/ private String timeStr; /** * 开始时间 毫米数00:10:00为10000 **/ private int time; /** * 歌词内容 **/ private String content; /** * 该行歌词显示的总时间 **/ private int totalTime; public long getTotalTime() { return totalTime; }public void setTotalTime(int totalTime) { this.totalTime = totalTime; }public String getTimeStr() { return timeStr; }public void setTimeStr(String timeStr) { this.timeStr = timeStr; }public int getTime() { return time; }public void setTime(int time) { this.time = time; }public String getContent() { return content; }public void setContent(String content) { this.content = content; }public LrcRow() { super(); }public LrcRow(String timeStr, int time, String content) { super(); this.timeStr = timeStr; this.time = time; this.content = content; }/** * 将歌词文件中的某一行 解析成一个List< LrcRow> * 因为一行中可能包含了多个LrcRow对象 * 比如[03:33.02][00:36.37]当鸽子不再象征和平,就包含了2个对象 * * @param lrcLine * @return */ public static final List< LrcRow> createRows(String lrcLine) { if (!lrcLine.startsWith("[")) { return null; } //最后一个"]" int lastIndexOfRightBracket = lrcLine.lastIndexOf("]"); //歌词内容 String content = lrcLine.substring(lastIndexOfRightBracket + 1, lrcLine.length()); //截取出歌词时间,并将"[" 和"]" 替换为"-"[off

    推荐阅读