import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
/**
• 基础Activity
• 
• @author llw
*/
public abstract class BasicActivity extends AppCompatActivity implements UiCallBack {
/**
• 快速点击的时间间隔
*/
private static final int FAST_CLICK_DELAY_TIME = 500;
/**
• 最后点击的时间
*/
private static long lastClickTime;
/**
• 上下文参数
*/
protected Activity context;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initBeforeView(savedInstanceState);
this.context = this;
//添加继承这个BaseActivity的Activity
BasicApplication.getActivityManager().addActivity(this);
//绑定布局id
if (getLayoutId() > 0) {
setContentView(getLayoutId());
}
//初始化数据
initData(savedInstanceState);
}
@Override
public void initBeforeView(Bundle savedInstanceState) {
}
/**
• 返回
• 
• @param toolbar
*/
protected void Back(Toolbar toolbar) {
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
context.finish();
if (!isFastClick()) {
context.finish();
}
}
});
}
/**
• 两次点击间隔不能少于500ms 防止多次点击
• 
• @return flag
*/
protected static boolean isFastClick() {
boolean flag = true;
long currentClickTime = System.currentTimeMillis();
if ((currentClickTime - lastClickTime) >= FAST_CLICK_DELAY_TIME) {
flag = false;
}
lastClickTime = currentClickTime;
return flag;
}
/**
• 消息提示
• 
• @param llw
*/
protected void show(CharSequence llw) {
Toast.makeText(context, llw, Toast.LENGTH_SHORT).show();
}
}

里面除了提供上下文参数以外,还有绑定布局的id,初始化参数等方法,还有一些需要在后面再增加。

现在都搞定了然后在com.llw.goodmusic下创建一个MusicApplication然后继承BasicApplication。代码如下:

package com.llw.goodmusic;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Handler;
import com.llw.goodmusic.basic.ActivityManager;
import com.llw.goodmusic.basic.BasicApplication;
/**
• 项目管理
• 
• @author llw
*/
public class MusicApplication extends BasicApplication {
/**
• 应用实例
*/
public static MusicApplication musicApplication;
private static Context context;
private static ActivityManager activityManager;
public static Context getMyContext() {
return musicApplication == null ? null : musicApplication.getApplicationContext();
}
private Handler myHandler;
public Handler getMyHandler() {
return myHandler;
}
public void setMyHandler(Handler handler) {
myHandler = handler;
}
@Override
public void onCreate() {
super.onCreate();
activityManager = new ActivityManager();
context = getApplicationContext();
musicApplication = this;
}
public static ActivityManager getActivityManager() {
return activityManager;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
}

还差最后一步配置,那就是在AndroidManifest.xml设置MusicApplication

android 根据音乐获取图片 安卓获取本地音乐_ide

然后再改一下styles.xml中的样式

android 根据音乐获取图片 安卓获取本地音乐_ide_02

里面colors.xml

<?xml version="1.0" encoding="utf-8"?> 
#26252B
#26252B
#D81B60
#26252B
#333439
#FFFFFF
#22FFFFFF 
 
#44FFFFFF 
 
#66FFFFFF 
 
#88FFFFFF 
 
#00000000 
 
#FF9D00

④ 页面设计

android 根据音乐获取图片 安卓获取本地音乐_android 根据音乐获取图片_03

这个图就是APP的主页面了,深色为主,下面来看这个怎么来写。图标可以去自己下载,也可以在源码中去拿,都行。下面看看activity_main.xml的布局内容

<?xml version="1.0" encoding="utf-8"?> 
<layout xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:app=“http://schemas.android.com/apk/res-auto”
xmlns:tools=“http://schemas.android.com/tools”>
<LinearLayout
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:background=“@color/app_bg”
android:orientation=“vertical”
tools:context=“.ui.MainActivity”>
<TextView
android:id=“@+id/tv_title”
android:gravity=“center”
android:layout_width=“match_parent”
android:layout_gravity=“center”
android:text=“Good Music”
android:layout_height=“?attr/actionBarSize”
android:background=“@color/app_color”
android:textColor=“@color/white”
android:textSize=“@dimen/sp_18” />
<LinearLayout
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:orientation=“vertical”
android:padding=“@dimen/dp_12”>
<LinearLayout
android:onClick=“onClick”
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:orientation=“vertical”>
<ImageView
android:layout_width=“@dimen/dp_48”
android:layout_height=“@dimen/dp_48”
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_16” />
然后当然是要在MainActivity中做处理了。
package com.llw.goodmusic.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import com.llw.goodmusic.R;
import com.llw.goodmusic.basic.BasicActivity;
/**
• 主页面
• 
• @author llw
*/
public class MainActivity extends BasicActivity {
@Override
public void initData(Bundle savedInstanceState) {
}
@Override
public int getLayoutId() {
return R.layout.activity_main;
}
public void onClick(View view) {
startActivity(new Intent(context,LocalMusicActivity.class));
}
}

里面的代码也比较的简单,继承BasicActivity。然后重写initData和getLayoutId,再绑定布局中的onclick就可以了。那么它要跳转到LocalMusicActivity。这个Activity现在还没有的,那就创建一个。创建好了之后同样继承BasicActivity,重写里面的两个方法,绑定布局之后,下面来写这个页面的布局。activity_local_music.xml代码如下:

<?xml version="1.0" encoding="utf-8"?> 
<layout xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:app=“http://schemas.android.com/apk/res-auto”
xmlns:tools=“http://schemas.android.com/tools”>
<LinearLayout
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:background=“@color/app_bg”
android:orientation=“vertical”
tools:context=“.ui.MainActivity”>
<androidx.appcompat.widget.Toolbar
android:id=“@+id/toolbar”
android:layout_width=“match_parent”
android:layout_height=“?attr/actionBarSize”
android:background=“@color/app_color”
app:navigationIcon=“@mipmap/icon_return_white”>
<TextView
android:id=“@+id/tv_title”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:layout_gravity=“center”
android:text=“本地音乐”
android:textColor=“@color/white”
android:textSize=“@dimen/sp_18” />
</androidx.appcompat.widget.Toolbar>
<RelativeLayout
android:layout_width=“match_parent”
android:layout_height=“match_parent”>
<LinearLayout
android:id=“@+id/lay_scan_music”
android:layout_width=“match_parent”
android:layout_height=“match_parent”
android:gravity=“center”
android:orientation=“vertical”>
<ImageView
android:layout_width=“@dimen/dp_140”
android:layout_height=“@dimen/dp_140”
android:src=“@mipmap/icon_empty” />
<com.google.android.material.button.MaterialButton
style=“@style/Widget.MaterialComponents.Button.UnelevatedButton”
android:layout_width=“@dimen/dp_140”
android:layout_height=“@dimen/dp_40”
android:layout_marginTop=“@dimen/dp_16”
android:insetTop=“@dimen/dp_0”
android:insetBottom=“@dimen/dp_0”
android:onClick=“scanLocalMusic”
android:text=“扫描本地音乐”
android:textSize=“@dimen/sp_14”
android:theme=“@style/Theme.MaterialComponents.Light.NoActionBar”
app:backgroundTint=“@color/transparent”
app:cornerRadius=“@dimen/dp_20”
app:strokeColor=“@color/white”
app:strokeWidth=“@dimen/dp_1” />
<androidx.recyclerview.widget.RecyclerView
android:id=“@+id/rv_music”
android:layout_width=“match_parent”
android:layout_height=“match_parent” />

里面有两个布局,一个是用来扫描本地歌曲的,一个是用来显示歌曲的列表,如果扫描不到就提示一下。现在页面的布局有了,下面就是要来写这个页面的业务逻辑。

⑤ 权限请求

之前在AndroidManifest.xml中注册了静态的文件读写权限,而在Android 6.0之后。危险权限需要动态申请才能够使用。所以我在build.gradle中增加了一个权限请求框架,现在就来使用吧。

/**
• 动态权限请求
*/
private void permissionsRequest() {
PermissionX.init(this).permissions(
//写入文件
Manifest.permission.WRITE_EXTERNAL_STORAGE)
.onExplainRequestReason(new ExplainReasonCallbackWithBeforeParam() {
@Override
public void onExplainReason(ExplainScope scope, List deniedList, boolean beforeRequest) {
scope.showRequestReasonDialog(deniedList, “即将申请的权限是程序必须依赖的权限”, “我已明白”);
}
})
.onForwardToSettings(new ForwardToSettingsCallback() {
@Override
public void onForwardToSettings(ForwardScope scope, List deniedList) {
scope.showForwardToSettingsDialog(deniedList, “您需要去应用程序设置当中手动开启权限”, “我已明白”);
}
})
.setDialogTintColor(R.color.white, R.color.app_color)
.request(new RequestCallback() {
@Override
public void onResult(boolean allGranted, List grantedList, List deniedList) {
if (allGranted) {
//通过后的业务逻辑
} else {
show(“您拒绝了如下权限:” + deniedList);
}
}
});
}

OK,权限申请就是这么简单。

⑥ 获取音乐数据

首先需要些几个工具类,方便APP后面的开发。第一个是日志,这里不用系统自带的日志。在utils包下新建一个BLog类。里面的代码如下:

package com.llw.goodmusic.utils;
import android.text.TextUtils;
import android.util.Log;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
/**
• 日志
• 
• @author llw
*/
public class BLog {
private static boolean IS_SHOW_LOG = true;
private static final String DEFAULT_MESSAGE = “execute”;
private static final String LINE_SEPARATOR = System.getProperty(“line.separator”);
private static final int JSON_INDENT = 4;
private static final int V = 0x1;
private static final int D = 0x2;
private static final int I = 0x3;
private static final int W = 0x4;
private static final int E = 0x5;
private static final int A = 0x6;
private static final int JSON = 0x7;
public static void init(boolean isShowLog) {
IS_SHOW_LOG = isShowLog;
}
public static void v() {
printLog(V, null, DEFAULT_MESSAGE);
}
public static void v(String msg) {
printLog(V, null, msg);
}
public static void v(String tag, String msg) {
printLog(V, tag, msg);
}
public static void d() {
printLog(D, null, DEFAULT_MESSAGE);
}
public static void d(String msg) {
printLog(D, null, msg);
}
public static void d(String tag, String msg) {
printLog(D, tag, msg);
}
public static void i() {
printLog(I, null, DEFAULT_MESSAGE);
}
public static void i(String msg) {
printLog(I, null, msg);
}
public static void i(String tag, String msg) {
printLog(I, tag, msg);
}
public static void w() {
printLog(W, null, DEFAULT_MESSAGE);
}
public static void w(String msg) {
printLog(W, null, msg);
}
public static void w(String tag, String msg) {
printLog(W, tag, msg);
}
public static void e() {
printLog(E, null, DEFAULT_MESSAGE);
}
public static void e(String msg) {
printLog(E, null, msg);
}
public static void e(String tag, String msg) {
printLog(E, tag, msg);
}
public static void a() {
printLog(A, null, DEFAULT_MESSAGE);
}
public static void a(String msg) {
printLog(A, null, msg);
}
public static void a(String tag, String msg) {
printLog(A, tag, msg);
}
public static void json(String jsonFormat) {
printLog(JSON, null, jsonFormat);
}
public static void json(String tag, String jsonFormat) {
printLog(JSON, tag, jsonFormat);
}
private static void printLog(int type, String tagStr, String msg) {
if (!IS_SHOW_LOG) {
return;
}
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
int index = 4;
String className = stackTrace[index].getFileName();
String methodName = stackTrace[index].getMethodName();
int lineNumber = stackTrace[index].getLineNumber();
String tag = (tagStr == null ? className : tagStr);
methodName = methodName.substring(0, 1).toUpperCase() + methodName.substring(1);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append(“[ (”).append(className).append(“:”).append(lineNumber).append(“)#”).append(methodName).append(" ] ");
if (msg != null && type != JSON) {
stringBuilder.append(msg);
}
String logStr = stringBuilder.toString();
switch (type) {
case V:
Log.v(tag, logStr);
break;
case D:
Log.d(tag, logStr);
break;
case I:
Log.i(tag, logStr);
break;
case W:
Log.w(tag, logStr);
break;
case E:
Log.e(tag, logStr);
break;
case A:
Log.wtf(tag, logStr);
break;
case JSON: {
if (TextUtils.isEmpty(msg)) {
Log.d(tag, “Empty or Null json content”);
return;
}
String message = null;
try {
if (msg.startsWith(“{”)) {
JSONObject jsonObject = new JSONObject(msg);
message = jsonObject.toString(JSON_INDENT);
} else if (msg.startsWith(“[”)) {
JSONArray jsonArray = new JSONArray(msg);
message = jsonArray.toString(JSON_INDENT);
}
} catch (JSONException e) {
e(tag, e.getCause().getMessage() + “\n” + msg);
return;
}
printLine(tag, true);
message = logStr + LINE_SEPARATOR + message;
String[] lines = message.split(LINE_SEPARATOR);
StringBuilder jsonContent = new StringBuilder();
for (String line : lines) {
jsonContent.append("║ ").append(line).append(LINE_SEPARATOR);
}
Log.d(tag, jsonContent.toString());
printLine(tag, false);
}
break;
default:
break;
}
}
private static void printLine(String tag, boolean isTop) {
if (isTop) {
Log.d(tag, “╔═══════════════════════════════════════════════════════════════════════════════════════”);
} else {
Log.d(tag, “╚═══════════════════════════════════════════════════════════════════════════════════════”);
}
}
}
为了方便使用,我再加上一个ToastUtils,代码如下:
package com.llw.goodmusic.utils;
import android.content.Context;
import android.widget.Toast;
public class ToastUtils {
/**
• 长消息
• 
• @param context 上下文参数
• @param llw 内容
*/
public static void longToast(Context context, CharSequence llw) {
Toast.makeText(context.getApplicationContext(), llw, Toast.LENGTH_LONG).show();
}
/**
• 短消息
• 
• @param context 上下文参数
• @param llw 内容
*/
public static void shortToast(Context context, CharSequence llw) {
Toast.makeText(context.getApplicationContext(), llw, Toast.LENGTH_SHORT).show();
}
}
既然是歌曲信息肯定是需要一个实体bean的。在com.llw.goodmusic下新建一个bean包。在包下新建一个Song类,代码如下:
package com.llw.goodmusic.bean;
/**
• 歌曲Bean
• 
• @author llw
*/
public class Song {
/**
• 歌手
*/
public String singer;
/**
• 歌曲名
*/
public String song;
/**
• 专辑名
*/
public String album;
/**
• 专辑图片
*/
public String album_art;
/**
• 歌曲的地址
*/
public String path;
/**
• 歌曲长度
*/
public int duration;
/**
• 歌曲的大小
*/
public long size;
/**
• 当前歌曲选中
*/
public boolean isCheck;
public String getSinger() {
return singer;
}
public void setSinger(String singer) {
this.singer = singer;
}
public String getSong() {
return song;
}
public void setSong(String song) {
this.song = song;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public int getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
public long getSize() {
return size;
}
public void setSize(long size) {
this.size = size;
}
public String getAlbum() {
return album;
}
public void setAlbum(String album) {
this.album = album;
}
public String getAlbum_art() {
return album_art;
}
public void setAlbum_art(String album_art) {
this.album_art = album_art;
}
public boolean isCheck() {
return isCheck;
}
public void setCheck(boolean check) {
isCheck = check;
}
}
然后还有一个最主要的工具类MusicUtils,代码如下:
package com.llw.goodmusic.utils;
import android.content.Context;
import android.database.Cursor;
import android.provider.MediaStore;
import com.llw.goodmusic.bean.Song;
import java.util.ArrayList;
import java.util.List;
/**
• 音乐扫描工具
• 
• @author llw
*/
public class MusicUtils {
/**
• 扫描系统里面的音频文件,返回一个list集合
*/
public static List getMusicData(Context context) {
List list = new ArrayList();
// 媒体库查询语句(写一个工具类MusicUtils)
Cursor cursor = context.getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, null, null,
null, MediaStore.Audio.Media.IS_MUSIC);
if (cursor != null) {
while (cursor.moveToNext()) {
Song song = new Song();
//歌曲名称
song.song = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE));
//歌手
song.singer = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST));
//专辑名
song.album = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM));
//歌曲路径
song.path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA));
//歌曲时长
song.duration = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION));
//歌曲大小
song.size = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE));
if (song.size > 1000 * 800) {
// 注释部分是切割标题,分离出歌曲名和歌手 (本地媒体库读取的歌曲信息不规范)
if (song.song.contains(“-”)) {
String[] str = song.song.split(“-”);
song.singer = str[0];
song.song = str[1];
}
list.add(song);
}
}
// 释放资源
cursor.close();
}
return list;
}
}

这个扫描请求的工具类是无法扫描到加密的音乐文件的,能扫描到mp3、flac格式的音乐文件,其他的格式我没有试过,因为现在网易云和QQ音乐下载本地歌曲有很多是需要VIP才能下载的,这种音乐下载之后是加密的音乐文件,QQ音乐的下载的加密文件是 .qmc后缀开头的,网易的我就不知道了,因为我没有开网易云音乐的VIP,不过这些加密文件有一个共同点,不允许其他播放器播放,这个就很恶心了,也就是说哪怕你通过文件夹路径扫描到添加到你自己的音乐播放列表里面之后也播放不了。因为加密规则你不知道,你就不能去解密,解密不了自然播放不了。

最终你的项目目录会如下图所示

android 根据音乐获取图片 安卓获取本地音乐_xml_04

如果有出入的话可以照这个来改一下,或者可以自己分包也可以。

⑦ 数据显示

做一个列表来显示本地的歌曲列表,列表由item决定,item需要新建一个xml文件,如下图这种。

android 根据音乐获取图片 安卓获取本地音乐_ide_05

在layout下面新建一个item_music_rv_list.xml,布局如下:

<?xml version="1.0" encoding="utf-8"?> 
<LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”
android:id=“@+id/item_music”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:layout_marginBottom=“@dimen/dp_1”
android:background=“@color/app_color”
android:foreground=“?android:attr/selectableItemBackground”
android:gravity=“center_vertical”
android:orientation=“horizontal”
android:padding=“@dimen/dp_10”>
<TextView
android:id=“@+id/tv_position”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:layout_margin=“2dp”
android:text=“1”
android:textColor=“@color/white”
android:textSize=“@dimen/sp_16” />
<LinearLayout
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:layout_marginLeft=“@dimen/dp_10”
android:orientation=“vertical”>
<TextView
android:id=“@+id/tv_song_name”
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:maxLines=“1”
android:text=“歌曲名”
android:textColor=“@color/white”
android:textSize=“@dimen/sp_18” />
<LinearLayout
android:layout_width=“match_parent”
android:layout_height=“wrap_content”
android:layout_marginTop=“@dimen/dp_4”>
<TextView
android:id=“@+id/tv_singer”
android:layout_width=“0dp”
android:layout_height=“wrap_content”
android:layout_weight=“1”
android:ellipsize=“end”
android:maxLines=“1”
android:text=“歌手”
android:textColor=“@color/white”
android:textSize=“@dimen/sp_14” />
<TextView
android:id=“@+id/tv_duration_time”
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”
android:layout_marginLeft=“12dp”
android:text=“时间”
android:textColor=“@color/white”
android:textSize=“@dimen/sp_14” />

里面的尺寸都是放在dimen.xml文件里面的,放在values.xml下,和colors.xml同级,这个我也贴一下代码

<?xml version="1.0" encoding="utf-8"?> 
 
0dp
0.1dp
0.5dp
1dp
1.5dp
2dp
2.5dp
3dp
3.5dp
4dp
4.5dp
5dp
6dp
7dp
8dp
9dp
10dp
11dp
12dp
13dp
14dp
15dp
16dp
17dp
18dp
19dp
20dp
21dp
22dp
23dp
24dp
25dp
26dp
27dp
28dp
29dp
30dp
31dp
32dp
33dp
34dp
35dp
36dp
37dp
38dp
39dp
40dp
41dp
42dp
43dp
44dp
45dp
46dp
47dp
48dp
49dp
50dp
51dp
52dp
53dp
54dp
55dp
56dp
57dp
58dp
59dp
60dp
61dp
62dp
63dp
64dp
65dp
66dp
67dp
68dp
69dp
70dp
71dp
72dp
73dp
74dp
75dp
76dp
77dp
78dp
79dp
80dp
81dp
82dp
83dp
84dp
85dp
86dp
87dp
88dp
89dp
90dp
91dp
92dp
93dp
94dp
95dp
96dp
97dp
98dp
99dp
100dp
101dp
102dp
103dp
104dp
105dp
106dp
107dp
108dp
109dp
110dp
111dp
112dp
113dp
114dp
115dp
116dp
117dp
118dp
119dp
120dp
121dp
122dp
123dp
124dp
125dp
126dp
127dp
128dp
129dp
130dp
131dp
132dp
133dp
134dp
135dp
136dp
137dp
138dp
139dp
140dp
141dp
142dp
143dp
144dp
145dp
146dp
147dp
148dp
149dp
150dp
151dp
152dp
153dp
154dp
155dp
156dp
157dp
158dp
159dp
160dp
161dp
162dp
163dp
164dp
165dp
166dp
167dp
168dp
169dp
170dp
171dp
172dp
173dp
174dp
175dp
176dp
177dp
178dp
179dp
180dp
181dp
182dp
183dp
184dp
185dp
186dp
187dp
188dp
189dp
190dp
191dp
192dp
193dp
194dp
195dp
196dp
197dp
198dp
199dp
200dp
201dp
202dp
203dp
204dp
205dp
206dp
207dp
208dp
209dp
210dp
211dp
212dp
213dp
214dp
215dp
216dp
217dp
218dp
219dp
220dp
221dp
222dp
223dp
224dp
225dp
226dp
227dp
228dp
229dp
230dp
231dp
232dp
233dp
234dp
235dp
236dp
237dp
238dp
239dp
240dp
241dp
242dp
243dp
244dp
245dp
246dp