Android 音乐APP 显示专辑图片、本地数据库、自定义通知栏样式、通知栏显示
前言
近段时间,写作的时间没有那么多,之前的项目要改动一些功能,所以这个文章也是断断续续才写好,好在我还没有忘记这个事情,写文章有没有人看不重要,我更在意这个过程,记录思路和心路历程,好了,进入正题吧。
正文
在上一篇文章中,实现了播放音乐和自动下一曲,并且自定义了一个播放音乐的圆环进度条,但是播放布局那里如果一直显示一个我写上去的默认图片感觉还是不太好,用户体验不强,参考了其他的音乐播放器的播放信息栏时,我发现会根据当前歌曲,显示这个专辑的图片,于是我也打算这么做。
① 显示专辑封面图片
首先先改动一下这个进度条的绘制半径,这样看起来的效果就是它会覆盖图片的白色边框,同时我改变了这个的颜色。
就会出现这样的效果。
因为专辑图片并不是每一首歌都能有,有的时候在你的本地没有找到的,所以,就用默认的图标,找到就用专辑封面。
同时更改LocalMusicActivity中的initAnimation方法中的动画时长,之前是3s,现在改成6s,体验会更好。
在MusicUtils中新增如下方法。
/**
* 获取专辑封面
*
* @param context 上下文
* @param path 歌曲路径
* @return
*/
public static Bitmap getAlbumPicture(Context context, String path) {
//歌曲检索
MediaMetadataRetriever mmr = new MediaMetadataRetriever();
//设置数据源
mmr.setDataSource(path);
//获取图片数据
byte[] data = mmr.getEmbeddedPicture();
Bitmap albumPicture = null;
if (data != null) {
//获取bitmap对象
albumPicture = BitmapFactory.decodeByteArray(data, 0, data.length);
//获取宽高
int width = albumPicture.getWidth();
int height = albumPicture.getHeight();
// 创建操作图片用的Matrix对象
Matrix matrix = new Matrix();
// 计算缩放比例
float sx = ((float) 120 / width);
float sy = ((float) 120 / height);
// 设置缩放比例
matrix.postScale(sx, sy);
// 建立新的bitmap,其内容是对原bitmap的缩放后的图
albumPicture = Bitmap.createBitmap(albumPicture, 0, 0, width, height, matrix, false);
} else {
//从歌曲文件读取不出来专辑图片时用来代替的默认专辑图片
albumPicture = BitmapFactory.decodeResource(context.getResources(), R.mipmap.icon_music);
int width = albumPicture.getWidth();
int height = albumPicture.getHeight();
// 创建操作图片用的Matrix对象
Matrix matrix = new Matrix();
// 计算缩放比例
float sx = ((float) 120 / width);
float sy = ((float) 120 / height);
// 设置缩放比例
matrix.postScale(sx, sy);
// 建立新的bitmap,其内容是对原bitmap的缩放后的图
albumPicture = Bitmap.createBitmap(albumPicture, 0, 0, width, height, matrix, false);
}
return albumPicture;
}
这里面就是通过点击时过去到歌曲的路径,通过路径去拿图片数据,为空的话则使用默认的图片,不为空则通过BitmapFactory.decodeByteArray将图片数据流转换为Bitmap,然后设置缩放比列,然后赋值返回。
下面要去调用这个方法了。
一目了然,现在你只要运行起来就可以了,下面看一下运行效果。
这样看起来是不是效果更好呢?没骗你吧!
② 本地数据库
之前的数据来源是通过扫描本地本地本地文件夹来获取到的,虽然我给了一个缓存用于记录当前是否有缓存歌曲,但是为了后面使用的方便,还是要使用本地数据库来操作数据会比较好,都知道Android使用的本地数据库是Sqlite。当然还有现在JetPack火热的Room数据库。不过我主要还是用Sqlite,第一次这个有很多成熟的框架,不需要需繁琐的sql语句,第二JetPack是Google18年才推出,目前来说有一定的受众群体,但是还不够,深思熟虑之下还是不去使用新的Room数据库了。既然要是用成熟的框架,那么肯定会有第三方的依赖库。
在app下面的build.gradle中添加如下依赖
//Android SQLite操作框架
implementation 'org.litepal.guolindev:core:3.1.1'
然后Sync,这个框架我个人觉得挺好用的,省了我很多事情,郭神出品,必属精品。
然后在main下新建一个assets文件夹,
在文件夹下面新建一个File文件,取名litepal.xml
然后先进入到Song这个实体里面,继承 LitePalSupport,现在它就具备操作数据库表的能力了,增删改查不在话下。
然后回到litepal.xml,里面现在是空的,不过不要紧,按照框架的要求写入就可以了。如下所示,如果你的包名和我不一致记得要改呀。
<?xml version="1.0" encoding="utf-8"?>
<litepal>
<!--数据库名称-->
<dbname value="GoodMusic" />
<!--数据库版本-->
<version value="1" />
<!--映射模型-->
<list>
<mapping class="com.llw.goodmusic.bean.Song"/>
</list>
</litepal>
做了这一步就还差最后一步,那就是初始化。进入到MusicApplication,在onCreate方法中进行初始化。
那么现在你运行项目的时候,这个时候你会发现,数据库和表都已经创建好了。那么怎么证明这一点呢?很简单,你可以在MainActivity中的initData,写入如下代码:
List<Song> list = LitePal.findAll(Song.class);
BLog.d(TAG,list.size() + "");
如果打印结果是0那就说明已经创建好了这个表,只是里面目前没有数据而已,如果你报错了的话,那肯定是哪里不对造成了。
数据表已经创建好了,下面自然就要写入数据了,之前我是在LocalMusicActivity中进行数据的扫描,然后写到这个页面的列表里,那么现在我就要写到数据库里。打开Constant,增加一个全局变量
然后打开LocalMusicActivity。新增加一个成员变量
/**
* 本地音乐数据 不是缓存
*/
private boolean localMusicData = false;
下面对之前的getMusicList方法做代码改动。
/**
* 获取音乐列表
*/
private void getMusicList() {
localMusicData = SPUtils.getBoolean(Constant.LOCAL_MUSIC_DB, false, context);
//清除列表数据
mList.clear();
if (localMusicData) {
//有数据则读取本地数据库的数据
BLog.d(TAG, "读取本地数据库 ====>");
mList = LitePal.findAll(Song.class);
} else {
//没有数据则扫描本地文件夹获取音乐数据
BLog.d(TAG, "扫描本地文件夹 ====>");
mList = MusicUtils.getMusicData(this);
}
if (mList != null && mList.size() > 0) {
//显示本地音乐
showLocalMusicData();
if (!localMusicData) {
//添加到本地数据库中
addLocalDB();
}
} else {
show("兄嘚,你是一无所有啊~");
}
}
进入这个方法之后,先获取系统的缓存,本地数据库是否有数据,第一次进来当然是没有的,所以执行false中的逻辑,这个时候通过扫描本地文件夹获取数据mList = MusicUtils.getMusicData(this); ,之后就是显示数据了,这个不用管,关键在于判断当前有没有本地数据,!localMusicData就是表示 false。所以添加到本地,调用addLocalDB();。该方法如下
/**
* 添加到本地数据库
*/
private void addLocalDB() {
new Handler().post(new Runnable() {
@Override
public void run() {
for (int i = 0; i < mList.size(); i++) {
Song song = new Song();
song.setSinger(mList.get(i).getSinger());
song.setSong(mList.get(i).getSong());
song.setAlbumId(mList.get(i).getAlbumId());
song.setAlbum(mList.get(i).getAlbum());
song.setPath(mList.get(i).getPath());
song.setDuration(mList.get(i).getDuration());
song.setSize(mList.get(i).getSize());
song.setCheck(mList.get(i).isCheck());
song.save();
}
List<Song> list = LitePal.findAll(Song.class);
if (list.size() > 0) {
SPUtils.putBoolean(Constant.LOCAL_MUSIC_DB, true, context);
BLog.d(TAG, "添加到本地数据库的音乐:" + list.size() + "首");
}
}
});
这里因为添加数据是在耗时操作,所以新开一个线程去进行,然后对列表进行遍历保存。然后设置缓存值为true ,当全部遍历完成之后再查询一下,添加到数据库里面的数据有多少条。到这一步,数据就已经添加到本地数据库了。那么这个时候你再次进入到LocalMusicActivity中时,就会直接查询本地的数据库了。而在显示数据之后,也不会重复添加数据到本地数据库了。那么这就完了吗?还没有的,现在是只有一个页面知道当前数据有多少,MainActivty对此还是一无所知。在MainActivity中创建一个全局变量。
private List<Song> mList;
同时我改变了进入本地音乐这个入口的布局。
<!--本地音乐-->
<LinearLayout
android:id="@+id/lay_local_music"
android:layout_width="@dimen/dp_120"
android:layout_height="@dimen/dp_120"
android:background="@drawable/shape_app_color_radius_5"
android:foreground="?android:attr/selectableItemBackground"
android:gravity="center"
android:onClick="onClick"
android:orientation="vertical">
<ImageView
android:layout_width="@dimen/dp_40"
android:layout_height="@dimen/dp_40"
android:src="@mipmap/icon_local" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_8"
android:text="本地音乐"
android:textColor="@color/white"
android:textSize="@dimen/sp_14" />
<TextView
android:id="@+id/tv_local_music_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dp_4"
android:text="0"
android:textColor="@color/white_8"
android:textSize="@dimen/sp_12" />
</LinearLayout>
加了一个本地音乐的数量。这样用户可以更好的感知当前音乐有多少,而不用到LocalMusicActivity中查看了。布局有了,下面自然要改动UI了。回到MainActivity,
/**
* 本地音乐数量
*/
private TextView tvLocalMusicNum;
然后在initData下,绑定这个控件。
现在这个控件可以正常使用了,重写onResume方法
@Override
protected void onResume() {
super.onResume();
BLog.d(TAG, "onResume");
mList = LitePal.findAll(Song.class);
tvLocalMusicNum.setText(String.valueOf(mList.size()));
}
在这里显示歌曲的数量。然后可以运行测试一波,看看效果如何。
效果显著,下面进入下一环境,通知栏的显示。
③ 自定义通知栏样式、通知栏显示
说道后台播放,可能不了解的人觉得很难,一听头都大了,后台这两个字,一听就是要掉头发的节奏,首先不要有这样的心理,代码又不是洪荒猛兽,又不会吃了你,所以不要怕,恐惧会让你止步不前的,进而颓废。后台,相信你在了解Android的四大组件的时候就知道了,后台最多的是什么?Service,就是服务,这个东西你是看不见的,所以理解起来就没有那么容易,但是你能感觉得到。比如过放音乐,你是听到的音乐。其实放音乐对于用户来说就是禁止的,因为你看不出什么名堂,你是用听的,为了让用户知道现在正在播放音乐,就会有播放的进度条,播放的状态,这些动态效果的支撑就来源于后台的服务,比如你就拿现在这个APP来说,你现在播放一首歌,然后你点击home键回到手机桌面,音乐还是在播放的,这时候音乐就是在后台的,但是你看不见,你能听见。
说了这么多都是概念性的东西,为什么要用服务在后台播放音乐呢?音乐你播放一首歌,就是在整个APP任何地方你都要知道这个歌曲当前的状态和播放进度,针对于这个需求,用Service来实现无疑是最好的方式,没有之一。下面在com.llw.goodmusic下新建一个service包,
然后新建一个Service。命名为MusicService。
点击Finish就创建完成了。我自己创建一个MusicService类然后继承Service也是一样的呀。那么我这样创建有什么好处呢?打开AndroidManifest.xml
可以看到,自动生成了Service的配置,就不需要我们手动再去写配置了,有的时候写代码往往会忘记这一步,通过AS来创建Service就避免了这个问题,何乐而不为呢?
现在MusicService创建好了,那么这个服务要做什么事情呢?首先要播放音乐,然后就是通知栏显示,之后才是通知栏和Activity之间的通信。
在layout下创建一个notification.xml。里面的代码如下:
<?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">
<!--专辑封面图-->
<ImageView
android:id="@+id/iv_album_cover"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@mipmap/icon_notification_default" />
<LinearLayout
android:gravity="center_vertical"
android:paddingStart="@dimen/dp_12"
android:paddingEnd="@dimen/dp_6"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="@dimen/dp_64">
<!--歌曲信息-->
<LinearLayout
android:gravity="center_vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/tv_notification_song_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:focusable="true"
android:focusableInTouchMode="true"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:text="歌曲名"
android:textColor="@color/black"
android:textSize="14sp" />
<TextView
android:layout_marginStart="@dimen/dp_12"
android:id="@+id/tv_notification_singer"
android:layout_width="0dp"
android:layout_weight="1"
android:singleLine="true"
android:layout_height="wrap_content"
android:text="歌手名"
android:textSize="@dimen/sp_12" />
<ImageButton
android:id="@+id/btn_notification_close"
android:layout_width="@dimen/dp_20"
android:layout_height="@dimen/dp_20"
android:background="@color/transparent"
android:src="@drawable/close_gray" />
</LinearLayout>
<!--歌曲控制-->
<LinearLayout
android:layout_marginTop="@dimen/dp_4"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
<!--上一曲-->
<ImageButton
android:id="@+id/btn_notification_previous"
android:layout_width="@dimen/dp_30"
android:layout_height="@dimen/dp_30"
android:background="@null"
android:scaleType="fitCenter"
android:src="@drawable/previous_black" />
<!--播放/暂停-->
<ImageButton
android:id="@+id/btn_notification_play"
android:layout_width="@dimen/dp_30"
android:layout_height="@dimen/dp_30"
android:layout_marginStart="@dimen/dp_30"
android:layout_marginEnd="@dimen/dp_30"
android:background="@null"
android:scaleType="fitCenter"
android:src="@drawable/play_black" />
<!--下一曲-->
<ImageButton
android:id="@+id/btn_notification_next"
android:layout_width="@dimen/dp_30"
android:layout_height="@dimen/dp_30"
android:background="@null"
android:scaleType="fitCenter"
android:src="@drawable/next_black" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
icon_notification_default.png
close_gray.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#8a8a8a"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M18.3,5.71c-0.39,-0.39 -1.02,-0.39 -1.41,0L12,10.59 7.11,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41L10.59,12 5.7,16.89c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,13.41l4.89,4.89c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L13.41,12l4.89,-4.89c0.38,-0.38 0.38,-1.02 0,-1.4z" />
</vector>
previous_black.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:tint="#000000"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M7,6c0.55,0 1,0.45 1,1v10c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L6,7c0,-0.55 0.45,-1 1,-1zM10.66,12.82l5.77,4.07c0.66,0.47 1.58,-0.01 1.58,-0.82L18.01,7.93c0,-0.81 -0.91,-1.28 -1.58,-0.82l-5.77,4.07c-0.57,0.4 -0.57,1.24 0,1.64z" />
</vector>
play_black.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:tint="#000000"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z" />
</vector>
pause_black.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:tint="#000000"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M8,19c1.1,0 2,-0.9 2,-2L10,7c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2v10c0,1.1 0.9,2 2,2zM14,7v10c0,1.1 0.9,2 2,2s2,-0.9 2,-2L18,7c0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2z" />
</vector>
next_black.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:tint="#000000"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/white"
android:pathData="M7.58,16.89l5.77,-4.07c0.56,-0.4 0.56,-1.24 0,-1.63L7.58,7.11C6.91,6.65 6,7.12 6,7.93v8.14c0,0.81 0.91,1.28 1.58,0.82zM16,7v10c0,0.55 0.45,1 1,1s1,-0.45 1,-1V7c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1z" />
</vector>
一共六个图标,一个png格式,其余五个为xml格式。
下面进入到MusicService中。里面的代码如下:
private static final String TAG = "MusicService";
public class MusicBinder extends Binder {
public MusicService getService() {
return MusicService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
super.onBind(intent);
return new MusicBinder();
}
@Override
public void onCreate() {
super.onCreate();
BLog.d(TAG, "onCreate");
}
通过绑定的方式启动服务,回到MainActivity,创建变量
private MusicService.MusicBinder musicBinder;
private MusicService musicService;
然后创建服务连接器
/**
* 服务连接
*/
private ServiceConnection connection = new ServiceConnection() {
/**
* 连接服务
* @param name
* @param service
*/
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
musicBinder = (MusicService.MusicBinder) service;
musicService = musicBinder.getService();
BLog.d(TAG, "Service与Activity已连接");
}
//断开服务
@Override
public void onServiceDisconnected(ComponentName name) {
musicBinder = null;
}
};
在initData方法中绑定服务。
//绑定服务
Intent serviceIntent = new Intent(context, MusicService.class);
bindService(serviceIntent, connection, BIND_AUTO_CREATE);
也要在页面销毁时解绑
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(connection);
System.exit(0);
}
我在initData中加了日志的打印
BLog.d(TAG, "initData");
下面运行一下:
从日志上可以得出,在initData中绑定时,会创建一个服务连接,在连接时对服务进行创建,创建之后连接这个服务,此时服务在后台运行,下面就要在服务中显示通知栏。
private static NotificationManager manager;
/**
* 显示通知
*/
private void showNotification() {
String channelId = "play_control";
String channelName = "播放控制";
int importance = NotificationManager.IMPORTANCE_HIGH;
createNotificationChannel(channelId, channelName, importance);
RemoteViews remoteViews = new RemoteViews(this.getPackageName(), R.layout.notification);
Notification notification = new NotificationCompat.Builder(this, "play_control")
.setWhen(System.currentTimeMillis())
.setSmallIcon(R.mipmap.icon_big_logo)
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.icon_big_logo))
.setCustomContentView(remoteViews)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setAutoCancel(false)
.setOnlyAlertOnce(true)
.setOngoing(true)
.build();
//发送通知
manager.notify(1, notification);
}
/**
* 创建通知渠道
*
* @param channelId 渠道id
* @param channelName 渠道名称
* @param importance 渠道重要性
*/
@TargetApi(Build.VERSION_CODES.O)
private void createNotificationChannel(String channelId, String channelName, int importance) {
NotificationChannel channel = new NotificationChannel(channelId, channelName, importance);
channel.enableLights(false);
channel.enableVibration(false);
channel.setVibrationPattern(new long[]{0});
channel.setSound(null, null);
//获取系统通知服务
manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);
}
然后在onCreate中调用showNotification();即可。运行之后你就能看到通知了,至于通知栏的点击和通知栏按钮的点击我放到下一篇文章来进行讲解。
结语
说实话写到这里的时候除了很多的状态,音乐APP的坑太多了,太多的细节要去想了,而且写代码的过程中思路经常断,因为还有公司的工作要做,断断续续才写好。
源码地址:Good Music