android自定义listview教程(简约风歌词控件)
作者:jsyjst最近重构了之前的音乐播放器,添加了许多功能,比如歌词,下载功能等这篇文章就让我们聊聊歌词控件的实现,先上效果图,如果感觉海星,就继续瞧下去,今天小编就来说说关于android自定义listview教程?下面更多详细答案一起来看看吧!
android自定义listview教程
前言作者:jsyjst
最近重构了之前的音乐播放器,添加了许多功能,比如歌词,下载功能等。这篇文章就让我们聊聊歌词控件的实现,先上效果图,如果感觉海星,就继续瞧下去!
看到这里,估计你对这个控件还有点感兴趣的吧,那接下来就让我们来瞧瞧实现这个歌词控件需要做些什么!
一、 歌词解析首先,我们得知道正常的歌词格式是怎样的,大概是长这个样子:
1[ti:喜欢你]
2[ar:.]
3[al:]
4[by:]
5[offset:0]
6[00:00.10]喜欢你-G.E.M.邓紫棋(GemTang)
7[00:00.20]词:黄家驹
8[00:00.30]曲:黄家驹
9[00:00.40]编曲:LupoGroinig
10[00:00.50]
11[00:12.65]细雨带风湿透黄昏的街道
12[00:18.61]抹去雨水双眼无故地仰望
13[00:24.04]望向孤单的晚灯
14[00:26.91]
15[00:27.44]是那伤感的记忆
16[00:30.52]
17[00:34.12]再次泛起心里无数的思念
18[00:39.28]
19[00:40.10]以往片刻欢笑仍挂在脸上
20[00:45.49]愿你此刻可会知
21[00:48.23]
22[00:48.95]是我衷心的说声
23[00:53.06]
24[00:54.35]喜欢你那双眼动人
25[00:59.35]
26[01:00.10]笑声更迷人
27[01:02.37]
28[01:03.15]愿再可轻抚你
29[01:08.56]
30[01:09.35]那可爱面容
31[01:12.40]挽手说梦话
32[01:14.78]
33[01:15.48]像昨天你共我
34[01:20.84]
35[01:26.32]满带理想的我曾经多冲动
36[01:32.45]屡怨与她相爱难有自由
37[01:37.82]愿你此刻可会知
38[01:40.40]
39[01:41.25]是我衷心的说声
40[01:44.81]
41[01:46.39]喜欢你那双眼动人
42[01:51.72]
43[01:52.42]笑声更迷人
44[01:54.75]
45[01:55.48]愿再可轻抚你
46[02:00.93]
47[02:01.68]那可爱面容
48[02:03.99]
49[02:04.73]挽手说梦话
50[02:07.13]
51[02:07.82]像昨天你共我
52[02:14.53]
53[02:25.54]每晚夜里自我独行
54[02:29.30]随处荡多冰冷
55[02:35.40]
56[02:37.83]以往为了自我挣扎
57[02:41.62]从不知她的痛苦
58[02:52.02]
59[02:54.11]喜欢你那双眼动人
60[03:00.13]笑声更迷人
61[03:02.38]
62[03:03.14]愿再可轻抚你
63[03:08.77]
64[03:09.33]那可爱面容
65[03:11.71]
66[03:12.41]挽手说梦话
67[03:14.61]
68[03:15.45]像昨天你共我
从上面可以看出这种格式前面是开始时间,从左往右一一对应分,秒,毫秒,后面就是歌词。所以我们要创建一个实体类来保存每一句的歌词信息。
1.歌词实体类LrcBean
1publicclassLrcBean{
2privateStringlrc;//歌词
3privatelongstart;//开始时间
4privatelongend;//结束时间
5
6publicStringgetLrc(){
7returnlrc;
8}
9
10publicvoidsetLrc(Stringlrc){
11this.lrc=lrc;
12}
13
14publiclonggetStart(){
15returnstart;
16}
17
18publicvoidsetStart(longstart){
19this.start=start;
20}
21
22publiclonggetEnd(){
23returnend;
24}
25
26publicvoidsetEnd(longend){
27this.end=end;
28}
29}
每句歌词,我们需要开始时间,结束时间和歌词这些信息,那么你就会有疑问了?上面提到的歌词格式好像只有歌词开始时间,那我们怎么知道结束时间呢?其实很简单,这一句歌词的开始时间就是上一句歌词的结束时间。有了歌词实体类,我们就得开始对歌词进行解析了!
2. 解析歌词工具类LrcUtil
1publicclassLrcUtil{
2
3/**
4*解析歌词,将字符串歌词封装成LrcBean的集合
5*@paramlrcStr字符串的歌词,歌词有固定的格式,一般为
6*[ti:喜欢你]
7*[ar:.]
8*[al:]
9*[by:]
10*[offset:0]
11*[00:00.10]喜欢你-G.E.M.邓紫棋(GemTang)
12*[00:00.20]词:黄家驹
13*[00:00.30]曲:黄家驹
14*[00:00.40]编曲:Lupo Groinig
15*@return歌词集合
16*/
17publicstaticList<LrcBean>parseStr2List(StringlrcStr){
18List<LrcBean>res=newArrayList<>();
19//根据转行字符对字符串进行分割
20String[]subLrc=lrcStr.split("\n");
21//跳过前四行,从第五行开始,因为前四行的歌词我们并不需要
22for(inti=5;i<subLrc.length;i ){
23StringlineLrc=subLrc[i];
24//[00:00.10]喜欢你-G.E.M.邓紫棋(GemTang)
25Stringmin=lineLrc.substring(lineLrc.indexOf("[") 1,lineLrc.indexOf("[") 3);
26Stringsec=lineLrc.substring(lineLrc.indexOf(":") 1,lineLrc.indexOf(":") 3);
27Stringmills=lineLrc.substring(lineLrc.indexOf(".") 1,lineLrc.indexOf(".") 3);
28//进制转化,转化成毫秒形式的时间
29longstartTime=getTime(min,sec,mills);
30//歌词
31StringlrcText=lineLrc.substring(lineLrc.indexOf("]") 1);
32//有可能是某个时间段是没有歌词,则跳过下面
33if(lrcText.equals(""))continue;
34//在第一句歌词中有可能是很长的,我们只截取一部分,即歌曲加演唱者
35//比如光年之外(《太空旅客(Passengers)》电影中国区主题曲)-G.E.M.邓紫棋(GemTang)
36if(i==5){
37intlineIndex=lrcText.indexOf("-");
38intfirst=lrcText.indexOf("(");
39if(first<lineIndex&&first!=-1){
40lrcText=lrcText.substring(0,first) lrcText.substring(lineIndex);
41}
42LrcBeanlrcBean=newLrcBean();
43lrcBean.setStart(startTime);
44lrcBean.setLrc(lrcText);
45res.add(lrcBean);
46continue;
47}
48//添加到歌词集合中
49LrcBeanlrcBean=newLrcBean();
50lrcBean.setStart(startTime);
51lrcBean.setLrc(lrcText);
52res.add(lrcBean);
53//如果是最后一句歌词,其结束时间是不知道的,我们将人为的设置为开始时间加上100s
54if(i==subLrc.length-1){
55res.get(res.size()-1).setEnd(startTime 100000);
56}elseif(res.size()>1){
57//当集合数目大于1时,这句的歌词的开始时间就是上一句歌词的结束时间
58res.get(res.size()-2).setEnd(startTime);
59}
60
61}
62returnres;
63}
64
65/**
66*根据时分秒获得总时间
67*@parammin分钟
68*@paramsec秒
69*@parammills毫秒
70*@return总时间
71*/
72privatestaticlonggetTime(Stringmin,Stringsec,Stringmills){
73returnLong.valueOf(min)*60*1000 Long.valueOf(sec)*1000 Long.valueOf(mills);
74}
75}
相信上面的代码和注释已经将这个歌词解析解释的挺明白了,需要注意的是上面对i=5,也就是歌词真正开始的第一句做了特殊处理,因为i=5这句有可能是很长的,假设i=5是“光年之外(《太空旅客(Passengers)》电影中国区主题曲) - G.E.M. 邓紫棋 (GemTang)”这句歌词,如果我们不做特殊处理,在后面绘制的时候,就会发现这句歌词会超过屏幕大小,很影响美观,所以我们只截取歌曲名和演唱者,有些说明直接省略掉了。解析好了歌词,接下来就是重头戏-歌词绘制!
二、歌词绘制歌词绘制就涉及到了自定义View的知识,所以还未接触自定义View的小伙伴需要先去看看自定View的基础知识。歌词绘制的主要工作主要由下面几部分构成:
在res文件中的values中新建一个attrs.xml文件,然后定义歌词的自定义View属性
1<?xmlversion="1.0"encoding="utf-8"?>
2<resources>
3<declare-styleablename="LrcView">
4<attrname="highLineTextColor"format="color|reference|integer"/>
5<attrname="lrcTextColor"format="color|reference|integer"/>
6<attrname="lineSpacing"format="dimension"/>
7<attrname="textSize"format="dimension"/>
8</declare-styleable>
9</resources>
这里只自定义了歌词颜色,歌词高亮颜色,歌词大小,歌词行间距的属性,可根据自己需要自行添加。
然后在Java代码中,设置默认值。
1privateintlrcTextColor;//歌词颜色
2privateinthighLineTextColor;//当前歌词颜色
3privateintwidth,height;//屏幕宽高
4privateintlineSpacing;//行间距
5privateinttextSize;//字体大小
6
7publicLrcView(Contextcontext,@NullableAttributeSetattrs,intdefStyleAttr){
8super(context,attrs,defStyleAttr);
9TypedArrayta=context.obtainStyledAttributes(attrs,R.styleable.LrcView);
10lrcTextColor=ta.getColor(R.styleable.LrcView_lrcTextColor,Color.GRAY);
11highLineTextColor=ta.getColor(R.styleable.LrcView_highLineTextColor,Color.BLUE);
12floatfontScale=context.getResources().getDisplayMetrics().scaledDensity;
13floatscale=context.getResources().getDisplayMetrics().density;
14//默认字体大小为16sp
15textSize=ta.getDimensionPixelSize(R.styleable.LrcView_textSize,(int)(16*fontScale));
16//默认行间距为30dp
17lineSpacing=ta.getDimensionPixelSize(R.styleable.LrcView_lineSpacing,(int)(30*scale));
18//回收
19ta.recycle();
20}
1privatevoidinit(){
2//初始化歌词画笔
3dPaint=newPaint();
4dPaint.setStyle(Paint.Style.FILL);//填满
5dPaint.setAntiAlias(true);//抗锯齿
6dPaint.setColor(lrcTextColor);//画笔颜色
7dPaint.setTextSize(textSize);//歌词大小
8dPaint.setTextAlign(Paint.Align.CENTER);//文字居中
9
10//初始化当前歌词画笔
11hPaint=newPaint();
12hPaint.setStyle(Paint.Style.FILL);
13hPaint.setAntiAlias(true);
14hPaint.setColor(highLineTextColor);
15hPaint.setTextSize(textSize);
16hPaint.setTextAlign(Paint.Align.CENTER);
17}
我们把初始化的方法放到了构造方法中,这样就可以避免在重绘时再次初始化。另外由于我们把init方法只放到了第三个构造方法中,所以在上面两个构造方法需要将super改成this,这样就能保证哪个构造方法都能执行init方法
1publicLrcView(Contextcontext){
2this(context,null);
3}
4
5publicLrcView(Contextcontext,@NullableAttributeSetattrs){
6this(context,attrs,0);
7}
8
9publicLrcView(Contextcontext,@NullableAttributeSetattrs,intdefStyleAttr){
10super(context,attrs,defStyleAttr);
11TypedArrayta=context.obtainStyledAttributes(attrs,R.styleable.LrcView);
12......
13//回收
14ta.recycle();
15init();
16}
因为后面的步骤都是在onDraw方法中执行的,所以我们先贴出onDraw方法中的代码
1@Override
2protectedvoidonDraw(Canvascanvas){
3super.onDraw(canvas);
4
5getMeasuredWidthAndHeight();//得到测量后的宽高
6getCurrentPosition();//得到当前歌词的位置
7drawLrc(canvas);//画歌词
8scrollLrc();//歌词滑动
9postInvalidateDelayed(100);//延迟0.1s刷新
10}
1.获得控件的测量后的宽高
1privateintwidth,height;//屏幕宽高
2privatevoidgetMeasuredWidthAndHeight(){
3if(width==0||height==0){
4width=getMeasuredWidth();
5height=getMeasuredHeight();
6}
7}
为什么要获得控件的宽高呢?因为在下面我们需要画歌词,画歌词时需要画的位置,这时候就需要用到控件的宽高了。
2. 得到当前歌词的位置
1privateList<LrcBean>lrcBeanList;//歌词集合
2privateintcurrentPosition;//当前歌词的位置
3privateMediaPlayerplayer;//当前的播放器
4
5
6privatevoidgetCurrentPosition(){
7intcurTime=player.getCurrentPosition();
8//如果当前的时间大于10分钟,证明歌曲未播放,则当前位置应该为0
9if(curTime<lrcBeanList.get(0).getStart()||curTime>10*60*1000){
10currentPosition=0;
11return;
12}elseif(curTime>lrcBeanList.get(lrcBeanList.size()-1).getStart()){
13currentPosition=lrcBeanList.size()-1;
14return;
15}
16for(inti=0;i<lrcBeanList.size();i ){
17if(curTime>=lrcBeanList.get(i).getStart()&&curTime<=lrcBeanList.get(i).getEnd()){
18currentPosition=i;
19}
20}
21}
我们根据当前播放的歌曲时间来遍历歌词集合,从而判断当前播放的歌词的位置。细心的你可能会发现在currentPosition = 0中有个curTime>10*60*1000的判断,这是因为在实际使用中发现当player还未播放时,这时候得到的curTime会很大,所以才有了这个判断(因为正常的歌曲不会超过10分钟)。
在这个方法我们会发现出现了歌词集合和播放器,你可能会感到困惑,这些不是还没赋值吗?困惑就对了,所以我们需要提供外部方法来给外部传给歌词控件歌词集合和播放器。
1//将歌词集合传给到这个自定义View中
2publicLrcViewsetLrc(Stringlrc){
3lrcBeanList=LrcUtil.parseStr2List(lrc);
4returnthis;
5}
6
7//传递mediaPlayer给自定义View中
8publicLrcViewsetPlayer(MediaPlayerplayer){
9this.player=player;
10returnthis;
11}
外部方法中setLrc的参数必须是前面提到的标准歌词格式的字符串形式,这样我们就能利用上文的解析工具类LrcUtil中的解析方法将字符串解析成歌词集合。
3. 画歌词
1privatevoiddrawLrc(Canvascanvas){
2for(inti=0;i<lrcBeanList.size();i ){
3if(currentPosition==i){//如果是当前的歌词就用高亮的画笔画
4canvas.drawText(lrcBeanList.get(i).getLrc(),width/2,height/2 i*lineSpacing,hPaint);
5}else{
6canvas.drawText(lrcBeanList.get(i).getLrc(),width/2,height/2 i*lineSpacing,dPaint);
7}
8}
9}
知道了当前歌词的位置就很容易画歌词了。遍历歌词集合,如果是当前歌词,则用高亮的画笔画,其它歌词就用普通画笔画。这里需注意的是两支画笔画的位置公式都是一样的,坐标位置为x=宽的一半,y=高的一半 当前位置*行间距。随着当前位置的变化,就能画出上下句歌词来。所以其实绘制出来后你会发现歌词是从控件的正中央开始绘制的,这是为了方便与下面歌词同步滑动功能配合。
4. 歌词同步滑动
1//歌词滑动
2privatevoidscrollLrc(){
3//下一句歌词的开始时间
4longstartTime=lrcBeanList.get(currentPosition).getStart();
5longcurrentTime=player.getCurrentPosition();
6
7//判断是否换行,在0.5内完成滑动,即实现弹性滑动
8floaty=(currentTime-startTime)>500?currentPosition*lineSpacing:lastPosition*lineSpacing (currentPosition-lastPosition)*lineSpacing*((currentTime-startTime)/500f);
9scrollTo(0,(int)y);
10if(getScrollY()==currentPosition*lineSpacing){
11lastPosition=currentPosition;
12}
13}
如果不实现弹性滑动的话,只要判断当前播放歌曲的时间是否大于当前位置歌词的结束时间,然后进行scrollTo(0,(int)currentPosition * lineSpacing)滑动即可。但是为了实现弹性滑动,我们需要将一次滑动分成若干次小的滑动并在一个时间段内完成,所以我们动态设置y的值,由于不断重绘,就能实现在0.5秒内完成View的滑动,这样就能实现歌词同步弹性滑动。
500其实就是0.5s,因为在这里currentTime和startTime的单位都是ms
1floaty=(currentTime-startTime)>500?currentPosition*lineSpacing:lastPosition*lineSpacing (currentPosition-lastPosition)*lineSpacing*((currentTime-startTime)/500f);
5.不断重绘
通过不断重绘才能实现歌词同步滑动,这里每隔0.1s进行重绘
1postInvalidateDelayed(100);//延迟0.1s刷新
你以为这样就结束了吗?其实还没有,答案下文揭晓!
三 、使用然后我们兴高采烈的在xml中,引用这个自定义View
LrcView前面的名称为你建这个类的完整包名
1<com.examplebrary.view.LrcView
2android:id="@ id/lrcView"
3android:layout_width="match_parent"
4android:layout_height="match_parent"
5app:lineSpacing="40dp"
6app:textSize="18sp"
7app:lrcTextColor="@color/colorPrimary"
8app:highLineTextColor="@color/highTextColor"
9/>
在Java代码中给这个自定义View传入标准歌词字符串和播放器。
1lrcView.setLrc(lrc).setPlayer(player);
点击运行,满心期待自己的成果,接着你就会一脸懵逼,what?怎么是一片空白,什么也没有!其实这时候你重新理一下上面歌词绘制的流程,就会发现问题所在。首先我们的自定义View控件引用到布局中时是先执行onDraw方法的,所以当你调用setLrc和setPlayer方法后,是不会再重新调用onDraw方法的,等于你并没有传入歌词字符串和播放器,所以当然会显示一片空白
解决方法 :我们在刚才自定义View歌词控件中添加一个外部方法来调用onDraw,刚好这个invalidate()就能够重新调用onDraw方法
1publicLrcViewdraw(){
2currentPosition=0;
3lastPosition=0;
4invalidate();
5returnthis;
6}
然后我们在主代码中,在调用setLrc和setPlayer后还得调用draw方法
1lrcView.setLrc(lrc).setPlayer(player).draw();
这样我们节约风的歌词控件就大功告成了。
最后在这里我分享一份由大佬亲自收录整理的Android学习PDF 架构视频 面试文档 源码笔记,高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料
这些都是我现在闲暇时还会反复翻阅的精品资料。里面对近几年的大厂面试高频知识点都有详细的讲解。相信可以有效地帮助大家掌握知识、理解原理,帮助大家在未来取得一份不错的答卷。
当然,你也可以拿去查漏补缺,提升自身的竞争力。
真心希望可以帮助到大家,Android路漫漫,共勉!
如果你有需要的话,只需私信我【进阶】即可获取
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com