前一篇已经将音乐播放及切换的相关逻辑弄好了,今天主要理一下剩余的部分,包含:
1. 自己定义通知栏的布局及逻辑处理
2. 滚动歌词的绘制
3. 歌词解析
效果图
通知栏
自己定义布局:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:id="@+id/layout_notification" android:padding="10dp" > <ImageView android:layout_width="40dp" android:layout_height="40dp" android:background="@mipmap/ic_launcher" /> <LinearLayout android:layout_width="0dp" android:layout_height="40dp" android:layout_weight="1" android:layout_marginLeft="10dp" android:orientation="vertical" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" android:text="标题" android:id="@+id/tv_notification_title" android:textColor="@color/white" android:textSize="17sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:singleLine="true" android:text="艺术家" android:id="@+id/tv_notification_content" android:textColor="@color/gray_white" android:textSize="14sp" /> </LinearLayout> <LinearLayout android:layout_width="0dp" android:layout_weight="1" android:gravity="center" android:orientation="horizontal" android:layout_height="40dp"> <ImageView android:layout_width="30dp" android:layout_height="30dp" android:layout_marginRight="20dp" android:id="@+id/btn_notification_pre" android:background="@mipmap/icon_notification_pre"/> <ImageView android:layout_width="30dp" android:layout_height="30dp" android:id="@+id/btn_notification_next" android:background="@mipmap/icon_notification_next"/> </LinearLayout> </LinearLayout>
通知栏的相关逻辑:
- 下一首
- 上一首
- 进入播放页
/** * 发送自己定义布局的通知 */ private void sendNotification() { Notification.Builder builder = new Notification.Builder(AudioPlayerService.this); builder.setOngoing(true) .setSmallIcon(R.mipmap.notification_music_playing) .setTicker("正在播放:" + StringUtil.formatAudioName(audioList.get(currentPosition).getTitle())) .setWhen(System.currentTimeMillis()) .setContent(getRemoteViews()); startForeground(1, builder.build()); } private RemoteViews getRemoteViews() { RemoteViews remoteViews = new RemoteViews(getPackageName(), R.layout.layout_music_notification); remoteViews.setTextViewText(R.id.tv_notification_title, StringUtil.formatAudioName(audioList.get(currentPosition).getTitle())); remoteViews.setTextViewText(R.id.tv_notification_content, audioList.get(currentPosition).getArtist()); remoteViews.setOnClickPendingIntent(R.id.btn_notification_pre, getPrePendingIntent()); remoteViews.setOnClickPendingIntent(R.id.btn_notification_next, getNextPendingIntent()); remoteViews.setOnClickPendingIntent(R.id.layout_notification, getContentPendingIntent()); return remoteViews; } private PendingIntent getPrePendingIntent() { Intent intent = new Intent(AudioPlayerService.this, AudioPlayerService.class); intent.putExtra("viewAction", ACTION_NOTIFICATION_PRE); intent.putExtra("isFromNotification", true); PendingIntent pendingIntent = PendingIntent.getService(AudioPlayerService.this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT); return pendingIntent; } private PendingIntent getNextPendingIntent() { Intent intent = new Intent(AudioPlayerService.this, AudioPlayerService.class); intent.putExtra("viewAction", ACTION_NOTIFICATION_NEXT); intent.putExtra("isFromNotification", true); PendingIntent pendingIntent = PendingIntent.getService(AudioPlayerService.this, 2, intent, PendingIntent.FLAG_UPDATE_CURRENT); return pendingIntent; } private PendingIntent getContentPendingIntent() { Intent intent = new Intent(AudioPlayerService.this, AudioPlayerActivity.class); Bundle bundle = new Bundle(); bundle.putInt("viewAction", ACTION_NOTIFICATION_LAYOUT); bundle.putBoolean("isFromNotification", true); intent.putExtras(bundle); PendingIntent pendingIntent = PendingIntent.getActivity(AudioPlayerService.this, 3, intent, PendingIntent.FLAG_UPDATE_CURRENT); return pendingIntent; }
/*发送通知的方法应该在音乐准备完毕和開始播放的时候调用*/ private OnPreparedListener onPreparedListener = new OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mediaPlayer.start(); notifyPrepared(); sendNotification(); } }; public void start() { if (mediaPlayer != null) { mediaPlayer.start(); } sendNotification(); } //音乐准备暂停时移除通知 public void pause() { if (mediaPlayer != null) { mediaPlayer.pause(); } stopForeground(true);//移除通知 }
歌词绘制
思路:自己定义LyricView继承TextView。覆盖onSizeChanged(),onDraw()方法。
绘制一行居中文本
/** * 绘制水平居中的歌词文本 * * @param canvas 画布 * @param text 文本 * @param y 竖直方向的y坐标 * @param isLight 是否高亮 */ private void drawCenterHorizontalText(Canvas canvas, String text, float y, boolean isLight) { paint.setColor(isLight ?
LYRCI_HIGHLIGHT_COLOR : LYRIC_DEFAULT_COLOR); paint.setTextSize(isLight ? getResources().getDimension(R.dimen.lyric_highlight_textsize) : getResources().getDimension(R.dimen.lyric_default_textsize)); float x = width / 2 - paint.measureText(text) / 2; canvas.drawText(text, x, y, paint); }
绘制多行歌词
/** * 绘制全部的歌词 * * @param canvas 画布 */ private void drawLyricList(Canvas canvas) { Lyric lightLyric = lyricList.get(lightLyricIndex); //1.首先将高亮行的歌词绘制出来,作为一个參照物 float lightLyricY = height / 2 + getTextHeight(lightLyric.getContent()) / 2; drawCenterHorizontalText(canvas, lightLyric.getContent(), lightLyricY, true); //2.遍历高亮行之前的歌词,并绘制出来 for (int pre = 0; pre < lightLyricIndex; pre++) { Lyric lyric = lyricList.get(pre); float y = lightLyricY - (lightLyricIndex - pre) * LYRIC_ROW_HEIGHT; drawCenterHorizontalText(canvas, lyric.getContent(), y, false); } //3.遍历高亮行之后的歌词,并绘制出来 for (int next = lightLyricIndex + 1; next < lyricList.size(); next++) { Lyric lyric = lyricList.get(next); float y = lightLyricY + (next - lightLyricIndex) * LYRIC_ROW_HEIGHT; drawCenterHorizontalText(canvas, lyric.getContent(), y, false); } }
/** * 获取文本的高度 * * @param text 文本 * @return 文本的高度 */ private float getTextHeight(String text) { Rect bounds = new Rect(); paint.getTextBounds(text, 0, text.length(), bounds); return bounds.height(); }
滚动歌词
/** * 滚动歌词 */ public void roll(long currentPosition,long audioDuration){ this.currentPosition = currentPosition; this.audioDuration = audioDuration; //1. 依据歌词播放的position。计算出高亮行的索引lightLyricIndex if(lyricList.size() != 0){ //1.依据当前歌曲播放的位置去计算lightLyricIndex caculateLightLyricIndex(); } //2. 拿到新的lightLyricIndex之后,更新view invalidate(); }
/** * 计算高亮歌词的索引值 * 仅仅要当前音乐的position大于当前行的startPoint。 * 而且小于下一行的startPoint,就是高亮行 */ private void caculateLightLyricIndex() { for (int i = 0; i < lyricList.size(); i++) { long startPoint = lyricList.get(i).getStartPoint(); if(i == lyricList.size() - 1){//最后一行 if(currentPosition > startPoint){ lightLyricIndex = i; } }else{//不是最后一行 Lyric next = lyricList.get(i + 1); if(currentPosition > startPoint && currentPosition < next.getStartPoint()){ lightLyricIndex = i; } } } }
平滑滚动歌词
/** * 绘制全部的歌词 * * @param canvas 画布 */ private void drawLyricList(Canvas canvas) { Lyric lightLyric = lyricList.get(lightLyricIndex); //平滑移动歌词 //1. 算出歌词的总的播放时间 即 下一行的startPoint - 当前的startPoint int totalDuration; if(lightLyricIndex==(lyricList.size()-1)){ //假设最后一行是高亮行,则拿歌曲总时间减去当前的startPoint totalDuration = (int) (audioDuration - lightLyric.getStartPoint()); }else { totalDuration = (int) (lyricList.get(lightLyricIndex+1).getStartPoint()-lightLyric.getStartPoint()); } //2. 算出当前已经播放的秒数占总时间的百分比 currentAudioPosition - startPoint float offsetPosition = (int) (currentPosition - lightLyric.getStartPoint()); float percent = offsetPosition/totalDuration; //3. 依据百分比算出应该移动的距离 percent * LYRIC_ROW_HEIGHT float dy = LYRIC_ROW_HEIGHT * percent; canvas.translate(0, -dy); //1.首先将高亮行的歌词绘制出来,作为一个參照物 float lightLyricY = height / 2 + getTextHeight(lightLyric.getContent()) / 2; drawCenterHorizontalText(canvas, lightLyric.getContent(), lightLyricY, true); //2.遍历高亮行之前的歌词。并绘制出来 for (int pre = 0; pre < lightLyricIndex; pre++) { Lyric lyric = lyricList.get(pre); float y = lightLyricY - (lightLyricIndex - pre) * LYRIC_ROW_HEIGHT; drawCenterHorizontalText(canvas, lyric.getContent(), y, false); } //3.遍历高亮行之后的歌词,并绘制出来 for (int next = lightLyricIndex + 1; next < lyricList.size(); next++) { Lyric lyric = lyricList.get(next); float y = lightLyricY + (next - lightLyricIndex) * LYRIC_ROW_HEIGHT; drawCenterHorizontalText(canvas, lyric.getContent(), y, false); } }
提供设置歌词的方法
public void setLyricList(ArrayList<Lyric> lyricList){ this.lyricList = lyricList; if(this.lyricList==null){ hasNoLyric = true; } }
歌词解析
- 读取每一行歌词文本
- 解析每一行歌词
对歌词集合进行排序
/** * 歌词解析的工具类 */ public class LyricParser { public static ArrayList<Lyric> parseLyricFromFile(File lyricFile){ if(lyricFile==null || !lyricFile.exists())return null; ArrayList<Lyric> list = new ArrayList<Lyric>(); try { //1.读取每一行歌词文本 BufferedReader reader = new BufferedReader(new InputStreamReader (new FileInputStream(lyricFile),"utf-8")); String line; while((line=reader.readLine())!=null){ //2.解析每一行歌词 //[00:04.05][00:24.05][01:24.05]北京北京 -> split("\]") //[00:04.05 [00:24.05 [01:24.05 北京北京 String[] arr = line.split("\]"); for (int i = 0; i < arr.length-1; i++) { Lyric lyric = new Lyric(); lyric.setContent(arr[arr.length-1]);//设置歌词内容 lyric.setStartPoint(formatStartPoint(arr[i])); list.add(lyric); } } //3.对歌词集合进行排序 Collections.sort(list);//从小到大 } catch (Exception e) { e.printStackTrace(); } return list; } /** * 将[00:04.05转long类型的时间 * @param str * @return */ private static long formatStartPoint(String str){ str = str.substring(1);//00:04.05 //1.先以冒号切割 String[] arr1 = str.split("~i");//00 04.05 String[] arr2 = arr1[1].split("\.");//04 05 int minute = Integer.parseInt(arr1[0]);//得到多少分钟 int second = Integer.parseInt(arr2[0]);//得到多少秒 int mills = Integer.parseInt(arr2[1]);//得到多少10毫秒 return mills*10 + second*1000 + minute*60*1000; } }
/**模拟歌词载入模块 * TODO:拿歌曲id去server请求相应的歌词文件 */ public class LyricLoader { // private static final String LYRIC_DIR = Environment.getExternalStorageDirectory()+"/MIUI/music/lyric"; private static final String LYRIC_DIR = Environment.getExternalStorageDirectory()+"/test/audio"; public static File loadLyricFileByName(String audioName){ File file = new File(LYRIC_DIR,StringUtil.formatAudioName(audioName)+".lrc"); LogUtils.i(LYRIC_DIR); if(!file.exists()){ file = new File(LYRIC_DIR,StringUtil.formatAudioName(audioName)+".txt"); } return file; } }
好了。手机影音项目的整理就到这里。