一.概述

  项目中设计到歌词显示的问题,这一块之前没有涉及过,只是套用过一个开源的项目,效果还行,于是想到拿来稍作修改,以适应项目需求.

二.歌词控件

  先来看下这个自定义控件写的歌词控件吧:

  1. public class LrcView extends View implements ILrcView {
  2. /**
  3. * 所有的歌词
  4. ***/
  5. private List<LrcRow> mLrcRows;
  6. /**
  7. * 无歌词数据的时候 显示的默认文字
  8. **/
  9. private static final String DEFAULT_TEXT = "*暂未获取到歌词*";
  10. /**
  11. * 默认文字的字体大小
  12. **/
  13. private static final float SIZE_FOR_DEFAULT_TEXT = CommonUtils.dip2px(MyApplication.getContext(), 28);
  14.  
  15. /**
  16. * 画高亮歌词的画笔
  17. ***/
  18. private Paint mPaintForHighLightLrc;
  19. /**
  20. * 高亮歌词的默认字体大小
  21. ***/
  22. private static final float DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC = CommonUtils.dip2px(MyApplication.getContext(), 32);
  23. /**
  24. * 高亮歌词当前的字体大小
  25. ***/
  26. private float mCurSizeForHightLightLrc = DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC;
  27. /**
  28. * 高亮歌词的默认字体颜色
  29. **/
  30. private static final int DEFAULT_COLOR_FOR_HIGHT_LIGHT_LRC = 0xffffffff;
  31.  
  32. /**
  33. * 高亮歌词当前的字体颜色
  34. **/
  35. private int mCurColorForHightLightLrc = DEFAULT_COLOR_FOR_HIGHT_LIGHT_LRC;
  36.  
  37. /**
  38. * 画其他歌词的画笔
  39. ***/
  40. private Paint mPaintForOtherLrc;
  41. /**
  42. * 其他歌词的默认字体大小
  43. ***/
  44. private static final float DEFAULT_SIZE_FOR_OTHER_LRC = CommonUtils.dip2px(MyApplication.getContext(), 28);
  45. /**
  46. * 其他歌词当前的字体大小
  47. ***/
  48. private float mCurSizeForOtherLrc = DEFAULT_SIZE_FOR_OTHER_LRC;
  49. /**
  50. * 其他歌词的默认字体颜色
  51. **/
  52. private static final int DEFAULT_COLOR_FOR_OTHER_LRC = 0x66ffffff;
  53. /**
  54. * 其他歌词当前的字体颜色
  55. **/
  56. private int mCurColorForOtherLrc = DEFAULT_COLOR_FOR_OTHER_LRC;
  57.  
  58. /**
  59. * 画时间线的画笔
  60. ***/
  61. private Paint mPaintForTimeLine;
  62. /***
  63. * 时间线的颜色
  64. **/
  65. private static final int COLOR_FOR_TIME_LINE = 0xff999999;
  66. /**
  67. * 时间文字大小
  68. **/
  69. private static final int SIZE_FOR_TIME = CommonUtils.dip2px(MyApplication.getContext(), 12);
  70. /**
  71. * 是否画时间线
  72. **/
  73. private boolean mIsDrawTimeLine = false;
  74.  
  75. /**
  76. * 歌词间默认的行距
  77. **/
  78. private static final float DEFAULT_PADDING = CommonUtils.dip2px(MyApplication.getContext(), 17);
  79. /**
  80. * 歌词当前的行距
  81. **/
  82. private float mCurPadding = DEFAULT_PADDING;
  83.  
  84. /**
  85. * 歌词的最大缩放比例
  86. **/
  87. public static final float MAX_SCALING_FACTOR = 1.5f;
  88. /**
  89. * 歌词的最小缩放比例
  90. **/
  91. public static final float MIN_SCALING_FACTOR = 0.5f;
  92. /**
  93. * 默认缩放比例
  94. **/
  95. private static final float DEFAULT_SCALING_FACTOR = 1.0f;
  96. /**
  97. * 歌词的当前缩放比例
  98. **/
  99. private float mCurScalingFactor = DEFAULT_SCALING_FACTOR;
  100.  
  101. /**
  102. * 实现歌词竖直方向平滑滚动的辅助对象
  103. **/
  104. private Scroller mScroller;
  105. /***
  106. * 移动一句歌词的持续时间
  107. **/
  108. private static final int DURATION_FOR_LRC_SCROLL = 500;
  109. /***
  110. * 停止触摸时 如果View需要滚动 时的持续时间
  111. **/
  112. private static final int DURATION_FOR_ACTION_UP = 400;
  113.  
  114. /**
  115. * 控制文字缩放的因子
  116. **/
  117. private float mCurFraction = 0;
  118. private int mTouchSlop;
  119.  
  120. private Bitmap arrowBitmap;
  121.  
  122. public LrcView(Context context) {
  123. super(context);
  124.  
  125. init(context);
  126. }
  127.  
  128. public LrcView(Context context, AttributeSet attrs) {
  129. super(context, attrs);
  130. init(context);
  131. }
  132.  
  133. /**
  134. * 初始化画笔等
  135. */
  136. @Override
  137. public void init(Context context) {
  138. mScroller = new Scroller(getContext());
  139. mPaintForHighLightLrc = new Paint();
  140. // mPaintForHighLightLrc.setShadowLayer(5,0,0, Color.parseColor("#66ffffff"));
  141. mPaintForHighLightLrc.setColor(mCurColorForHightLightLrc);
  142. mPaintForHighLightLrc.setTextSize(mCurSizeForHightLightLrc);
  143. mPaintForHighLightLrc.setAntiAlias(true);
  144.  
  145. mPaintForOtherLrc = new Paint();
  146. mPaintForOtherLrc.setColor(mCurColorForOtherLrc);
  147. mPaintForOtherLrc.setTextSize(mCurSizeForOtherLrc);
  148. mPaintForOtherLrc.setAntiAlias(true);
  149.  
  150. mPaintForTimeLine = new Paint();
  151. mPaintForTimeLine.setColor(COLOR_FOR_TIME_LINE);
  152. mPaintForTimeLine.setTextSize(SIZE_FOR_TIME);
  153.  
  154. mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
  155.  
  156. BitmapFactory.Options options = new BitmapFactory.Options();
  157. options.inDensity = 30;
  158. options.inTargetDensity = 30;
  159. arrowBitmap = BitmapFactory.decodeResource(context.getResources(), R.raw.lrc_arrow, options);
  160. }
  161.  
  162. private int mTotleDrawRow;
  163.  
  164. @Override
  165. protected void onDraw(Canvas canvas) {
  166. super.onDraw(canvas);
  167. if (mLrcRows == null || mLrcRows.size() == 0) {
  168. //画默认的显示文字
  169. mPaintForOtherLrc.setTextSize(SIZE_FOR_DEFAULT_TEXT);
  170. float textWidth = mPaintForOtherLrc.measureText(DEFAULT_TEXT);
  171. float textX = (getWidth() - textWidth) / 2;
  172. canvas.drawText(DEFAULT_TEXT, textX, getHeight() / 2, mPaintForOtherLrc);
  173. return;
  174. }
  175. if (mTotleDrawRow == 0) {
  176. //初始化将要绘制的歌词行数
  177. mTotleDrawRow = (int) (getHeight() / (mCurSizeForOtherLrc + mCurPadding)) + 4;
  178. }
  179. //因为不需要将所有歌词画出来
  180. int minRaw = mCurRow - (mTotleDrawRow - 1) / 2;
  181. int maxRaw = mCurRow + (mTotleDrawRow - 1) / 2;
  182. minRaw = Math.max(minRaw, 0); //处理上边界
  183. maxRaw = Math.min(maxRaw, mLrcRows.size() - 1); //处理下边界
  184. //实现渐变的最大歌词行数
  185. int count = Math.max(maxRaw - mCurRow, mCurRow - minRaw);
  186. if (count == 0) {
  187. return;
  188. }
  189. //两行歌词间字体颜色变化的透明度
  190. int alpha = (0xFF - 0x11) / count;
  191. //画出来的第一行歌词的y坐标
  192. float rowY = getHeight() / 2 + minRaw * (mCurSizeForOtherLrc + mCurPadding);
  193. for (int i = minRaw; i <= maxRaw; i++) {
  194.  
  195. if (i == mCurRow) {//画高亮歌词
  196. //因为有缩放效果,所有需要动态设置歌词的字体大小
  197. float textSize = mCurSizeForOtherLrc + (mCurSizeForHightLightLrc - mCurSizeForOtherLrc) * mCurFraction;
  198. mPaintForHighLightLrc.setTextSize(textSize);
  199.  
  200. String text = mLrcRows.get(i).getContent();//获取到高亮歌词
  201. float textWidth = mPaintForHighLightLrc.measureText(text);//用画笔测量歌词的宽度
  202. if (textWidth > getWidth()) {
  203. //如果歌词宽度大于view的宽,则需要动态设置歌词的起始x坐标,以实现水平滚动
  204. canvas.drawText(text, mCurTextXForHighLightLrc, rowY, mPaintForHighLightLrc);
  205. } else {
  206. //如果歌词宽度小于view的宽,则让歌词居中显示
  207. float textX = (getWidth() - textWidth) / 2;
  208. canvas.drawText(text, textX, rowY, mPaintForHighLightLrc);
  209. }
  210. } else {
  211. if (i == mLastRow) {//画高亮歌词的上一句
  212. //因为有缩放效果,所有需要动态设置歌词的字体大小
  213. float textSize = mCurSizeForHightLightLrc - (mCurSizeForHightLightLrc - mCurSizeForOtherLrc) * mCurFraction;
  214. mPaintForOtherLrc.setTextSize(textSize);
  215. } else {//画其他的歌词
  216. mPaintForOtherLrc.setTextSize(mCurSizeForOtherLrc);
  217. }
  218. String text = mLrcRows.get(i).getContent();
  219. float textWidth = mPaintForOtherLrc.measureText(text);
  220. float textX = (getWidth() - textWidth) / 2;
  221. //如果计算出的textX为负数,将textX置为0(实现:如果歌词宽大于view宽,则居左显示,否则居中显示)
  222. textX = Math.max(textX, 0);
  223. //实现颜色渐变 从0xFFFFFFFF 逐渐变为 0x11FFFFFF(颜色还是白色,只是透明度变化)
  224. int curAlpha = 255 - (Math.abs(i - mCurRow) - 1) * alpha; //求出当前歌词颜色的透明度
  225. //mPaintForOtherLrc.setColor(0x1000000*curAlpha+0xffffff);
  226. canvas.drawText(text, textX, rowY, mPaintForOtherLrc);
  227. }
  228. //计算出下一行歌词绘制的y坐标
  229. rowY += mCurSizeForOtherLrc + mCurPadding;
  230. }
  231.  
  232. //画时间线和时间
  233. if (mIsDrawTimeLine) {
  234. float y = getHeight() / 2 + getScrollY();
  235. float x = getWidth();
  236. canvas.drawBitmap(arrowBitmap, -20, y - 41, null);
  237. canvas.drawText(mLrcRows.get(mCurRow).getTimeStr().substring(0, 5), x - 105, y + 13, mPaintForTimeLine);
  238. canvas.drawLine(60, y, getWidth() - 110, y, mPaintForTimeLine);
  239. }
  240.  
  241. }
  242.  
  243. /**
  244. * 是否可拖动歌词
  245. **/
  246. private boolean canDrag = false;
  247. /**
  248. * 事件的第一次的y坐标
  249. **/
  250. private float firstY;
  251. /**
  252. * 事件的上一次的y坐标
  253. **/
  254. private float lastY;
  255. private float lastX;
  256.  
  257. @Override
  258. public boolean onTouchEvent(MotionEvent event) {
  259. switch (event.getAction()) {
  260. case MotionEvent.ACTION_DOWN:
  261. firstY = event.getRawY();
  262. lastX = event.getRawX();
  263. break;
  264. case MotionEvent.ACTION_MOVE:
  265. if (mLrcRows == null || mLrcRows.size() == 0) {
  266. return false;
  267. }
  268. if (!canDrag) {
  269. if (Math.abs(event.getRawY() - firstY) > mTouchSlop && Math.abs(event.getRawY() - firstY) > Math.abs(event.getRawX() - lastX)) {
  270. canDrag = true;
  271. mIsDrawTimeLine = true;
  272. mScroller.forceFinished(true);
  273. stopScrollLrc();
  274. mCurFraction = 1;
  275. }
  276. lastY = event.getRawY();
  277. }
  278.  
  279. if (canDrag) {
  280. float offset = event.getRawY() - lastY;//偏移量
  281. if (getScrollY() - offset < 0) {
  282. if (offset > 0) {
  283. offset = offset / 3;
  284. }
  285. } else if (getScrollY() - offset > mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding) {
  286. if (offset < 0) {
  287. offset = offset / 3;
  288. }
  289. }
  290. scrollBy(getScrollX(), -(int) offset);
  291. lastY = event.getRawY();
  292. int currentRow = (int) (getScrollY() / (mCurSizeForOtherLrc + mCurPadding));
  293. currentRow = Math.min(currentRow, mLrcRows.size() - 1);
  294. currentRow = Math.max(currentRow, 0);
  295. seekTo(mLrcRows.get(currentRow).getTime(), false, false);
  296. return true;
  297. }
  298. lastY = event.getRawY();
  299. break;
  300. case MotionEvent.ACTION_UP:
  301. case MotionEvent.ACTION_CANCEL:
  302. if (!canDrag) {
  303. if (onLrcClickListener != null) {
  304. onLrcClickListener.onClick();
  305. }
  306. } else {
  307. if (onSeekToListener != null && mCurRow != -1) {
  308. onSeekToListener.onSeekTo(mLrcRows.get(mCurRow).getTime());
  309. }
  310. if (getScrollY() < 0) {
  311. smoothScrollTo(0, DURATION_FOR_ACTION_UP);
  312. } else if (getScrollY() > mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding) {
  313. smoothScrollTo((int) (mLrcRows.size() * (mCurSizeForOtherLrc + mCurPadding) - mCurPadding), DURATION_FOR_ACTION_UP);
  314. }
  315.  
  316. canDrag = false;
  317. mIsDrawTimeLine = false;
  318. invalidate();
  319. }
  320. break;
  321. }
  322. return true;
  323. }
  324.  
  325. /**
  326. * 为LrcView设置歌词List集合数据
  327. */
  328. @Override
  329. public void setLrcRows(List<LrcRow> lrcRows) {
  330. reset();
  331. this.mLrcRows = lrcRows;
  332. invalidate();
  333. }
  334.  
  335. /**
  336. * 当前高亮歌词的行号
  337. **/
  338. private int mCurRow = -1;
  339. /**
  340. * 上一次的高亮歌词的行号
  341. **/
  342. private int mLastRow = -1;
  343.  
  344. @Override
  345. public void seekTo(int progress, boolean fromSeekBar, boolean fromSeekBarByUser) {
  346. if (mLrcRows == null || mLrcRows.size() == 0) {
  347. return;
  348. }
  349. //如果是由seekbar的进度改变触发 并且这时候处于拖动状态,则返回
  350. if (fromSeekBar && canDrag) {
  351. return;
  352. }
  353. for (int i = mLrcRows.size() - 1; i >= 0; i--) {
  354.  
  355. if (progress >= mLrcRows.get(i).getTime()) {
  356. if (mCurRow != i) {
  357. mLastRow = mCurRow;
  358. mCurRow = i;
  359. log("mCurRow=i=" + mCurRow);
  360. if (fromSeekBarByUser) {
  361. if (!mScroller.isFinished()) {
  362. mScroller.forceFinished(true);
  363. }
  364. scrollTo(getScrollX(), (int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)));
  365. } else {
  366. smoothScrollTo((int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)), DURATION_FOR_LRC_SCROLL);
  367. }
  368. //如果高亮歌词的宽度大于View的宽,就需要开启属性动画,让它水平滚动
  369. float textWidth = mPaintForHighLightLrc.measureText(mLrcRows.get(mCurRow).getContent());
  370. log("textWidth=" + textWidth + "getWidth()=" + getWidth());
  371. if (textWidth > getWidth()) {
  372. if (fromSeekBarByUser) {
  373. mScroller.forceFinished(true);
  374. }
  375. log("开始水平滚动歌词:" + mLrcRows.get(mCurRow).getContent());
  376. startScrollLrc(getWidth() - textWidth, (long) (mLrcRows.get(mCurRow).getTotalTime() * 0.6));
  377. }
  378. invalidate();
  379. }
  380. break;
  381. }
  382. }
  383.  
  384. }
  385.  
  386. /**
  387. * 控制歌词水平滚动的属性动画
  388. ***/
  389. private ValueAnimator mAnimator;
  390.  
  391. /**
  392. * 开始水平滚动歌词
  393. *
  394. * @param endX 歌词第一个字的最终的x坐标
  395. * @param duration 滚动的持续时间
  396. */
  397.  
  398. private void startScrollLrc(float endX, long duration) {
  399. if (mAnimator == null) {
  400. mAnimator = ValueAnimator.ofFloat(0, endX);
  401. mAnimator.addUpdateListener(updateListener);
  402. } else {
  403. mCurTextXForHighLightLrc = 0;
  404. mAnimator.cancel();
  405. mAnimator.setFloatValues(0, endX);
  406. }
  407. mAnimator.setDuration(duration);
  408. // mAnimator.setStartDelay((long) (duration * 0.2)); //延迟执行属性动画
  409. mAnimator.start();
  410. }
  411.  
  412. /**
  413. * 停止歌词的滚动
  414. */
  415. private void stopScrollLrc() {
  416. if (mAnimator != null) {
  417. mAnimator.cancel();
  418. }
  419. mCurTextXForHighLightLrc = 0;
  420. }
  421.  
  422. /**
  423. * 高亮歌词当前的其实x轴绘制坐标
  424. **/
  425. private float mCurTextXForHighLightLrc;
  426. /***
  427. * 监听属性动画的数值值的改变
  428. */
  429. AnimatorUpdateListener updateListener = new AnimatorUpdateListener() {
  430.  
  431. @Override
  432. public void onAnimationUpdate(ValueAnimator animation) {
  433. mCurTextXForHighLightLrc = (Float) animation.getAnimatedValue();
  434. log("mCurTextXForHighLightLrc=" + mCurTextXForHighLightLrc);
  435. invalidate();
  436. }
  437. };
  438.  
  439. /**
  440. * 设置歌词的缩放比例
  441. */
  442. @Override
  443. public void setLrcScalingFactor(float scalingFactor) {
  444. mCurScalingFactor = scalingFactor;
  445. mCurSizeForHightLightLrc = DEFAULT_SIZE_FOR_HIGHT_LIGHT_LRC * mCurScalingFactor;
  446. mCurSizeForOtherLrc = DEFAULT_SIZE_FOR_OTHER_LRC * mCurScalingFactor;
  447. mCurPadding = DEFAULT_PADDING * mCurScalingFactor;
  448. mTotleDrawRow = (int) (getHeight() / (mCurSizeForOtherLrc + mCurPadding)) + 3;
  449. log("mRowTotal=" + mTotleDrawRow);
  450. scrollTo(getScrollX(), (int) (mCurRow * (mCurSizeForOtherLrc + mCurPadding)));
  451. invalidate();
  452. mScroller.forceFinished(true);
  453. }
  454.  
  455. /**
  456. * 重置
  457. */
  458. @Override
  459. public void reset() {
  460. if (!mScroller.isFinished()) {
  461. mScroller.forceFinished(true);
  462. }
  463. mLrcRows = null;
  464. scrollTo(getScrollX(), 0);
  465. invalidate();
  466. }
  467.  
  468. /**
  469. * 平滑的移动到某处
  470. *
  471. * @param dstY
  472. */
  473. private void smoothScrollTo(int dstY, int duration) {
  474. int oldScrollY = getScrollY();
  475. int offset = dstY - oldScrollY;
  476. mScroller.startScroll(getScrollX(), oldScrollY, getScrollX(), offset, duration);
  477. invalidate();
  478. }
  479.  
  480. @Override
  481. public void computeScroll() {
  482. if (!mScroller.isFinished()) {
  483. if (mScroller.computeScrollOffset()) {
  484. int oldY = getScrollY();
  485. int y = mScroller.getCurrY();
  486. if (oldY != y && !canDrag) {
  487. scrollTo(getScrollX(), y);
  488. }
  489. mCurFraction = mScroller.timePassed() * 3f / DURATION_FOR_LRC_SCROLL;
  490. mCurFraction = Math.min(mCurFraction, 1F);
  491. invalidate();
  492. }
  493. }
  494. }
  495.  
  496. /**
  497. * 返回当前的歌词缩放比例
  498. *
  499. * @return
  500. */
  501. public float getmCurScalingFactor() {
  502. return mCurScalingFactor;
  503. }
  504.  
  505. private OnSeekToListener onSeekToListener;
  506.  
  507. public void setOnSeekToListener(OnSeekToListener onSeekToListener) {
  508. this.onSeekToListener = onSeekToListener;
  509. }
  510.  
  511. public interface OnSeekToListener {
  512. void onSeekTo(int progress);
  513. }
  514.  
  515. private OnLrcClickListener onLrcClickListener;
  516.  
  517. public void setOnLrcClickListener(OnLrcClickListener onLrcClickListener) {
  518. this.onLrcClickListener = onLrcClickListener;
  519. }
  520.  
  521. public interface OnLrcClickListener {
  522. void onClick();
  523. }
  524.  
  525. public void log(Object o) {
  526. Log.d("LrcView", o + "");
  527. }
  528. }
  1. * ViewGroup里面 scrollToscrollBy方法移动的是子View
  2. * View里面scrollToscrollBy方法移动的是View里面绘制的内容
  3. * 要点:
  4. * 1:歌词的上下平移用什么实现?
  5. * Scroller实现,Scroller只是一个工具而已,
  6. * 真正实现滚动效果的还是ViewscrollTo方法
  7. * 2:歌词的水平滚动怎么实现?
  8. * 通过属性动画ValueAnimator控制高亮歌词绘制的x轴起始坐标
  9.  
  10. 歌词与播放进度联动,只需要调用seekTo方法,原理是拿播放进度和歌词每一行前的时间作比较,只要播放进度超前,那么就滚动到歌词的相应时间上显示,理论上,只要歌词文本的时间是精确的,那么歌词就会随着
    播放进度一直滚动.
  11.  
  12. 触摸滑动和点击事件都是在onTouchEvent中处理的,原理是记录手指按下和抬起这段时间内Y方向的偏移量,把这个偏移量与每行文字的高度作比较,看滚动到歌词的哪个部分,然后再把播放进度调到对应的时间位置,这样
    就实现了歌词进度与播放进度的完全绑定了.
  13.  
  14. 当然,这些都是建立在歌词解析完成,并且获取到的前提之下,因此,歌词解析也是很重要的部分.

三.歌词解析

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

这样的,既然如此,那么只需要按"["和"]"来截取字符串就可以了.来看下歌词解析类吧(歌词解析不少网友都分享过):

  1. public class DefaultLrcParser implements ILrcParser {
  2. private static final DefaultLrcParser istance = new DefaultLrcParser();
  3.  
  4. public static final DefaultLrcParser getIstance() {
  5. return istance;
  6. }
  7.  
  8. private DefaultLrcParser() {
  9. }
  10.  
  11. /***
  12. * 将歌词文件里面的字符串 解析成一个List<LrcRow>
  13. */
  14. @Override
  15. public List<LrcRow> getLrcRows(String str) {
  16.  
  17. if (TextUtils.isEmpty(str)) {
  18. return null;
  19. }
  20. BufferedReader br = new BufferedReader(new StringReader(str));
  21.  
  22. List<LrcRow> lrcRows = new ArrayList<>();
  23. String lrcLine;
  24. try {
  25. while ((lrcLine = br.readLine()) != null) {
  26. List<LrcRow> rows = LrcRow.createRows(lrcLine);
  27. if (rows != null && rows.size() > 0) {
  28. lrcRows.addAll(rows);
  29. }
  30. }
  31. Collections.sort(lrcRows);
  32. int len = lrcRows.size();
  33. for (int i = 0; i < len - 1; i++) {
  34. lrcRows.get(i).setTotalTime(lrcRows.get(i + 1).getTime() - lrcRows.get(i).getTime());
  35. }
  36. lrcRows.get(len - 1).setTotalTime(5000);
  37. } catch (Exception e) {
  38. e.printStackTrace();
  39. return null;
  40. } finally {
  41. if (br != null) {
  42. try {
  43. br.close();
  44. } catch (IOException e) {
  45. e.printStackTrace();
  46. }
  47. }
  48. }
  49.  
  50. return lrcRows;
  51. }

歌词的实体类:

  1. /**
  2. * 每行歌词的实体类,实现了Comparable接口,方便List<LrcRow>的sort排序
  3. *
  4. * @author Ligang 2014/8/19
  5. */
  6. public class LrcRow implements Comparable<LrcRow> {
  7.  
  8. /**
  9. * 开始时间 为00:10:00
  10. ***/
  11. private String timeStr;
  12. /**
  13. * 开始时间 毫米数 00:10:00 为10000
  14. **/
  15. private int time;
  16. /**
  17. * 歌词内容
  18. **/
  19. private String content;
  20. /**
  21. * 该行歌词显示的总时间
  22. **/
  23. private int totalTime;
  24.  
  25. public long getTotalTime() {
  26. return totalTime;
  27. }
  28.  
  29. public void setTotalTime(int totalTime) {
  30. this.totalTime = totalTime;
  31. }
  32.  
  33. public String getTimeStr() {
  34. return timeStr;
  35. }
  36.  
  37. public void setTimeStr(String timeStr) {
  38. this.timeStr = timeStr;
  39. }
  40.  
  41. public int getTime() {
  42. return time;
  43. }
  44.  
  45. public void setTime(int time) {
  46. this.time = time;
  47. }
  48.  
  49. public String getContent() {
  50. return content;
  51. }
  52.  
  53. public void setContent(String content) {
  54. this.content = content;
  55. }
  56.  
  57. public LrcRow() {
  58. super();
  59. }
  60.  
  61. public LrcRow(String timeStr, int time, String content) {
  62. super();
  63. this.timeStr = timeStr;
  64. this.time = time;
  65. this.content = content;
  66. }
  67.  
  68. /**
  69. * 将歌词文件中的某一行 解析成一个List<LrcRow>
  70. * 因为一行中可能包含了多个LrcRow对象
  71. * 比如 [03:33.02][00:36.37]当鸽子不再象征和平 ,就包含了2个对象
  72. *
  73. * @param lrcLine
  74. * @return
  75. */
  76. public static final List<LrcRow> createRows(String lrcLine) {
  77. if (!lrcLine.startsWith("[")) {
  78. return null;
  79. }
  80. //最后一个"]"
  81. int lastIndexOfRightBracket = lrcLine.lastIndexOf("]");
  82. //歌词内容
  83. String content = lrcLine.substring(lastIndexOfRightBracket + 1, lrcLine.length());
  84. //截取出歌词时间,并将"[" 和"]" 替换为"-" [offset:0]
  85. Log.e("歌词","lrcLine=" + lrcLine);
  86. // -03:33.02--00:36.37-
  87. String times = lrcLine.substring(0, lastIndexOfRightBracket + 1).replace("[", "-").replace("]", "-");
  88. String[] timesArray = times.split("-");
  89. List<LrcRow> lrcRows = new ArrayList<LrcRow>();
  90. for (String tem : timesArray) {
  91. if (TextUtils.isEmpty(tem.trim())) {
  92. continue;
  93. }
  94. //
  95. try {
  96. LrcRow lrcRow = new LrcRow(tem, formatTime(tem), content);
  97. lrcRows.add(lrcRow);
  98. } catch (Exception e) {
  99. Log.w("LrcRow", e.getMessage());
  100. }
  101. }
  102. return lrcRows;
  103. }
  104.  
  105. /****
  106. * 把歌词时间转换为毫秒值 如 将00:10.00 转为10000
  107. *
  108. * @param timeStr
  109. * @return
  110. */
  111. private static int formatTime(String timeStr) {
  112. timeStr = timeStr.replace('.', ':');
  113. String[] times = timeStr.split(":");
  114.  
  115. return Integer.parseInt(times[0]) * 60 * 1000
  116. + Integer.parseInt(times[1]) * 1000
  117. + Integer.parseInt(times[2]);
  118. }
  119.  
  120. @Override
  121. public int compareTo(LrcRow anotherLrcRow) {
  122. return (int) (this.time - anotherLrcRow.time);
  123. }
  124.  
  125. @Override
  126. public String toString() {
  127. return "LrcRow [timeStr=" + timeStr + ", time=" + time + ", content="
  128. + content + "]";
  129. }
  130.  
  131. }

解析成LrcRow后按照时间顺序排列,得到的集合作为一篇歌词的解析结果.

四.歌词显示

歌词显示的逻辑也有要注意的地方,下面画了个简图

  1. private class RequestLrc implements Runnable {
  2.  
  3. private TrackEntity musicInfo;
  4. private boolean stop;
  5.  
  6. RequestLrc(TrackEntity info) {
  7. this.musicInfo = info;
  8. }
  9.  
  10. public void stop() {
  11. stop = true;
  12. }
  13.  
  14. @Override
  15. public void run() {
  16. String url;
  17. if (musicInfo == null || musicInfo.getTrackId() == 0) {
  18. return;
  19. }
  20. String action = MUSIC_DETAIL + musicInfo.getTrackId();
  21. AscHttpHelper helper = new AscHttpHelper(getContext());
  22. url = helper.wrapUrl(action, null);
  23. Log.i(TAG, "请求url" + url);
  24. String resposeString = HttpUtil.getResposeString(url);
  25. Log.i(TAG, "请求结果" + resposeString);
  26. Gson gson = new Gson();
  27. SongDetailBean mDetailSong = gson.fromJson(resposeString, SongDetailBean.class);
  28. if (mDetailSong == null || mDetailSong.data == null || mDetailSong.data.musicInfoList == null) {
  29. return;
  30. }
  31. if (!stop) {
  32. File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + LRC_PATH + musicInfo.getTrackId());
  33. String lrc;
  34. try {
  35. lrc = HttpUtil.getResposeString(mDetailSong.data.musicInfoList.get(0).lyricUrl);
  36. if (!TextUtils.isEmpty(lrc)) {
  37. if (!file.exists()) {
  38. file.createNewFile();
  39. }
  40. writeToFile(file, lrc);
  41. }
  42. } catch (Exception e) {
  43. e.printStackTrace();
  44. mTryGetLrc.post(new Runnable() {
  45. @Override
  46. public void run() {
  47. mTryGetLrc.setVisibility(View.VISIBLE);
  48. mLrcView.reset();
  49. }
  50. });
  51. }
  52. }
  53.  
  54. }
  55. }
  56.  
  57. private synchronized void writeToFile(File file, String lrc) {
  58. FileOutputStream outputStream = null;
  59. try {
  60. outputStream = new FileOutputStream(file);
  61. outputStream.write(lrc.getBytes());
  62. } catch (Exception e) {
  63. e.printStackTrace();
  64. mTryGetLrc.setVisibility(View.VISIBLE);
  65. mLrcView.reset();
  66. } finally {
  67. if (outputStream != null) {
  68. try {
  69. outputStream.flush();
  70. outputStream.close();
  71. final List<LrcRow> list = getLrcRows();
  72. mTryGetLrc.post(new Runnable() {
  73. @Override
  74. public void run() {
  75. if (list != null && list.size() > 0) {
  76. mTryGetLrc.setVisibility(View.INVISIBLE);
  77. mLrcView.setLrcRows(list);
  78. }
  79. }
  80. });
  81. } catch (IOException e) {
  82. e.printStackTrace();
  83. mTryGetLrc.setVisibility(View.VISIBLE);
  84. mLrcView.reset();
  85. }
  86. }
  87. }
  88. }

五.体会与总结

千里之行,始于足下;任何看起来很棒的效果都是一行一行基础代码在发挥作用,他们相互关联,却又相互独立.

这两天来看,上面的歌词显示逻辑还是存在一些问题的,比如切换到下一首的时候,偶现显示了上一首歌曲的歌词.好的,我去修bug了.

Android 歌词显示的更多相关文章

  1. Android ImageView显示本地图片

    Android ImageView 显示本地图片 布局文件 <?xml version="1.0" encoding="utf-8"?> <R ...

  2. unity,将camera设为don't clear在android上会显示不正常

    将camera设置为don't clear,在pc和ios上显示没问题,但在android上显示不正常,改为only depth可以.

  3. Android中显示网页的多种方式

    在android中显示页面主要有两种方式,一种是在Activity里面直接显示网页,另一种是调用浏览器显示网页.方式不同,使用的方法也不同,下面我们分别讲解. 一.在Activity里面直接显示网页 ...

  4. app开发历程——android手机显示服务器端图片思路

    以前自己都不知道怎么去显示服务器端的图片,还好在apkbus论坛上找到一个特别简单的例子.虽然一天天忙忙碌碌,但是自己内心其实有一种想逃的心里,说不定哪天就会冒出来. 1.首先服务器端图片 这里的Im ...

  5. Android 图片显示

    一.Android手机显示图片 若R.G.B每种颜色使用一个字节(8bit)表示,每幅图像可以有1670万种颜色:若R.G.B每种颜色使用两个字节(16bit)表示,每幅图像可以有10的12次方种颜色 ...

  6. html5图片上传时IOS和Android均显示摄像头拍照和图片选择

    最近在做信开发时,发现<input type="file" />在IOS中可以拍照或从照片图库选择,而Android系统则显示资源管理器,无拍照选项,网上查找资料,改为 ...

  7. Android中显示gif动态图片

    在android中显示一个静态图片比如png jpg等等都很方便,但是如果要显示一个gif 动态图片就需要进行一些处理. 本文是采用自定义view 然后进行重新onDraw方法来实现 首先自定义Vie ...

  8. Android系统显示原理

    Android的显示过程可以概括为:Android应用程序把经过测量.布局.绘制后的surface缓存数据,通过SurfaceFlinger把数据渲染到屏幕上,通过Android的刷新机制来刷新数据. ...

  9. android textview 显示一行,且超出自动截断,显示"..."

    android textview 显示一行,且超出自动截断,显示"..." <TextView android:layout_width="wrap_content ...

随机推荐

  1. 洛谷 P2947 [USACO09MAR]向右看齐Look Up【单调栈】

    题目描述 Farmer John's N (1 <= N <= 100,000) cows, conveniently numbered 1..N, are once again stan ...

  2. Linux命令之ftp

    ftp [-pinegvd] [host] pftp [-inegvd] [host] 用户通过ftp这个程序来使用Internet上的标准文件传输协议(FTP).本程序允许用户向远端网站发送文件,或 ...

  3. 04、Unity 5--全局光照技术

    本文整理自Unity全球官方网站,原文:UNITY 5 - LIGHTING AND RENDERING 简介全局光照,简称GI,是一个用来模拟光的互动和反弹等复杂行为的算法,要精确的仿真全局光照非常 ...

  4. 【BZOJ 4567】【SCOI 2016】背单词

    http://www.lydsy.com/JudgeOnline/problem.php?id=4567 贪心. 任何不用第一种情况的方案吃的泡椒数都小于\(n^2\),所以最小泡椒数的方案一定不包含 ...

  5. BZOJ 4756 [Usaco2017 Jan]Promotion Counting(线段树合并)

    [题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=4756 [题目大意] 给出一棵树,对于每个节点,求其子树中比父节点大的点个数 [题解] ...

  6. 【枚举约数】Gym - 101412A - Ginkgo Numbers

    给你一堆定义,问你在那个定义下,<p,q>是不是素数.其实那堆定义都不用管,只要看最下面给你的提示即可. 根据,只要把m^2+n^2当一个整体,去枚举(p^2+q^2)的约数即可,然后再枚 ...

  7. git远程仓库创建及权限管理(二)多个项目

    本文介绍ubutu下使用gitolite实现多项目的权限管理1.安装git sudo apt-get install git 2.设置Git的user name和email: git config - ...

  8. Educational Codeforces Round 9 A. Grandma Laura and Apples 水题

    A. Grandma Laura and Apples 题目连接: http://www.codeforces.com/contest/632/problem/A Description Grandm ...

  9. Web安全测试指南--信息泄露

    5.4.1.源代码和注释: 编号 Web_InfoLeak_01 用例名称 源代码和注释检查测试 用例描述 在浏览器中检查目标系统返回的页面是否存在敏感信息. 严重级别 中 前置条件 1.  目标we ...

  10. VUE2.0学习总结

    摘要: 年后公司项目开始上vue2.0,自己对学习进行了总结,希望对大家有帮助! VUE2.0学习 vue介绍 vue是什么? https://vuefe.cn/guide vue也是一个数据驱动框架 ...