Android开发—-lrc歌词的同步展示

别的不说,先上图,看效果

LrcFragment.java代码如下:
package com.android.administrator.happymusic.fragment;import android.Manifest;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;import com.android.administrator.happymusic.R;
import com.android.administrator.happymusic.base.BaseFragment;
import com.android.administrator.happymusic.bean.SongBean;
import com.android.administrator.happymusic.constant.FlagConstant;
import com.android.administrator.happymusic.constant.UrlConstant;
import com.android.administrator.happymusic.lyric.DefaultLrcBuilder;
import com.android.administrator.happymusic.lyric.ILrcBuilder;
import com.android.administrator.happymusic.lyric.ILrcViewListener;
import com.android.administrator.happymusic.lyric.LrcRow;
import com.android.administrator.happymusic.lyric.LrcView;
import com.android.administrator.happymusic.testutils.LogUtils;
import com.android.administrator.happymusic.untils.PlayMicService;
import com.zhy.http.okhttp.OkHttpUtils;
import com.zhy.http.okhttp.callback.FileCallBack;import org.apache.http.util.EncodingUtils;import java.io.File;
import java.io.FileInputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;import okhttp3.Call;//展示的歌词的Fragment
public class LyricFragment extends BaseFragment {private LrcView lrcView;//自定义View---用来展示歌词private int updateLrcDuration = 1000;//更新歌词的频率,每秒更新一次private Timer timer;//更新歌词的定时器private TimerTask task;//更新歌词的定时任务private String lyric_path;//歌词的路径private String music_name;//歌名private BroadcastReceiver receiver;private int lrc_flag;private View view;@Overridepublic void onAttach(Context context) {super.onAttach(context);//接收PlayAcitivity传递的歌曲信息if (getArguments() != null) {Bundle arguments = getArguments();lyric_path = arguments.getString("lyric_path");//歌词music_name = arguments.getString("music_name");//歌曲名lrc_flag = arguments.getInt("lrc_flag");//标识符}}@Nullable@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {view = inflater.inflate(R.layout.fragment_lyric, container, false);//初始化ViewinitView(view);if (lrc_flag == 100) {getPermission();lrc_flag = 0;}else {readLocalLrc(lyric_path);}//注册本地音乐列表的广播receiver = new LocalMusicBroadcast();IntentFilter filter1 = new IntentFilter();filter1.addAction(FlagConstant.LOCAL_MUSIC);getActivity().registerReceiver(receiver,filter1);return view;}//接收PlayActivity的广播信息class LocalMusicBroadcast extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {if (intent != null){lyric_path = intent.getStringExtra("lrc_path");lrc_flag = intent.getIntExtra("LRC_FLAG", 0);}}}public static final int READ_EXTERNAL_STORAGE_REQUEST_CODE = 101;/*** 获取运行时权限------------特别说明:Android6.0之后,请求手机系统的SDCard权限* 需要动态过,当第一次运行APP时,弹出请求对话框*/public void getPermission() {// 如果权限没有被授予if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) !=PackageManager.PERMISSION_GRANTED) {// 申请权限ActivityCompat.requestPermissions(getActivity(),new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, READ_EXTERNAL_STORAGE_REQUEST_CODE);} else {//授予权限File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), "lrc");if (!file.exists()) {//如果文件夹不存在,则新建file.mkdir();}try {OkHttpUtils.get().url(lyric_path).build().execute(new FileCallBack(file.getAbsolutePath(), music_name + ".lrc") {@Overridepublic void onError(Call call, Exception e, int i) {LogUtils.e("TAG", "保存歌词失败" + e.getMessage());}@Overridepublic void onResponse(File file, int i) {LogUtils.e("TAG", "保存歌词成功" + file);//读取SD卡网络下载歌词文件readDataFromSD(file);}});} catch (IllegalArgumentException e) {Toast.makeText(getContext(), "未找到歌词,赶快去下载吧....", Toast.LENGTH_SHORT).show();}}}@Overridepublic void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {super.onRequestPermissionsResult(requestCode, permissions, grantResults);switch (requestCode) {case READ_EXTERNAL_STORAGE_REQUEST_CODE:if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {Toast.makeText(getContext(), "同意SD卡权限访问", Toast.LENGTH_SHORT).show();} else {//用户拒绝授予权限Toast.makeText(getContext(), "您拒绝了权限访问", Toast.LENGTH_SHORT).show();}break;}}/*** 读取本地歌词文件** @param lrc_path*/public void readLocalLrc(String lrc_path) {try {// FileInputSteam 输入流的对象FileInputStream fis = new FileInputStream(lrc_path);//准备一个字节数组用户装即将读取的数据byte[] buffer = new byte[fis.available()];//开始进行文件的读取fis.read(buffer);//关闭流fis.close();//将字节数组转换成字符串, 并转换编码的格式String res = EncodingUtils.getString(buffer, "UTF-8");//读取歌词并播放歌词playLrc(res);} catch (Exception ex) {Toast.makeText(getContext(), "文件读取失败!", Toast.LENGTH_SHORT).show();}}/*** 读取SD卡网络下载歌词文件** @param file 保存的文件路径*/public void readDataFromSD(File file) {try {// FileInputSteam 输入流的对象FileInputStream fis = new FileInputStream(file);//准备一个字节数组用户装即将读取的数据byte[] buffer = new byte[fis.available()];//开始进行文件的读取fis.read(buffer);//关闭流fis.close();//将字节数组转换成字符串, 并转换编码的格式String res = EncodingUtils.getString(buffer, "UTF-8");//读取歌词并播放歌词playLrc(res);} catch (Exception ex) {Toast.makeText(getContext(), "文件读取失败!", Toast.LENGTH_SHORT).show();}}/*** 读取歌词并播放歌词** @param res*/private void playLrc(String res) {//解析歌词构造器ILrcBuilder builder = new DefaultLrcBuilder();//解析歌词返回LrcRow集合List<LrcRow> rows = builder.getLrcRows(res);//将得到的歌词集合传给mLrcView用来展示lrcView.setLrc(rows);//设置自定义的LrcView上下拖动歌词时监听lrcView.setListener(new ILrcViewListener() {//当歌词被用户上下拖动的时候回调该方法,从高亮的那一句歌词开始播放@Overridepublic void onLrcSeeked(int newPosition, LrcRow row) {if (PlayMicService.player != null) {PlayMicService.player.seekTo((int) row.getTime());}}});if (timer == null) {timer = new Timer();task = new LrcTask();timer.scheduleAtFixedRate(task, 0, updateLrcDuration);}}//停止歌词滚动public void stopLrcPlay() {if (timer != null) {timer.cancel();timer = null;}}private class LrcTask extends TimerTask {@Overridepublic void run() {//获取歌曲播放的位置final long timePassed = PlayMicService.player.getCurrentPosition();if (getActivity() != null) {getActivity().runOnUiThread(new Runnable() {@Overridepublic void run() {//滚动歌词lrcView.seekLrcToTime(timePassed);}});}}}/*** 初始化View** @param view*/private void initView(View view) {lrcView = ((LrcView) view.findViewById(R.id.lrcView));}
}

DefaultLrcBuilder.java代码如下:

package com.android.administrator.happymusic.lyric;import android.util.Log;import com.android.administrator.happymusic.testutils.LogUtils;import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;/*** 解析歌词,得到LrcRow的集合*/
public class DefaultLrcBuilder implements ILrcBuilder {static final String TAG = "DefaultLrcBuilder";public List<LrcRow> getLrcRows(String rawLrc) {LogUtils.e(TAG,"getLrcRows by rawString");if(rawLrc == null || rawLrc.length() == 0){LogUtils.e(TAG,"getLrcRows rawLrc null or empty");return null;}StringReader reader = new StringReader(rawLrc);BufferedReader br = new BufferedReader(reader);String line = null;List<LrcRow> rows = new ArrayList<LrcRow>();try{//循环地读取歌词的每一行do{line = br.readLine();/**一行歌词只有一个时间的  例如:徐佳莹   《我好想你》[01:15.33]我好想你 好想你一行歌词有多个时间的  例如:草蜢 《失恋战线联盟》[02:34.14][01:07.00]当你我不小心又想起她[02:45.69][02:42.20][02:37.69][01:10.60]就在记忆里画一个叉**/LogUtils.e(TAG,"lrc raw line: " + line);if(line != null && line.length() > 0){//解析每一行歌词 得到每行歌词的集合,因为有些歌词重复有多个时间,就可以解析出多个歌词行来List<LrcRow> lrcRows = LrcRow.createRows(line);if(lrcRows != null && lrcRows.size() > 0){for(LrcRow row : lrcRows){rows.add(row);}}}}while(line != null);if( rows.size() > 0 ){// 根据歌词行的时间排序Collections.sort(rows);if(rows!=null&&rows.size()>0){for(LrcRow lrcRow:rows){Log.d(TAG, "lrcRow:" + lrcRow.toString());}}}}catch(Exception e){Log.e(TAG,"parse exceptioned:" + e.getMessage());return null;}finally{try {br.close();} catch (IOException e) {e.printStackTrace();}reader.close();}return rows;}
}

LrcRow.java

package com.android.administrator.happymusic.lyric;import android.util.Log;import java.util.ArrayList;
import java.util.List;/*** 歌词行* 包括该行歌词的时间,歌词内容*/
public class LrcRow implements Comparable<LrcRow>{public final static String TAG = "LrcRow";/** 该行歌词要开始播放的时间,格式如下:[02:34.14] */private String strTime;/** 该行歌词要开始播放的时间,由[02:34.14]格式转换为long型,* 即将2分34秒14毫秒都转为毫秒后 得到的long型值:time=02*60*1000+34*1000+14*/private long time;/** 该行歌词的内容 */private String content;public LrcRow(){}public LrcRow(String strTime,long time,String content){this.strTime = strTime;this.time = time;this.content = content;}public String getStrTime() {return strTime;}public void setStrTime(String strTime) {this.strTime = strTime;}public long getTime() {return time;}public void setTime(long time) {this.time = time;}public String getContent() {return content;}public void setContent(String content) {this.content = content;}@Overridepublic String toString() {return "[" + strTime + " ]"  + content;}/*** 读取歌词的每一行内容,转换为LrcRow,加入到集合中*/public static List<LrcRow> createRows(String standardLrcLine){/**一行歌词只有一个时间的  例如:徐佳莹   《我好想你》[01:15.33]我好想你 好想你一行歌词有多个时间的  例如:草蜢 《失恋战线联盟》[02:34.14][01:07.00]当你我不小心又想起她[02:45.69][02:42.20][02:37.69][01:10.60]就在记忆里画一个叉**/try{if(standardLrcLine.indexOf("[") != 0 || standardLrcLine.indexOf("]") != 9 ){return null;}//[02:34.14][01:07.00]当你我不小心又想起她//找到最后一个 ‘]’ 的位置int lastIndexOfRightBracket = standardLrcLine.lastIndexOf("]");//歌词内容就是 ‘]’ 的位置之后的文本   eg:   当你我不小心又想起她String content = standardLrcLine.substring(lastIndexOfRightBracket + 1, standardLrcLine.length());//歌词时间就是 ‘]’ 的位置之前的文本   eg:   [02:34.14][01:07.00]/**将时间格式转换一下  [mm:ss.SS][mm:ss.SS] 转换为  -mm:ss.SS--mm:ss.SS-即:[02:34.14][01:07.00]  转换为      -02:34.14--01:07.00-*/String times = standardLrcLine.substring(0,lastIndexOfRightBracket + 1).replace("[", "-").replace("]", "-");//通过 ‘-’ 来拆分字符串String arrTimes[] = times.split("-");List<LrcRow> listTimes = new ArrayList<LrcRow>();for(String temp : arrTimes){if(temp.trim().length() == 0){continue;}/** [02:34.14][01:07.00]当你我不小心又想起她*上面的歌词的就可以拆分为下面两句歌词了[02:34.14]当你我不小心又想起她[01:07.00]当你我不小心又想起她*/LrcRow lrcRow = new LrcRow(temp, timeConvert(temp), content);listTimes.add(lrcRow);}return listTimes;}catch(Exception e){return null;}}/*** 将解析得到的表示时间的字符转化为Long型*/private static long timeConvert(String timeString){//因为给如的字符串的时间格式为XX:XX.XX,返回的long要求是以毫秒为单位//将字符串 XX:XX.XX 转换为 XX:XX:XXtimeString = timeString.replace('.', ':');//将字符串 XX:XX:XX 拆分String[] times = timeString.split(":");// mm:ss:SSreturn Integer.valueOf(times[0]) * 60 * 1000 +//分Integer.valueOf(times[1]) * 1000 +//秒Integer.valueOf(times[2]) ;//毫秒}/*** 排序的时候,根据歌词的时间来排序*/public int compareTo(LrcRow another) {return (int)(this.time - another.time);}
}

LrcView.java代码如下:

package com.android.administrator.happymusic.lyric;import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.PointF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;import java.util.List;/*** 自定义LrcView,可以同步显示歌词,拖动歌词,缩放歌词*/
public class LrcView extends View implements ILrcView {public final static String TAG = "LrcView";/*** 正常歌词模式*/public final static int DISPLAY_MODE_NORMAL = 0;/*** 拖动歌词模式*/public final static int DISPLAY_MODE_SEEK = 1;/*** 缩放歌词模式*/public final static int DISPLAY_MODE_SCALE = 2;/*** 歌词的当前展示模式*/private int mDisplayMode = DISPLAY_MODE_NORMAL;/*** 歌词集合,包含所有行的歌词*/private List<LrcRow> mLrcRows;/*** 最小移动的距离,当拖动歌词时如果小于该距离不做处理*/private int mMinSeekFiredOffset = 10;/*** 当前高亮歌词的行数*/private int mHignlightRow = 0;/*** 当前高亮歌词的字体颜色为黄色*/private int mHignlightRowColor = Color.YELLOW;/*** 不高亮歌词的字体颜色为白色*/private int mNormalRowColor = Color.WHITE;/*** 拖动歌词时,在当前高亮歌词下面的一条直线的字体颜色**/private int mSeekLineColor = Color.CYAN;/*** 拖动歌词时,展示当前高亮歌词的时间的字体颜色**/private int mSeekLineTextColor = Color.CYAN;/*** 拖动歌词时,展示当前高亮歌词的时间的字体大小默认值**/private int mSeekLineTextSize = 18;/*** 拖动歌词时,展示当前高亮歌词的时间的字体大小最小值**/private int mMinSeekLineTextSize = 16;/*** 拖动歌词时,展示当前高亮歌词的时间的字体大小最大值**/private int mMaxSeekLineTextSize = 22;/*** 歌词字体大小默认值**/private int mLrcFontSize = 30;    // font size of lrc/*** 歌词字体大小最小值**/private int mMinLrcFontSize = 16;/*** 歌词字体大小最大值**/private int mMaxLrcFontSize = 50;/*** 两行歌词之间的间距**/private int mPaddingY = 10;/*** 拖动歌词时,在当前高亮歌词下面的一条直线的起始位置**/private int mSeekLinePaddingX = 0;/*** 拖动歌词的监听类,回调LrcViewListener类的onLrcSeeked方法**/private ILrcViewListener mLrcViewListener;/*** 当没有歌词的时候展示的内容**/private String mLoadingLrcTip = "Downloading lrc...";private Paint mPaint;public LrcView(Context context, AttributeSet attr) {super(context, attr);mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mPaint.setTextSize(mLrcFontSize);}//歌词的点击监听public void setListener(ILrcViewListener l) {mLrcViewListener = l;}//设置进度文本public void setLoadingTipText(String text) {mLoadingLrcTip = text;}@Overrideprotected void onDraw(Canvas canvas) {final int height = getHeight(); // height of this viewfinal int width = getWidth(); // width of this view//当没有歌词的时候if (mLrcRows == null || mLrcRows.size() == 0) {if (mLoadingLrcTip != null) {//画没有歌词时的页面信息mPaint.setColor(mHignlightRowColor);mPaint.setTextSize(mLrcFontSize);mPaint.setTextAlign(Align.CENTER);canvas.drawText(mLoadingLrcTip, width / 2, height / 2 - mLrcFontSize, mPaint);}return;}int rowY = 0;final int rowX = width / 2;int rowNum = 0;/*** 分以下三步来绘制歌词:**  第1步:高亮地画出正在播放的那句歌词*  第2步:画出正在播放的那句歌词的上面可以展示出来的歌词*  第3步:画出正在播放的那句歌词的下面的可以展示出来的歌词*/// 1、 高亮地画出正在要高亮的的那句歌词String highlightText = mLrcRows.get(mHignlightRow).getContent();int highlightRowY = height / 2 - mLrcFontSize;mPaint.setColor(mHignlightRowColor);mPaint.setTextSize(mLrcFontSize);mPaint.setTextAlign(Align.CENTER);canvas.drawText(highlightText, rowX, highlightRowY, mPaint);// 上下拖动歌词的时候 画出拖动要高亮的那句歌词的时间 和 高亮的那句歌词下面的一条直线if (mDisplayMode == DISPLAY_MODE_SEEK) {// 画出高亮的那句歌词下面的一条直线mPaint.setColor(mSeekLineColor);//该直线的x坐标从0到屏幕宽度  y坐标为高亮歌词和下一行歌词中间canvas.drawLine(mSeekLinePaddingX, highlightRowY + mPaddingY, width - mSeekLinePaddingX, highlightRowY + mPaddingY, mPaint);// 画出高亮的那句歌词的时间mPaint.setColor(mSeekLineTextColor);mPaint.setTextSize(mSeekLineTextSize);mPaint.setTextAlign(Align.LEFT);canvas.drawText(mLrcRows.get(mHignlightRow).getStrTime(), 0, highlightRowY, mPaint);}// 2、画出正在播放的那句歌词的上面可以展示出来的歌词mPaint.setColor(mNormalRowColor);mPaint.setTextSize(mLrcFontSize);mPaint.setTextAlign(Align.CENTER);rowNum = mHignlightRow - 1;rowY = highlightRowY - mPaddingY - mLrcFontSize;//画出正在播放的那句歌词的上面所有的歌词while( rowY > -mLrcFontSize && rowNum >= 0){String text = mLrcRows.get(rowNum).getContent();canvas.drawText(text, rowX, rowY, mPaint);rowY -=  (mPaddingY + mLrcFontSize);rowNum --;}// 3、画出正在播放的那句歌词的下面的可以展示出来的歌词rowNum = mHignlightRow + 1;rowY = highlightRowY + mPaddingY + mLrcFontSize;//画出正在播放的那句歌词的所有下面的可以展示出来的歌词while( rowY < height && rowNum < mLrcRows.size()){String text = mLrcRows.get(rowNum).getContent();canvas.drawText(text, rowX, rowY, mPaint);rowY += (mPaddingY + mLrcFontSize);rowNum ++;}}/*** 设置要高亮的歌词为第几行歌词** @param position 要高亮的歌词行数* @param cb       是否是手指拖动后要高亮的歌词*/public void seekLrc(int position, boolean cb) {if (mLrcRows == null || position < 0 || position > mLrcRows.size()) {return;}LrcRow lrcRow = mLrcRows.get(position);mHignlightRow = position;invalidate();//如果是手指拖动歌词后if (mLrcViewListener != null && cb) {//回调onLrcSeeked方法,将音乐播放器播放的位置移动到高亮歌词的位置mLrcViewListener.onLrcSeeked(position, lrcRow);}}private float mLastMotionY;/*** 第一个手指的坐标**/private PointF mPointerOneLastMotion = new PointF();/*** 第二个手指的坐标**/private PointF mPointerTwoLastMotion = new PointF();/*** 是否是第一次移动,当一个手指按下后开始移动的时候,设置为true,* 当第二个手指按下的时候,即两个手指同时移动的时候,设置为false*/private boolean mIsFirstMove = false;@Overridepublic boolean onTouchEvent(MotionEvent event) {if (mLrcRows == null || mLrcRows.size() == 0) {return super.onTouchEvent(event);}switch (event.getAction()) {//手指按下case MotionEvent.ACTION_DOWN:mLastMotionY = event.getY();mIsFirstMove = true;invalidate();break;//手指移动case MotionEvent.ACTION_MOVE:if (event.getPointerCount() == 2) {doScale(event);//双指按下,进行歌词大小缩放return true;}//如果是双指同时按下,进行歌词大小缩放,抬起其中一个手指,另外一个手指不离开屏幕地移动的话,不做任何处理if (mDisplayMode == DISPLAY_MODE_SCALE) {return true;}//如果一个手指按下,在屏幕上移动的话,拖动歌词上下doSeek(event);break;case MotionEvent.ACTION_CANCEL://手指抬起case MotionEvent.ACTION_UP:if (mDisplayMode == DISPLAY_MODE_SEEK) {//高亮手指抬起时的歌词并播放从该句歌词开始播放seekLrc(mHignlightRow, true);}mDisplayMode = DISPLAY_MODE_NORMAL;invalidate();break;}return true;}/*** 处理双指在屏幕移动时的,歌词大小缩放*/private void doScale(MotionEvent event) {//如果歌词的模式为:拖动歌词模式if (mDisplayMode == DISPLAY_MODE_SEEK) {//如果是单指按下,在进行歌词上下滚动,然后按下另外一个手指,则把歌词模式从 拖动歌词模式 变为 缩放歌词模式mDisplayMode = DISPLAY_MODE_SCALE;return;}if (mIsFirstMove) {mDisplayMode = DISPLAY_MODE_SCALE;invalidate();mIsFirstMove = false;//两个手指的x坐标和y坐标setTwoPointerLocation(event);}//获取歌词大小要缩放的比例int scaleSize = getScale(event);//如果缩放大小不等于0,进行缩放,重绘LrcViewif (scaleSize != 0) {setNewFontSize(scaleSize);invalidate();}setTwoPointerLocation(event);}/*** 处理单指在屏幕移动时,歌词上下滚动*/private void doSeek(MotionEvent event) {float y = event.getY();//手指当前位置的y坐标float offsetY = y - mLastMotionY; //第一次按下的y坐标和目前移动手指位置的y坐标之差//如果移动距离小于10,不做任何处理if (Math.abs(offsetY) < mMinSeekFiredOffset) {return;}//将模式设置为拖动歌词模式mDisplayMode = DISPLAY_MODE_SEEK;int rowOffset = Math.abs((int) offsetY / mLrcFontSize); //歌词要滚动的行数if (offsetY < 0) {//手指向上移动,歌词向下滚动mHignlightRow += rowOffset;//设置要高亮的歌词为 当前高亮歌词 向下滚动rowOffset行后的歌词} else if (offsetY > 0) {//手指向下移动,歌词向上滚动mHignlightRow -= rowOffset;//设置要高亮的歌词为 当前高亮歌词 向上滚动rowOffset行后的歌词}//设置要高亮的歌词为0和mHignlightRow中的较大值,即如果mHignlightRow < 0,mHignlightRow=0mHignlightRow = Math.max(0, mHignlightRow);//设置要高亮的歌词为0和mHignlightRow中的较小值,即如果mHignlight > RowmLrcRows.size()-1,mHignlightRow=mLrcRows.size()-1mHignlightRow = Math.min(mHignlightRow, mLrcRows.size() - 1);//如果歌词要滚动的行数大于0,则重画LrcViewif (rowOffset > 0) {mLastMotionY = y;invalidate();}}/*** 设置当前两个手指的x坐标和y坐标*/private void setTwoPointerLocation(MotionEvent event) {mPointerOneLastMotion.x = event.getX(0);mPointerOneLastMotion.y = event.getY(0);mPointerTwoLastMotion.x = event.getX(1);mPointerTwoLastMotion.y = event.getY(1);}/*** 设置缩放后的字体大小*/private void setNewFontSize(int scaleSize) {//设置歌词缩放后的的最新字体大小mLrcFontSize += scaleSize;mLrcFontSize = Math.max(mLrcFontSize, mMinLrcFontSize);mLrcFontSize = Math.min(mLrcFontSize, mMaxLrcFontSize);//设置显示高亮的那句歌词的时间最新字体大小mSeekLineTextSize += scaleSize;mSeekLineTextSize = Math.max(mSeekLineTextSize, mMinSeekLineTextSize);mSeekLineTextSize = Math.min(mSeekLineTextSize, mMaxSeekLineTextSize);}/*** 获取歌词大小要缩放的比例*/private int getScale(MotionEvent event) {float x0 = event.getX(0);float y0 = event.getY(0);float x1 = event.getX(1);float y1 = event.getY(1);float maxOffset = 0; // max offset between x or y axis,used to decide scale sizeboolean zoomin = false;//第一次双指之间的x坐标的差距float oldXOffset = Math.abs(mPointerOneLastMotion.x - mPointerTwoLastMotion.x);//第二次双指之间的x坐标的差距float newXoffset = Math.abs(x1 - x0);//第一次双指之间的y坐标的差距float oldYOffset = Math.abs(mPointerOneLastMotion.y - mPointerTwoLastMotion.y);//第二次双指之间的y坐标的差距float newYoffset = Math.abs(y1 - y0);//双指移动之后,判断双指之间移动的最大差距maxOffset = Math.max(Math.abs(newXoffset - oldXOffset), Math.abs(newYoffset - oldYOffset));//如果x坐标移动的多一些if (maxOffset == Math.abs(newXoffset - oldXOffset)) {//如果第二次双指之间的x坐标的差距大于第一次双指之间的x坐标的差距则是放大,反之则缩小zoomin = newXoffset > oldXOffset ? true : false;}//如果y坐标移动的多一些else {//如果第二次双指之间的y坐标的差距大于第一次双指之间的y坐标的差距则是放大,反之则缩小zoomin = newYoffset > oldYOffset ? true : false;}if (zoomin) {return (int) (maxOffset / 10);//放大双指之间移动的最大差距的1/10} else {return -(int) (maxOffset / 10);//缩小双指之间移动的最大差距的1/10}}/*** 设置歌词行集合* @param lrcRows*/public void setLrc(List<LrcRow> lrcRows) {mLrcRows = lrcRows;invalidate();}/*** 播放的时候调用该方法滚动歌词,高亮正在播放的那句歌词* @param time*/public void seekLrcToTime(long time) {if (mLrcRows == null || mLrcRows.size() == 0) {return;}if (mDisplayMode != DISPLAY_MODE_NORMAL) {return;}for (int i = 0; i < mLrcRows.size(); i++) {LrcRow current = mLrcRows.get(i);LrcRow next = i + 1 == mLrcRows.size() ? null : mLrcRows.get(i + 1);/***  正在播放的时间大于current行的歌词的时间而小于next行歌词的时间, 设置要高亮的行为current行*  正在播放的时间大于current行的歌词,而current行为最后一句歌词时,设置要高亮的行为current行*/if ((time >= current.getTime() && next != null && time < next.getTime())|| (time > current.getTime() && next == null)){seekLrc(i, false);return;}}}
}

歌词拖动时候的监听接口代码如下:

package com.android.administrator.happymusic.lyric;/*** 歌词拖动时候的监听类*/
public interface ILrcViewListener {/*** 当歌词被用户上下拖动的时候回调该方法*/void onLrcSeeked(int newPosition, LrcRow row);
}

展示歌词的接口:

package com.android.administrator.happymusic.lyric;import java.util.List;/*** 展示歌词的接口*/
public interface ILrcView {/*** 设置要展示的歌词行集合*/void setLrc(List<LrcRow> lrcRows);/*** 音乐播放的时候调用该方法滚动歌词,高亮正在播放的那句歌词*/void seekLrcToTime(long time);/*** 设置歌词拖动时候的监听类*/void setListener(ILrcViewListener l);
}

解析歌词接口:

package com.android.administrator.happymusic.lyric;import java.util.List;/*** 解析歌词,得到LrcRow的集合*/
public interface ILrcBuilder {List<LrcRow> getLrcRows(String rawLrc);
}

!!!!!最后别忘记在布局中使用自定义LrcView

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical" android:layout_width="match_parent"android:layout_height="match_parent"android:paddingTop="@dimen/hm_app_height_twenty"android:paddingBottom="@dimen/hm_app_height_twenty"><com.android.administrator.happymusic.lyric.LrcView
        android:id="@+id/lrcView"android:layout_width="match_parent"android:layout_height="match_parent" />
</LinearLayout>

到此就Ok了,最终的效果就是歌词滚动显示,同时当前演唱的那句歌词高亮,拖动歌词可以实现改变歌曲进度,双指滑动屏幕可以改变的字体大小.
能力有限,就只能做到目前的样子了,应该实现了基本的歌词展示功能…………………..

Android开发----lrc歌词的同步展示相关推荐

  1. 我的Android进阶之旅------Android自定义View来实现解析lrc歌词并同步滚动、上下拖动、缩放歌词的功能...

    前言 一LRC歌词文件简介 1什么是LRC歌词文件 2LRC歌词文件的格式 LRC歌词文件的标签类型 1标识标签 2时间标签 二解析LRC歌词 1读取出歌词文件 2解析得到的歌词内容 1表示每行歌词内 ...

  2. Android自定义View来实现解析lrc歌词并同步滚动、上下拖动、缩放歌词的功能

    原文地址https://blog.csdn.net/qq446282412/article/details/50813419 前言 一LRC歌词文件简介 1什么是LRC歌词文件 2LRC歌词文件的格式 ...

  3. 我的Android进阶之旅------gt;Android自定义View来实现解析lrc歌词并同步滚动、上下拖动、缩放歌词的功能...

    前言 一LRC歌词文件简介 1什么是LRC歌词文件 2LRC歌词文件的格式 LRC歌词文件的标签类型 1标识标签 2时间标签 二解析LRC歌词 1读取出歌词文件 2解析得到的歌词内容 1表示每行歌词内 ...

  4. Android自定义View来实现解析lrc歌词并同步滚动、上下拖动、缩放歌词的功能[转]

    前言 一LRC歌词文件简介 1什么是LRC歌词文件 2LRC歌词文件的格式 LRC歌词文件的标签类型 1标识标签 2时间标签 二解析LRC歌词 1读取出歌词文件 2解析得到的歌词内容 1表示每行歌词内 ...

  5. 【我的Android进阶之旅】Android自定义View来实现解析lrc歌词并同步滚动、上下拖动、缩放歌词、卡拉OK高亮的功能

    文章目录 前言 一.LRC歌词文件简介 1.什么是LRC歌词文件 2.LRC歌词文件的格式 LRC歌词文件的标签类型 1.标识标签 2.时间标签 二.解析LRC歌词 1.读取出歌词文件 2.解析得到的 ...

  6. android mp3 lrc歌词文件utf-8歌词显示为乱码,百度歌词显示乱码 LRC歌词批量转换 UTF-8编码批量转换为GB或ANSI 文本编码批量转换...

    百度歌词显示乱码LRC歌词批量转换UTF-8编码批量转换为GB或ANSI 文本编码批量转换 当从百度下载很多歌词的时候,发现在一些MP3上播放总是显示乱码,这时以为是MP3的问题或者是百度歌词的问题, ...

  7. android 本地lrc 歌词同步,android 音乐播放器-------歌词同步 lrc

    lrc格式 : [al:这首歌所在的唱片集 ] [ar:歌词作者 ] [by:本LRC文件的创建者 ] [offset:+/- 以毫秒为单位整体时间戳调整,+增加,-减小 ] [re:创建此LRC文件 ...

  8. android mp3 lrc歌词文件utf-8歌词显示为乱码,Android读取本地json文件的方法(解决显示乱码问题)...

    本文实例讲述了Android读取本地json文件的方法.分享给大家供大家参考,具体如下: 1.读取本地JSON ,但是显示汉字乱码 public static String readLocalJson ...

  9. Android开发学习之录音同步播放的实现

    最近看到一篇关于音频的文章,忽然想起以前有个中国传媒大学的一位朋友,要我帮她设计一个可以实时播放输入音频的程序,我当时想到了要用DirectSound,可是对于这种从来没有碰过的东西,我内心是多少有些 ...

最新文章

  1. ASP.NET MVC 4 (十一) Bundles和显示模式
  2. leetcode刷题 60 61
  3. 逗号表达式的值--最后一项的值
  4. “操作系统不以 C 开头和结尾,C 不等于整个世界”
  5. framset和fram的嵌套
  6. 关于三层交换机的智能流技术
  7. 代码整洁之道——9、格式化
  8. 共享内存、消息队列、信号量之ipcs命令详解
  9. 勒索病毒 -- “永恒之蓝”NSA 武器免疫工具
  10. Pygame实战之外星人入侵NO.12——点击按钮开始游戏
  11. ubuntu挂载raid硬盘_Ubuntu服务器挂载新硬盘的步骤
  12. SAR 三点回波模拟 正侧视RD算法(经典好用)
  13. 远程语音 开源_通过开源语音聊天简化远程会议
  14. oracle 无效连接,Oracle SQL多重连接与重复记录或“无效标识符”
  15. 【Vue】Vue打包文件后需要添加版本号Version,来防止更新后的页面有缓存
  16. 电脑蓝屏错误代码大全及解决办法
  17. miui系统分身测试软件,miui8系统分身
  18. ETL DataStage实现
  19. thinkjs查询mysql_Mysql · ThinkJs2.0开发手册 · 看云
  20. Android系统开发:GMS包移植

热门文章

  1. c++ string 回文串_C++刷题——2802: 判断字符串是否为回文
  2. USB4规范解读(三):深入了解USB4的系统结构和工作原理
  3. 7779 - KKT基本算法304保龄球340
  4. 搜索引擎如何面对Web3.0
  5. JavaIDE 添加git忽略文件
  6. type=Not Found, status=404
  7. RuntimeError: Failed to init API, possibly an invalid tessdata
  8. python提取发票信息发票识别_(附完整python源码)基于tensorflow、opencv的入门案例_发票识别二:字符分割...
  9. 心态爆炸啊.....
  10. mysql 建库与授权