转载一个博主的。起因是测试同学发现满屏歌词,歌词过长被裁切遮挡了,主要原因就是使用的canvas.drawText() 的效果会是屏幕覆盖掉多余的 text 文字。所以这边博主给了详细的解释,和建议,做得很好,重新文章整理一下思路。

歌词的需求我想大家都很清楚,简单的话,直接打开一个音乐播放器查看一下。我们打开后分析一下歌词的功能:歌词完整的显示出来、当前歌词变色、可以根据时间而进行定位、可以手动滑动、滑动后显示一个指示器、点击指示器播放进度跳转、滑动时指示器变色等等。OK,我们自己写歌词控件,这些功能也是必不可少的,接下来就逐步分析下实现的过程。实现歌词解析歌词显示滑动处理指示器基本实现就是这几个过程,接下来一步步的分析。歌词解析首先,我们在网上下载一个歌词,即以 lrc 为后缀的文件。比如海阔天空这首歌的歌词,我们用记事本或者其他工具打开后就可以看到具体的歌词内容,如下:
[ti: 海阔天空]
[ar:黄家驹]
[al:乐与怒]
[by:mp3.50004.com]
[00:00.00]Beyond:海阔天空
[01:40.00][00:16.00]今天我寒夜里看雪飘过
[01:48.00][00:24.00]怀著冷却了的心窝飘远方
[01:53.00][00:29.00]风雨里追赶
...

[00:42.00]多少次迎著冷眼与嘲笑
[00:49.00]从没有放弃过心中的理想
[00:54.00]一刹那恍惚
...

可以看到,歌词主要包含歌名、歌手、专辑、作者等头元素,以及歌词的主体内容,我们需要处理的就是主体的歌词内容。首先,歌词是一行一行的文本,其次,每行的文本都包含时间标签和具体的一行歌词,我们首先将歌词解析为一行行的数据。
InputStreamReader isr = null;
BufferedReader br = null;
try {
isr = new InputStreamReader(inputStream, CHARSET);
br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
//此处的 line 即为一行行的文本
//parseLrc 方法为解析单行
List lrcList = parseLrc(line);
if (lrcList != null && lrcList.size() != 0) {
lrcs.addAll(lrcList);
}
}
sortLrcs(lrcs);
return lrcs;
}catch ...

解析为一行行的文字后,就需要具体的处理单行的文字了,我们可以看到,大部分歌词包含两种格式,即单个时间标签和多个时间标签,这里可以采用正则表达式来匹配文字,正则表达式为 (([\d{2}:\d{2}.\d{2}])+)(.*)
[01:53.00][00:29.00]风雨里追赶 //多个时间标签

[00:42.00]多少次迎著冷眼与嘲笑 //单个时间标签

接下来根据正则表达式来解析单行歌词
private static List parseLrc(String lrcLine) {
if (lrcLine.trim().isEmpty()) {
return null;
}
List lrcs = new ArrayList<>();
Matcher matcher = Pattern.compile(LINE_REGEX).matcher(lrcLine);
if (!matcher.matches()) {
return null;
}

String time = matcher.group(1);
String content = matcher.group(3);
Matcher timeMatcher = Pattern.compile(TIME_REGEX).matcher(time);

while (timeMatcher.find()) {
String min = timeMatcher.group(1);
String sec = timeMatcher.group(2);
String mil = timeMatcher.group(3);
Lrc lrc = new Lrc();
if (content != null && content.length() != 0) {
lrc.setTime(Long.parseLong(min) * 60 * 1000 + Long.parseLong(sec) * 1000
+ Long.parseLong(mil) * 10);
lrc.setText(content);
lrcs.add(lrc);
}
}
return lrcs;
}

这样,第一步就完成了,歌词解析完成后得到歌词的数据集合,每个元素都包括时间和内容。歌词显示歌词显示的思路就是将歌词一行行的画出来,我们首先假设屏幕足够大,那么只需要定位第一行歌词的位置,画出来第一行歌词,然后逐行下移一个固定的距离,再画出下一行歌词,依次类推,整个歌词内容就会全部画在画布上了。依照这个思路,我们可以先画出来文字。
//此处为伪代码

float y = getLrcHeight() / 2;
float x = getLrcWidth() / 2 + getPaddingLeft();
for (int i = 0; i < getLrcCount(); i++) {
if (i > 0) {
y += textHeight + mLrcLineSpaceHeight;
}
...
canvas.drawText(text, x, y, mPaint);
}

画出来文字的思路就是这样,首先从屏幕的中间开始,然后纵坐标每次增加文字的高度与距离之和,依次画出来每行文字。这样,假如屏幕足够大的话,那么所有的歌词就会从屏幕中间开始,依次向下一行行的显示出来。但是,我们的屏幕不可能是无限大的。首先,假如一行歌词很长的话,canvas.drawText() 的效果会是屏幕覆盖掉多余的 text 文字,所以当一行文字超过我们设置的 View 最大宽度时,最理想的方法就是多余的部分换行,就像 TextView 一样。所幸的是,Android 中给我们提供了方法,那就是 StaticLayout ,StaticLayout 用法很简单,我们使用它来替代 canvas.drawText(),下面是基本用法。
private void drawLrc(Canvas canvas, float x, float y, int i) {
mTextPaint.setTextSize(mLrcTextSize);
String text = mLrcData.get(i).getText();
StaticLayout staticLayout = new StaticLayout(text, mTextPaint, getLrcWidth(),
Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);
canvas.save();
canvas.translate(x, y - staticLayout.getHeight() / 2 - mOffset);
staticLayout.draw(canvas);
canvas.restore();
}

这样我们就能获取想要的效果了,文字一行行的排列,文字比较长的话,会自动换行到下一行。但是,这样仅仅是实现效果,在 onDraw() 方法中,我们应该尽量的避免新建对象,以免造成界面的卡顿,而 StaticLayout 需要实例化对象,所以这边需要我们手动优化一下。

因为使用 StaticLayout 后,一行文字的高度不再固定,所以 y 坐标不再累加固定的文字高度,而是上一行和下一行文字之和的一半+文字间距。代码如下:
for (int i = 0; i < getLrcCount(); i++) {
if (i > 0) {
y += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight;
}
drawLrc(canvas, x, y, i);
}

为了避免过多的实例化,在使用 StaticLayout 时,这里采用 map 进行缓存,创建过对象后缓存起来,后边就不需要再继续创建。
private void drawLrc(Canvas canvas, float x, float y, int i) {
String text = mLrcData.get(i).getText();
StaticLayout staticLayout = mLrcMap.get(text);
if (staticLayout == null) {
mTextPaint.setTextSize(mLrcTextSize);
staticLayout = new StaticLayout(text, mTextPaint, getLrcWidth(),
Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false);
mLrcMap.put(text, staticLayout);
}
canvas.save();
canvas.translate(x, y - staticLayout.getHeight() / 2 - mOffset);
staticLayout.draw(canvas);
canvas.restore();
}

到这里,我们已经解决了水平方向的显示,但是垂直方向呢,垂直方向则利用滑动来解决,这也是歌词的基本需求之一。滑动处理歌词的滑动是做歌词控件的必然要求,包括根据音乐播放的进度进行自动的滑动,以及用户主动拖动的滑动,我们来逐个分析。1、根据播放进度滚动音乐的播放时间进度可以根据 MediaPlayer 来获取,在一首音乐播放的过程中,播放的进度是不断更新的,所以就需要我们根据这个不断更新的时间,来决定歌词滚动的位置。我们需要比较不断更新的时间和每行歌词的时间,最接近或者相等时,就可以视作音乐播放的进度对应当前这一行歌词,所以需要获取播放时间对应的歌词行数。 private int getUpdateTimeLinePosition(long time) {
int linePos = 0;
for (int i = 0; i < getLrcCount(); i++) {
Lrc lrc = mLrcData.get(i);
if (time >= lrc.getTime()) {
if (i == getLrcCount() - 1) {假如时间大于最后一行歌词的时间,则行数为最后一行
linePos = getLrcCount() - 1;
} else if (time < mLrcData.get(i + 1).getTime()) {//否则若同时小于下一行,则行数为 i
linePos = i;
break;
}
}
}
return linePos;
}
获取行数之后,行数变化时,就可以利用动画,来让歌词进行滚动。 private void scrollToPosition(int linePosition) {
float scrollY = getItemOffsetY(linePosition);//将要滚动的一行的偏移量
final ValueAnimator animator = ValueAnimator.ofFloat(mOffset, scrollY);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override
public void onAnimationUpdate(ValueAnimator animation) {
mOffset = (float) animation.getAnimatedValue();
invalidateView();
}
});
animator.setDuration(300);
animator.start();
}

此处最重要的属性就是 mOffset ,mOffset 是为了决定歌词偏移量而定义的一个属性, mOffset 的取值是在原有值和目标行的偏移量之间,由动画控制其变化。假如向下滑动,初始为0,则滚动到第二行歌词,mOffset 就是从 0 到 getItemOffsetY(1) 的过程。 getItemOffsetY(i) 就是第 i 行的偏移量。 private float getItemOffsetY(int linePosition) {
float tempY = 0;
for (int i = 1; i <= linePosition; i++) {
tempY += (getTextHeight(i - 1) + getTextHeight(i)) / 2 + mLrcLineSpaceHeight;
}
return tempY;
}
然后,再根据播放进度,进行不断的更新。 public void updateTime(long time) {
if (isLrcEmpty()) {
return;
}
int linePosition = getUpdateTimeLinePosition(time);
if (mCurrentLine != linePosition) {
mCurrentLine = linePosition;
ViewCompat.postOnAnimation(LrcView.this, mScrollRunnable);
}
}

private Runnable mScrollRunnable = new Runnable() {
@Override
public void run() {
scrollToPosition(mCurrentLine);
}
};
到此为止,我们已经完成了歌词的自动滚动功能。2、滑动事件处理仅仅有自动滚动是无法满足歌词的需求的,所以我们还需要控制歌词的滑动事件,让用户可以手动滑动歌词到某个位置。既然是手势的事件,那么就需要我们重写 onTouch 方法,处理不同的手势。@Override
public boolean onTouchEvent(MotionEvent event) {
if (isLrcEmpty()) { //歌词为空,则默认事件
return super.onTouchEvent(event);
}
//速度跟踪
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
removeCallbacks(mScrollRunnable);
if (!mOverScroller.isFinished()) {
mOverScroller.abortAnimation();
}
mLastMotionX = event.getX();
mLastMotionY = event.getY();
isUserScroll = true;
isDragging = false;
break;

case MotionEvent.ACTION_MOVE:
float moveY = event.getY() - mLastMotionY;
if (Math.abs(moveY) > mScaledTouchSlop) {
isDragging = true;
isShowTimeIndicator = isEnableShowIndicator;
}
if (isDragging) {
float maxHeight = getItemOffsetY(getLrcCount() - 1);
if (mOffset < 0 || mOffset > maxHeight) {
moveY /= 3.5f;
}
mOffset -= moveY;
mLastMotionY = event.getY();
invalidateView();
}
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
handleActionUp(event);
break;
}
return true;
}

简单解释下上述代码,先忽略掉 VelocityTracker 和 OverScroller。在 ACTION_DOWN 时,记录下 x 和 y 的坐标;然后在 ACTION_MOVE 时,若拖动的距离大于触发滑动的最小值,则改变 mOffset 的值,然后刷新 View。当 mOffset < 0 或者 mOffset > maxHeight 即歌词已经滚动到顶部或者底部时,为了回弹的阻尼效果,将 moveY 的值大幅减小。

接下来介绍下手势抬起的事件,VelocityTracker 和 OverScroller 就是用于此处,在手势滑动抬起时,我们希望有一个 fling 的效果,Android 中的 OverScroller 可以简单的实现这种效果。
private void handleActionUp(MotionEvent event) {

//越界的处理
if (overScrolled() && mOffset < 0) {
scrollToPosition(0);
ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);
return;
}

if (overScrolled() && mOffset > getItemOffsetY(getLrcCount() - 1)) {
scrollToPosition(getLrcCount() - 1);
ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);
return;
}

mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
float YVelocity = mVelocityTracker.getYVelocity();
float absYVelocity = Math.abs(YVelocity);
if (absYVelocity > mMinimumFlingVelocity) {
mOverScroller.fling(0, (int) mOffset, 0, (int) (-YVelocity), 0,
0, 0, (int) getItemOffsetY(getLrcCount() - 1),
0, (int) getTextHeight(0));
invalidateView();
}
releaseVelocityTracker();
if (isAutoAdjustPosition) {
ViewCompat.postOnAnimationDelayed(LrcView.this, mScrollRunnable, mTouchDelay);
}
}

当手势抬起时,计算下当前的手势速度,然后利用 mOverScroller.fling() 方法,在 computeScroll() 中改变 mOffset 的值即可。 @Override
public void computeScroll() {
super.computeScroll();
if (mOverScroller.computeScrollOffset()) {
mOffset = mOverScroller.getCurrY();
invalidateView();
}
}
这样,主动的手势功能也已经实现了。指示器用户手动滑动歌词的目的,很大一部分是为了滑动后能根据歌词来控制播放的进度,所以指示器也是一个不可或缺的功能。当用户滑动歌词时,显示指示器,歌词经过指示器的位置时变色,用户点击指示器按钮后,歌词跳转到这个位置,播放进度也到了这里。

首先要做的就是显示指示器以及歌词变色,这里就需要我们获取歌词在指示器的位置时,歌词的行数,因为指示器画在歌词的中间位置,所以某一行歌词的偏移量和 mOffset 的差值最小时,就可以看作这一行歌词经过了指示器。public int getIndicatePosition() {
int pos = 0;
float min = Float.MAX_VALUE;
//itemOffset 和 mOffset 最小时,当前的位置
for (int i = 0; i < mLrcData.size(); i++) {
float offsetY = getItemOffsetY(i);
float abs = Math.abs(offsetY - mOffset);
if (abs < min) {
min = abs;
pos = i;
}
}
return pos;
}
然后在 onDraw() 中,画出来具体的特性。 if (isShowTimeIndicator) {
mPlayDrawable.draw(canvas); // 画出指示器的播放按钮
long time = mLrcData.get(indicatePosition).getTime();
float timeWidth = mIndicatorPaint.measureText(LrcHelper.formatTime(time)); //获取指示时间的文字长度
mIndicatorPaint.setColor(mIndicatorLineColor);
// 画出指示线
canvas.drawLine(mPlayRect.right + mIconLineGap, getHeight() / 2,
getWidth() - timeWidth * 1.3f, getHeight() / 2, mIndicatorPaint);
int baseX = (int) (getWidth() - timeWidth * 1.1f);
float baseline = getHeight() / 2 - (mIndicatorPaint.descent() - mIndicatorPaint.ascent()) / 2 - mIndicatorPaint.ascent();
mIndicatorPaint.setColor(mIndicatorTextColor);
//画出指示时间文字
canvas.drawText(LrcHelper.formatTime(time), baseX, baseline, mIndicatorPaint);
}
最后,处理用户点击事件,并且将当前行的歌词及时间进行回调,来控制播放进度。if (isShowTimeIndicator && mPlayRect != null && onClickPlayButton(event)) {
isShowTimeIndicator = false;
invalidateView();
if (mOnPlayIndicatorLineListener != null) {
mOnPlayIndicatorLineListener.onPlay(mLrcData.get(getIndicatePosition()).getTime(),
mLrcData.get(getIndicatePosition()).getText());
}
}
//点击在按钮范围才响应
private boolean onClickPlayButton(MotionEvent event) {
float left = mPlayRect.left;
float right = mPlayRect.right;
float top = mPlayRect.top;
float bottom = mPlayRect.bottom;
float x = event.getX();
float y = event.getY();
return mLastMotionX > left && mLastMotionX < right && mLastMotionY > top
&& mLastMotionY < bottom && x > left && x < right && y > top && y < bottom;
}
这样,指示器的功能也就完成了。

android歌词控件相关推荐

  1. android歌词效果,自定义View:Android歌词控件

    TicktockMusic 音乐播放器项目相关文章汇总: 简介 之前做 TicktockMusic 音乐播放器,一个必要的需求肯定是歌词,在 github 上找了几个,发现或多或少都有点不满足需求,所 ...

  2. android 歌词控件,歌词显示控件的实现上——歌词解析

    最近打算仿网易云音乐的音乐播放器,除了网络框架.接口数据等这些外,最核心的就是音乐的播放和歌词的显示. 考虑到歌词显示控件涉及到歌词解析,自定义控件的实现等等诸多方面,可能文章的篇幅上会比较冗长,同时 ...

  3. android动手写平滑滚动歌词控件

    马上毕业了,前段时间一直忙自己的毕业设计和毕业论文,做的是一个android音乐播放器,今天特意抽出里面的一块功能来凑这篇博客--歌词的显示. 看看QQ音乐,歌词显示略屌,可惜我们的LRC文件并不能做 ...

  4. Android自定义View-简约风歌词控件

    前言 最近重构了之前的音乐播放器,添加了许多功能,比如歌词,下载功能等.这篇文章就让我们聊聊歌词控件的实现(歌词控件也已经开源,地址也在文章底部),先上效果图,如果感觉海星,就继续瞧下去! 看到这里, ...

  5. android MusicPlayer 音乐播放器 Lrc歌词控件的实现

    MusicPlayer Lrc歌词控件的实现 最近在做一个音乐播放器,关于其中歌词控件,上网查过了一些资料,然后进行修改,也算完整的实现了其功能.先看看实现后的效果. 实现的原理实际上是自定义一个Vi ...

  6. Android自定义View实战:简约风歌词控件

    作者:jsyjst 前言 最近重构了之前的音乐播放器,添加了许多功能,比如歌词,下载功能等.这篇文章就让我们聊聊歌词控件的实现,先上效果图,如果感觉海星,就继续瞧下去! 看到这里,估计你对这个控件还有 ...

  7. Android 开源歌词控件 LyricViewX

    Android 开源歌词控件 LyricViewX 开源地址 Github https://github.com/Moriafly/LyricViewX LyricViewX 是一个美观的安卓歌词控件 ...

  8. Android自定义View实战:简约风歌词控件,Android开发者值得深入思考的几个问题

    57[02:41.62]从不知 她的痛苦 58[02:52.02] 59[02:54.11]喜欢你 那双眼动人 60[03:00.13]笑声更迷人 61[03:02.38] 62[03:03.14]愿 ...

  9. Android漂亮的音乐歌词控件,仿网易云音乐滑动效果

    前言: 项目有个音乐播发器功能,实现音乐在线播放,同时需要带有歌词显示功能.网上也找过,在github找到勉强能用的控件,只是效果还是差强人意,不是特别好.于是趁有空的时间,参考了网上的部分demo, ...

  10. Android开源控件收集整理

    一 .基本控件 TextView HTextView 一款支持TextView文字动画效果的Android组件库.GitHub - hanks-zyh/HTextView: Animation eff ...

最新文章

  1. AI公开课:19.02.20 雷鸣教授《人工智能革命与机遇》课堂笔记以及个人感悟
  2. Gevent简明教程
  3. 牛顿法、梯度下降法、高斯牛顿法、Levenberg-Marquardt算法
  4. 用python画一只可爱的皮卡丘_用python画一只可爱的皮卡丘实例
  5. 我是如何使用wireshark软件的
  6. 关于多线程编程您不知道的 5 件事 有关高性能线程处理的微妙之处
  7. java gzip rest_RestTemplate与Gzip压缩
  8. 魅族16T刚发布就被锤!德国莱茵TÜV:它没通过我的认证
  9. 数据库左连接查询时候的技巧.
  10. 我的blog终于开张啦 :)
  11. php字符串怎么判断是否相等,php判断两个字符串是否相等
  12. php中conf,php 中 php-fpm.conf
  13. 【转】Ubuntu下用G++编译C++程序
  14. 如何使用腾讯云GPU云服务器完成 blender 的动画图片渲染
  15. python实现排序算法lowb三人组之选择排序
  16. 宫廷秘方,给大家分享一下,祝大家身体健康
  17. win10 新版文件资源管理器
  18. 百度智能云 x 民生银行 | 智能+创新,数字化运营再升级
  19. 2022年淘宝女王节预售活动时间介绍
  20. 基于微信小程序游泳馆管理系统(微信小程序毕业设计)

热门文章

  1. 双网卡电脑同时访问内外网设置静态路由表
  2. windows7中文企业版安装英文语言包
  3. webpack 图像压缩_基于 TinyPNG 封装的一个支持nodejs、命令行和 webpack 的图片压缩工具...
  4. 麻瓜python视频教程_麻瓜编程Python Web基础视频教程
  5. Bex5开发平台分辨率问题解决方法
  6. 《统一沟通-微软-实战》-6-部署-2-中介服务器-2-安装中介服务器
  7. cam全称_在ADU高级参数(parameter)中,术语CAM全称是什么?
  8. MyEclipse10破解详解过程
  9. jetson nano使用python读取解析GPS数据(GPRMC,GPGGA)。
  10. Arduino-atmega328p最小系统