文末
那么对于想坚持程序员这行的真的就一点希望都没有吗?
其实不然,在互联网的大浪淘沙之下,留下的永远是最优秀的,我们考虑的不是哪个行业差哪个行业难,就逃避掉这些,无论哪个行业,都会有他的问题,但是无论哪个行业都会有站在最顶端的那群人。我们要做的就是努力提升自己,让自己站在最顶端,学历不够那就去读,知识不够那就去学。人之所以为人,不就是有解决问题的能力吗?挡住自己的由于只有自己。
Android希望=技能+面试
- 技能
- 面试技巧+面试题
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
==============================================================================
1public class LrcUtil {
2
3 /**
4 * 解析歌词,将字符串歌词封装成LrcBean的集合
5 * @param lrcStr 字符串的歌词,歌词有固定的格式,一般为
6 * [ti:喜欢你]
7 * [ar:.]
8 * [al:]
9 * [by:]
10 * [offset:0]
11 * [00:00.10]喜欢你 - G.E.M. 邓紫棋 (Gem Tang)
12 * [00:00.20]词:黄家驹
13 * [00:00.30]曲:黄家驹
14 * [00:00.40]编曲:Lupo Groinig
15 * @return 歌词集合
16 */
17 public static List parseStr2List(String lrcStr){
18 List res = new ArrayList<>();
19 //根据转行字符对字符串进行分割
20 String[] subLrc = lrcStr.split(“\n”);
21 //跳过前四行,从第五行开始,因为前四行的歌词我们并不需要
22 for (int i = 5; i < subLrc.length; i++) {
23 String lineLrc = subLrc[i];
24 //[00:00.10]喜欢你 - G.E.M. 邓紫棋 (Gem Tang)
25 String min = lineLrc.substring(lineLrc.indexOf(“[”)+1,lineLrc.indexOf(“[”)+3);
26 String sec = lineLrc.substring(lineLrc.indexOf(“:”)+1,lineLrc.indexOf(“:”)+3);
27 String mills = lineLrc.substring(lineLrc.indexOf(“.”)+1,lineLrc.indexOf(“.”)+3);
28 //进制转化,转化成毫秒形式的时间
29 long startTime = getTime(min,sec,mills);
30 //歌词
31 String lrcText = lineLrc.substring(lineLrc.indexOf(“]”)+1);
32 //有可能是某个时间段是没有歌词,则跳过下面
33 if(lrcText.equals(“”)) continue;
34 //在第一句歌词中有可能是很长的,我们只截取一部分,即歌曲加演唱者
35 //比如 光年之外 (《太空旅客(Passengers)》电影中国区主题曲) - G.E.M. 邓紫棋 (Gem Tang)
36 if (i == 5) {
37 int lineIndex = lrcText.indexOf(“-”);
38 int first = lrcText.indexOf(“(”);
39 if(first<lineIndex&&first!=-1){
40 lrcText = lrcText.substring(0,first)+lrcText.substring(lineIndex);
41 }
42 LrcBean lrcBean = new LrcBean();
43 lrcBean.setStart(startTime);
44 lrcBean.setLrc(lrcText);
45 res.add(lrcBean);
46 continue;
47 }
48 //添加到歌词集合中
49 LrcBean lrcBean = new LrcBean();
50 lrcBean.setStart(startTime);
51 lrcBean.setLrc(lrcText);
52 res.add(lrcBean);
53 //如果是最后一句歌词,其结束时间是不知道的,我们将人为的设置为开始时间加上100s
54 if(i == subLrc.length-1){
55 res.get(res.size()-1).setEnd(startTime+100000);
56 }else if(res.size()>1){
57 //当集合数目大于1时,这句的歌词的开始时间就是上一句歌词的结束时间
58 res.get(res.size()-2).setEnd(startTime);
59 }
60
61 }
62 return res;
63 }
64
65 /**
66 * 根据时分秒获得总时间
67 * @param min 分钟
68 * @param sec 秒
69 * @param mills 毫秒
70 * @return 总时间
71 */
72 private static long getTime(String min,String sec,String mills){
73 return Long.valueOf(min)601000+Long.valueOf(sec)*1000+Long.valueOf(mills);
74 }
75}
相信上面的代码和注释已经将这个歌词解析解释的挺明白了,需要注意的是上面对i=5,也就是歌词真正开始的第一句做了特殊处理,因为i=5这句有可能是很长的,假设i=5是“光年之外
(《太空旅客(Passengers)》电影中国区主题曲) - G.E.M. 邓紫棋 (Gem
Tang)”这句歌词,如果我们不做特殊处理,在后面绘制的时候,就会发现这句歌词会超过屏幕大小,很影响美观,所以我们只截取歌曲名和演唱者,有些说明直接省略掉了。解析好了歌词,接下来就是重头戏-歌词绘制!
==================================================================
歌词绘制就涉及到了自定义View的知识,所以还未接触自定义View的小伙伴需要先去看看自定View的基础知识。歌词绘制的主要工作主要由下面几部分构成:
-
为歌词控件设置自定义属性,在构造方法中获取并设置自定义属性的默认值
-
初始化两支画笔。分别是歌词普通画笔,歌词高亮画笔。
-
获取当前播放歌词的位置
-
画歌词,根据当前播放歌词的位置来决定用哪支画笔画
-
歌词随歌曲播放同步滑动
-
重新绘制
==================================================================================
在res文件中的values中新建一个attrs.xml文件,然后定义歌词的自定义View属性
1<?xml version="1.0" encoding="utf-8"?>
2
3
4
5
6
7
8
9
这里只自定义了歌词颜色,歌词高亮颜色,歌词大小,歌词行间距的属性,可根据自己需要自行添加。
然后在Java代码中,设置默认值。
1 private int lrcTextColor;//歌词颜色
2 private int highLineTextColor;//当前歌词颜色
3 private int width, height;//屏幕宽高
4 private int lineSpacing;//行间距
5 private int textSize;//字体大小
6
7 public LrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
8 super(context, attrs, defStyleAttr);
9 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LrcView);
10 lrcTextColor = ta.getColor(R.styleable.LrcView_lrcTextColor, Color.GRAY);
11 highLineTextColor = ta.getColor(R.styleable.LrcView_highLineTextColor, Color.BLUE);
12 float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
13 float scale = context.getResources().getDisplayMetrics().density;
14 //默认字体大小为16sp
15 textSize = ta.getDimensionPixelSize(R.styleable.LrcView_textSize, (int) (16 * fontScale));
16 //默认行间距为30dp
17 lineSpacing = ta.getDimensionPixelSize(R.styleable.LrcView_lineSpacing, (int) (30 * scale));
18 //回收
19 ta.recycle();
20 }
=======================================================================
1 private void init() {
2 //初始化歌词画笔
3 dPaint = new Paint();
4 dPaint.setStyle(Paint.Style.FILL);//填满
5 dPaint.setAntiAlias(true);//抗锯齿
6 dPaint.setColor(lrcTextColor);//画笔颜色
7 dPaint.setTextSize(textSize);//歌词大小
8 dPaint.setTextAlign(Paint.Align.CENTER);//文字居中
9
10 //初始化当前歌词画笔
11 hPaint = new Paint();
12 hPaint.setStyle(Paint.Style.FILL);
13 hPaint.setAntiAlias(true);
14 hPaint.setColor(highLineTextColor);
15 hPaint.setTextSize(textSize);
16 hPaint.setTextAlign(Paint.Align.CENTER);
17 }
我们把初始化的方法放到了构造方法中,这样就可以避免在重绘时再次初始化。另外由于我们把init方法只放到了第三个构造方法中,所以在上面两个构造方法需要将super改成this,这样就能保证哪个构造方法都能执行init方法
1 public LrcView(Context context) {
2 this(context, null);
3 }
4
5 public LrcView(Context context, @Nullable AttributeSet attrs) {
6 this(context, attrs, 0);
7 }
8
9 public LrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
10 super(context, attrs, defStyleAttr);
11 TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.LrcView);
12 …
13 //回收
14 ta.recycle();
15 init();
16 }
============================================================================
因为后面的步骤都是在onDraw方法中执行的,所以我们先贴出onDraw方法中的代码
1 @Override
2 protected void onDraw(Canvas canvas) {
3 super.onDraw(canvas);
4
5 getMeasuredWidthAndHeight();//得到测量后的宽高
6 getCurrentPosition();//得到当前歌词的位置
7 drawLrc(canvas);//画歌词
8 scrollLrc();//歌词滑动
9 postInvalidateDelayed(100);//延迟0.1s刷新
10 }
1.获得控件的测量后的宽高
1 private int width, height;//屏幕宽高
2 private void getMeasuredWidthAndHeight(){
3 if (width == 0 || height == 0) {
4 width = getMeasuredWidth();
5 height = getMeasuredHeight();
6 }
7 }
为什么要获得控件的宽高呢?因为在下面我们需要画歌词,画歌词时需要画的位置,这时候就需要用到控件的宽高了。
2. 得到当前歌词的位置
1 private List lrcBeanList;//歌词集合
2 private int currentPosition;//当前歌词的位置
3 private MediaPlayer player;//当前的播放器
4
5
6 private void getCurrentPosition() {
7 int curTime = player.getCurrentPosition();
8 //如果当前的时间大于10分钟,证明歌曲未播放,则当前位置应该为0
9 if (curTime < lrcBeanList.get(0).getStart()||curTime>10601000) {
10 currentPosition = 0;
11 return;
12 } else if (curTime > lrcBeanList.get(lrcBeanList.size() - 1).getStart()) {
13 currentPosition = lrcBeanList.size() - 1;
14 return;
15 }
16 for (int i = 0; i < lrcBeanList.size(); i++) {
17 if (curTime >= lrcBeanList.get(i).getStart() && curTime <= lrcBeanList.get(i).getEnd()) {
18 currentPosition = i;
19 }
20 }
21 }
我们根据当前播放的歌曲时间来遍历歌词集合,从而判断当前播放的歌词的位置。细心的你可能会发现在currentPosition = 0中有个curTime>10_60_1000的判断,这是因为在实际使用中发现当player还未播放时,这时候得到的curTime会很大,所以才有了这个判断(因为正常的歌曲不会超过10分钟)。
在这个方法我们会发现出现了歌词集合和播放器,你可能会感到困惑,这些不是还没赋值吗?困惑就对了,所以我们需要提供外部方法来给外部传给歌词控件歌词集合和播放器。
1 //将歌词集合传给到这个自定义View中
2 public LrcView setLrc(String lrc) {
3 lrcBeanList = LrcUtil.parseStr2List(lrc);
4 return this;
5 }
6
7 //传递mediaPlayer给自定义View中
8 public LrcView setPlayer(MediaPlayer player) {
9 this.player = player;
10 return this;
11 }
外部方法中setLrc的参数必须是前面提到的标准歌词格式的字符串形式,这样我们就能利用上文的解析工具类LrcUtil中的解析方法将字符串解析成歌词集合。
3. 画歌词
1 private void drawLrc(Canvas canvas) {
2 for (int i = 0; i < lrcBeanList.size(); i++) {
3 if (currentPosition == i) {//如果是当前的歌词就用高亮的画笔画
4 canvas.drawText(lrcBeanList.get(i).getLrc(), width / 2, height / 2 + i * lineSpacing, hPaint);
5 } else {
6 canvas.drawText(lrcBeanList.get(i).getLrc(), width / 2, height / 2 + i * lineSpacing, dPaint);
7 }
8 }
9 }
知道了当前歌词的位置就很容易画歌词了。遍历歌词集合,如果是当前歌词,则用高亮的画笔画,其它歌词就用普通画笔画。这里需注意的是两支画笔画的位置公式都是一样的,坐标位置为x=宽的一半,y=高的一半+当前位置*行间距。随着当前位置的变化,就能画出上下句歌词来。所以其实绘制出来后你会发现歌词是从控件的正中央开始绘制的,这是为了方便与下面歌词同步滑动功能配合。
4. 歌词同步滑动
1 //歌词滑动
2 private void scrollLrc() {
3 //下一句歌词的开始时间
4 long startTime = lrcBeanList.get(currentPosition).getStart();
5 long currentTime = player.getCurrentPosition();
6
7 //判断是否换行,在0.5内完成滑动,即实现弹性滑动
8 float y = (currentTime - startTime) > 500 ? currentPosition * lineSpacing : lastPosition * lineSpacing + (currentPosition - lastPosition) * lineSpacing * ((currentTime - startTime) / 500f);
9 scrollTo(0,(int)y);
10 if (getScrollY() == currentPosition * lineSpacing) {
11 lastPosition = currentPosition;
12 }
13 }
如果不实现弹性滑动的话,只要判断当前播放歌曲的时间是否大于当前位置歌词的结束时间,然后进行scrollTo(0,(int)currentPosition * lineSpacing)滑动即可。但是为了实现弹性滑动,我们需要将一次滑动分成若干次小的滑动并在一个时间段内完成,所以我们动态设置y的值,由于不断重绘,就能实现在0.5秒内完成View的滑动,这样就能实现歌词同步弹性滑动。
500其实就是0.5s,因为在这里currentTime和startTime的单位都是ms
最后
针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!
往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
0其实就是0.5s,因为在这里currentTime和startTime的单位都是ms
最后
针对Android程序员,我这边给大家整理了一些资料,包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!
往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、混合式开发(ReactNative+Weex)全方面的Android进阶实践技术,群内还有技术大牛一起讨论交流解决问题。
[外链图片转存中…(img-aBB10PDP-1714830632759)]
[外链图片转存中…(img-1DKqXmso-1714830632759)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!