马上毕业了,前段时间一直忙自己的毕业设计和毕业论文,做的是一个android音乐播放器,今天特意抽出里面的一块功能来凑这篇博客--歌词的显示。
看看QQ音乐,歌词显示略屌,可惜我们的LRC文件并不能做到词的同步,只能做到行的同步,所以,退而求之,今天的歌词空间只是同步行,那他有什么功能呢? 歌词同步就不说了,切换滑动效果是我后加上的,因为我看着一行行的切换太过生硬。
下面开始进入主题。

1、首先我们来看看如何使用,控件的使用很简单,可以在xml中配置使用:

<org.loader.liteplayer.ui.LrcViewxmlns:lrc="http://schemas.android.com/apk/res/org.loader.liteplayer"android:id="@+id/play_first_lrc_2"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="5dp"android:layout_marginBottom="5dp"lrc:textSize="18sp"lrc:normalTextColor="@android:color/white"lrc:currentTextColor="@color/main"lrc:dividerHeight="20dp"lrc:rows="9" />

这里我们来看看几个以lrc为命名空间的配置项。
textSize不用多说,肯定是文本的大小了;normalTextColor是普通文本的颜色,因为歌词分为普通的行和当前高亮行,那currentTextColor肯定是高亮行的颜色了;dividerHeight是行间距;rows是显示多少行歌词,在该配置文件中是显示9行的歌词。配置好了,我们需要在activity或者fragment中来使用它。

...
mLrcViewOnSecondPage = (LrcView) lrcView.findViewById(R.id.play_first_lrc_2);
...
mLrcViewOnSecondPage.setLrcPath(lrcPath);
...@Override
public void onPublish(int progress) {if(mLrcViewOnSecondPage.hasLrc()) mLrcViewOnSecondPage.changeCurrent(progress);
}

第一行代码去获取该控件,接着调用setLrcPath将歌词文件加载到内存中,在onPushlish方法中不断调用changeCurrent来更新歌词,那changeCurrent的参数哪来的呢?这个是音乐播放回调的进度,到这里,可能会有大神出疑问了, 这样做是不是会不断的更新歌词控件?就算当前没有切换歌词也回去更新? 这里先给出回答:当然不是了,我们在changeCurrent方法中做了判断,所以这里尽管调用,放心调用!

那接下来,我们开始进入今天的主题:LrcView。

在进入代码之前,先来看看我的设计思路吧:
当我们传进一个lrc文件的path,首先按照行去read文件,并且利用正则解析出时间和歌词分别存放。设置完歌词后,我们通过不断调用changeCurrent()方法来切换歌词,那么changeCurrent又负责了什么工作呢? 在changeCurrent中首先判断下一行开始的时间是不是大于当前传进来的时间,如果是,直接返回,否则,遍历所有的时间,找到大于当前时间的上一行的key, 再次通过key找到歌词,咔咔咔, 显示出来就ok了。

look code:

public class LrcView extends View {private static final int SCROLL_TIME = 500;private static final String DEFAULT_TEXT = "暂无歌词";private List<String> mLrcs = new ArrayList<String>(); // 存放歌词private List<Long> mTimes = new ArrayList<Long>(); // 存放时间private long mNextTime = 0l; // 保存下一句开始的时间private int mViewWidth; // view的宽度private int mLrcHeight; // lrc界面的高度private int mRows;      // 多少行private int mCurrentLine = 0; // 当前行private int mOffsetY;   // y上的偏移private int mMaxScroll; // 最大滑动距离=一行歌词高度+歌词间距private float mTextSize; // 字体private float mDividerHeight; // 行间距private Rect mTextBounds;private Paint mNormalPaint; // 常规的字体private Paint mCurrentPaint; // 当前歌词的大小private Bitmap mBackground;private Scroller mScroller;public LrcView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public LrcView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);mScroller = new Scroller(context, new LinearInterpolator());inflateAttributes(attrs);}
...
}

这么多变量!到底是干嘛用的!只是为了装B吗? NO NO NO, 我们定义它,肯定是需要啦,一个个的来解释一下吧吧。
常量SCROLL_TIME定义了当歌词切换时滑动的时间,这里是500ms。
常量DEFAULT_TEXT定义的是当没有歌词的时候显示的默认文本。
两个ArrayList,mLrcs保存的是一行行的歌词,mTimes保存的是歌词对应的时间。
mNextTime表示的是下一行开始的时间。
其他的一些变量,可以看看代码里的注释,这里就不一一贴出来了。

再来看看构造方法,除了初始化Scroller外,我们调用了inflateAttributes(),那我们跟进inflateAttributes():

// 初始化操作private void inflateAttributes(AttributeSet attrs) {// <begin>// 解析自定义属性TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.Lrc);mTextSize = ta.getDimension(R.styleable.Lrc_textSize, 50.0f);mRows = ta.getInteger(R.styleable.Lrc_rows, 5);mDividerHeight = ta.getDimension(R.styleable.Lrc_dividerHeight, 0.0f);int normalTextColor = ta.getColor(R.styleable.Lrc_normalTextColor, 0xffffffff);int currentTextColor = ta.getColor(R.styleable.Lrc_currentTextColor, 0xff00ffde);ta.recycle();// </end>// 计算lrc面板的高度mLrcHeight = (int) (mTextSize + mDividerHeight) * mRows + 5;mNormalPaint = new Paint();mCurrentPaint = new Paint();// 初始化paintmNormalPaint.setTextSize(mTextSize);mNormalPaint.setColor(normalTextColor);mNormalPaint.setAntiAlias(true);mCurrentPaint.setTextSize(mTextSize);mCurrentPaint.setColor(currentTextColor);mCurrentPaint.setAntiAlias(true);mTextBounds = new Rect();mCurrentPaint.getTextBounds(DEFAULT_TEXT, 0, DEFAULT_TEXT.length(), mTextBounds);mMaxScroll = (int) (mTextBounds.height() + mDividerHeight);}

5~12行,解析出属性值,没有什么好说的,无非就是获取用户配置的颜色啦,字体大小啦,多少行啦。
16行,通过获取到的属性,计算出Lrc能显示下需要多少高度。
然后接下来的一系列动作就是初始化两个Paint,并获取Scroller最大滚动的距离,为什么要计算这个呢? 因为我们需要知道歌词每次要滚动多大距离。(废话!)
初始化完了,就是测量了,我们的测量也是很简单的。

@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, heightMeasureSpec);// 重新设置view的高度int measuredHeightSpec = MeasureSpec.makeMeasureSpec(mLrcHeight, MeasureSpec.AT_MOST);super.onMeasure(widthMeasureSpec, measuredHeightSpec);}@Overrideprotected void onSizeChanged(int w, int h, int oldw, int oldh) {super.onSizeChanged(w, h, oldw, oldh);// 获取view宽度mViewWidth = getMeasuredWidth();}

测量,我们只是重新定义了高度,然后在onSizeChanged中获取了该view的宽度。

按照,进度呢,我们接下来应该看draw了,但是现在我们先不去看onDraw,而是去看看setLrcPath这个方法。

// 外部提供方法// 设置lrc的路径public void setLrcPath(String path) {reset();File file = new File(path);if (!file.exists()) {postInvalidate();return;}BufferedReader reader = null;try {reader = new BufferedReader(new InputStreamReader(new FileInputStream(file)));String line = "";String[] arr;while (null != (line = reader.readLine())) {arr = parseLine(line);if (arr == null) continue;// 如果解析出来只有一个if (arr.length == 1) {String last = mLrcs.remove(mLrcs.size() - 1);mLrcs.add(last + arr[0]);continue;}mTimes.add(Long.parseLong(arr[0]));mLrcs.add(arr[1]);}} catch (Exception e) {e.printStackTrace();} finally {if(reader != null) {try {reader.close();      } catch (IOException e) {e.printStackTrace();}}}}

虽然长了点,但是都是基本的java io,按行去读取文件,然后正则匹配。
需要注意的是22~26行,这里需要说明一下,我们只是匹配了形如:

[05:20.59] 我从天上来

这样的歌词。继续看看parseLine()方法吧。用正则去匹配歌词。

// 解析每行private String[] parseLine(String line) {Matcher matcher = Pattern.compile("\\[\\d.+\\].+").matcher(line);// 如果形如:[xxx]后面啥也没有的,则return空if (!matcher.matches()) {System.out.println("throws " + line);return null;}line = line.replaceAll("\\[", "");String[] result = line.split("\\]");result[0] = String.valueOf(parseTime(result[0]));return result;}

只是简单的正则,没看懂的可以脑补正则了,这里我们只匹配[开头是数字的],如果不是数字,例如:[title:可惜没如果],这样的我们直接抛弃掉。在这个方法中,我们保存了每一行歌词,但是时间还需要调用parseTime()方法来处理一下。继续跟进parseTime()。

// 解析时间private Long parseTime(String time) {// 03:02.12String[] min = time.split(":");String[] sec = min[1].split("\\.");long minInt = Long.parseLong(min[0].replaceAll("\\D+", "").replaceAll("\r", "").replaceAll("\n", "").trim());long secInt = Long.parseLong(sec[0].replaceAll("\\D+", "").replaceAll("\r", "").replaceAll("\n", "").trim());long milInt = Long.parseLong(sec[1].replaceAll("\\D+", "").replaceAll("\r", "").replaceAll("\n", "").trim());return minInt * 60 * 1000 + secInt * 1000 + milInt * 10;}

也是很简单的,通过分割形如“03:02.12”的时间,并且在最后以毫秒的形式返回。

到目前为止,所有的歌词和歌词对应的时间已经保存起来了,接下来,就是要调用changeCurrent()方法来切换歌词了。

// 外部提供方法// 传入当前播放时间public synchronized void changeCurrent(long time) {// 如果当前时间小于下一句开始的时间// 直接returnif (mNextTime > time) {return;}// 每次进来都遍历存放的时间for (int i = 0; i < mTimes.size(); i++) {// 发现这个时间大于传进来的时间// 那么现在就应该显示这个时间前面的对应的那一行// 每次都重新显示,是不是要判断:现在正在显示就不刷新了if (mTimes.get(i) > time) {mNextTime = mTimes.get(i);mScroller.abortAnimation();mScroller.startScroll(i, 0, 0, mMaxScroll, SCROLL_TIME);
//              mNextTime = mTimes.get(i);
//              mCurrentLine = i <= 1 ? 0 : i - 1;postInvalidate();return;}}}

6~8行判断一下现在传进来的时间是不是大于下一行的时间,如果不是,直接返回,避免过度重绘。
接下来,去遍历所有的时间,如果发现该时间大于传进来的时间,那么证明现在要显示上一行了,保存这个时间,并开始一个Scroller。startScroll方法的参数我们是这样设置的。x值在scroll中没有用,所以我们用来保存当前key,并且让他的变化度为0,y的值是从0到mMaxScroll.
接着来看看computeScroll()中怎么处理的。

@Overridepublic void computeScroll() {if(mScroller.computeScrollOffset()) {mOffsetY = mScroller.getCurrY();if(mScroller.isFinished()) {int cur = mScroller.getCurrX();mCurrentLine = cur <= 1 ? 0 : cur - 1;mOffsetY = 0;}postInvalidate();}}

y的变化值我们作为滑动的偏移量,而x呢 当然就是当前行了。

接下来,我们就要开始进入onDraw方法了。

@Overrideprotected void onDraw(Canvas canvas) { // float centerY = (getMeasuredHeight() + mTextBounds.height() - mDividerHeight) / 2;float centerY = (getMeasuredHeight() + mTextBounds.height()) / 2;if (mLrcs.isEmpty() || mTimes.isEmpty()) {canvas.drawText(DEFAULT_TEXT, (mViewWidth - mCurrentPaint.measureText(DEFAULT_TEXT)) / 2,centerY, mCurrentPaint);return;}String currentLrc = mLrcs.get(mCurrentLine);float currentX = (mViewWidth - mCurrentPaint.measureText(currentLrc)) / 2;// 画当前行canvas.drawText(currentLrc, currentX, centerY - mOffsetY, mCurrentPaint);float offsetY = mTextBounds.height() + mDividerHeight;int firstLine = mCurrentLine - mRows / 2;firstLine = firstLine <= 0 ? 0 : firstLine;int lastLine = mCurrentLine + mRows / 2 + 2;lastLine = lastLine >= mLrcs.size() - 1 ? mLrcs.size() - 1 : lastLine;// 画当前行上面的for (int i = mCurrentLine - 1,j=1; i >= firstLine; i--,j++) {String lrc = mLrcs.get(i);float x = (mViewWidth - mNormalPaint.measureText(lrc)) / 2;canvas.drawText(lrc, x, centerY - j * offsetY - mOffsetY, mNormalPaint);}// 画当前行下面的for (int i = mCurrentLine + 1,j=1; i <= lastLine; i++,j++) {String lrc = mLrcs.get(i);float x = (mViewWidth - mNormalPaint.measureText(lrc)) / 2;canvas.drawText(lrc, x, centerY + j * offsetY - mOffsetY, mNormalPaint);}}

首先第4行,我们计算出了该view的中间位置,因为我们的歌词是从中间往两边画的。
5~10行,如果歌词为空,则显示默认的文本”暂无歌词“
12~15行是去绘制当前正在歌唱的行,drawText的第三个参数,我们减去了mOffetY,效果就是一个滑动的过程。

绘制完当前行,我们就需要绘制出当前行上面的和下面的。
17行,是每一行占领的高度。
18、19行,获取的是当前需要显示的第一行(并不是歌词的第一行)。
20、21行,获取需要显示的最后一行。
24~28行,去绘制当前行上面的的需要显示的歌词,需要注意的drawText的第三个参数,我们是通过中间那行的绘制位置去偏移的。
31~35行是去绘制当前行下面的,原理和绘制上面的一样。
这样,一个带有平滑滚动效果的歌词控件就完成了。

最后我们来看看最终的效果:

最后,是关于代码的问题,有人说我的博客没有demo下载,这个以后会注意哈, 这次的代码,等我这个月月底毕业答辩完了,会把音乐播放器一块开源了。

LitePlayer源码下载:https://github.com/qibin0506/LitePlayer

android动手写平滑滚动歌词控件相关推荐

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

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

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

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

  3. 自定义 View 歌词控件

    Yuan-LrcView 项目地址:jsyjst/Yuan-LrcView 简介: 自定义 View 歌词控件 更多:作者   提 Bug 标签: 简约风的歌词控件,如果对具体实现感兴趣的可以看这篇文 ...

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

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

  5. 【Android】App首页上下滚动快报控件 通知控件 类似京东快报控件(一)

    前言 快过年了,对于大伙来说手头上的事情做完没有呢,马上也该让自己轻松一阵子了,哈哈哈.好,说正事,由于公司App这个版本首页的改版,新增了很多新的控件,类似于京东快报这种控件的话我在写之前也去找了一 ...

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

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

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

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

  8. Android 开源歌词控件 LyricViewX

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

  9. 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]愿 ...

最新文章

  1. 交换两个变量的值不使用第三个变量(Java)
  2. MIT警告深度学习正在逼近计算极限,网友:放缓不失为一件好事
  3. SQL点滴20—T-SQL中的排名函数
  4. ubuntu如何修改php文件夹权限,Linux命令chmod:修改文件或文件夹权限
  5. MVP群聊某美国公司的应聘试题(压死九个还是一个)
  6. java 路径获取文件名称_java 根据文件获取文件名及路径的方法
  7. 为Xen虚拟机扩容根文件系统(LVM)
  8. phpstorm xdebug本地调试断点不生效_PHPSTORM与xdebug配置
  9. [Linux] 常用Linux命令
  10. 系统盘格式化数据恢复
  11. caffe2 mdl文件转init_net.pb, predict_net.pb
  12. 使用netsh interface ip set 命令实现快速切换IP地址及DNS地址
  13. Mac如何添加打印机
  14. 01 初识微信小程序
  15. jmeter实现手机号归属地接口测试案例
  16. adjacent_difference 的使用
  17. 04_Mybatis输入\出映射
  18. Python入门(10)——宝可梦数据集探索
  19. 【历史上的今天】12 月 30 日:C++ 之父诞生;Hotmail 创始人出生;Facebook 注册破百万
  20. 神经网络按结构可以分为,神经网络主要包括哪些

热门文章

  1. 查看支付宝所有交易记录方法
  2. 51单片机通过DHT11温度传感器读取温度(2)
  3. 算法leetcode|剑指 Offer 27. 二叉树的镜像|226. 翻转二叉树(rust很强)
  4. 十张图,看数据分析如何赋能销售
  5. Hi3531D加载TVP7002驱动模块——新手全过程记录
  6. 触摸屏键盘插件Virtual Keyboard 该怎么用 Virtual Keyboard 入门指南
  7. adb工具(通用的调试工具、debug工具)操作命令详解
  8. 获取焦点(onFocus)和失去焦点(onBlur) (js)
  9. 子平格局——从旺格/从强格
  10. win7 64位利用eclipse搭建android开发环境教程