Android 音乐APP(一)扫描本地音乐

简介: Android 音乐APP(一)扫描本地音乐

效果图

image.gif


音乐APP 扫描本地音乐



前言


  这个项目纯粹的就是心血来潮,打算写一个,写作的方式和天气APP类似,把博客当成开发笔记吧,感兴趣可以跟着看,OK,新建一个项目。


正文


① 新建项目


写之前我就想好名字了,就叫GoodMusic,如下图创建项目


20201009163214850.png


最低版本从6.0开始,然后点击Finish完成创建。


② 第三方依赖


打开项目的build.gradle,新增依赖库

    //新增
        maven { url "https://jitpack.io" }
        //新增
        mavenCentral()


如下图


20201009164423573.png


然后再打开app下面的build.gradle,现在android闭包中指定使用的JDK版本,和开启DataBinding的使用。


20201010092417546.png


然后再dependencies闭包中,增加如下依赖:


  //Google Material控件,以及迁移到AndroidX下一些控件的依赖
    implementation 'com.google.android.material:material:1.2.0'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
    implementation 'androidx.annotation:annotation:1.1.0'
    implementation 'androidx.legacy:legacy-support-v4:1.0.0'
    //RecyclerView最好的适配器,让你的适配器一目了然,告别代码冗余
    implementation 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.30'
    //权限请求框架
    implementation 'com.permissionx.guolindev:permissionx:1.4.0'


然后Sync同步一下。


③ 权限和基础配置


从文章的标题可以得知,扫描本地音乐就是要打开手机的文件夹,是需要权限的,不光要在AndroidManifest.xml中注册,也要在打开的时候动态申请才行,因为这个是危险权限。


打开AndroidManifest.xml,增加如下代码:


  <!-- 文件读写权限  Android6.0 以后需要动态获取  10.0之后对文件的处理更复杂了 -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_INTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_INTERNAL_STORAGE" />


当然这还没有玩,因为我想过,我的这个APP不可能只有一个Activity,会有多个,所以就需要写一个管理Activity的类。


下面在com.llw.goodmusic包下新建一个basic包,在该包下新建一个ActivityManager类,类的代码如下所示:


package com.llw.goodmusic.basic;
import android.app.Activity;
import java.util.ArrayList;
import java.util.List;
/**
 * 管理所有的Activity
 *
 * @author llw
 */
public class ActivityManager {
    /**
     * 保存所有创建的Activity
     */
    private List<Activity> allActivities = new ArrayList<>();
    /**
     * 添加Activity到管理器
     *
     * @param activity activity
     */
    public void addActivity(Activity activity) {
        if (activity != null) {
            allActivities.add(activity);
        }
    }
    /**
     * 从管理器移除Activity
     *
     * @param activity activity
     */
    public void removeActivity(Activity activity) {
        if (activity != null) {
            allActivities.remove(activity);
        }
    }
    /**
     * 关闭所有Activity
     */
    public void finishAll() {
        for (Activity activity : allActivities) {
            activity.finish();
        }
    }
    /**
     * 获取栈顶的Activity
     *
     * @return
     */
    public Activity getTaskTop() {
        return allActivities.get(allActivities.size() - 1);
    }
}


从方法的注释你就明白这个类是干嘛的了。当然光是有一个管理的还不够,还需要一个对工程进行控制的类。在basic包下在建一个BasicApplication,代码如下:

package com.llw.goodmusic.basic;
import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
/**
 * 工程管理
 *
 * @author llw
 */
public class BasicApplication extends Application {
    private static ActivityManager activityManager;
    private static BasicApplication application;
    private static Context context;
    @Override
    public void onCreate() {
        super.onCreate();
        //声明Activity管理
        activityManager = new ActivityManager();
        context = getApplicationContext();
        application = this;
    }
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
        super.onConfigurationChanged(newConfig);
    }
    public static ActivityManager getActivityManager() {
        return activityManager;
    }
    /**
     * 内容提供器
     * @return
     */
    public static Context getContext() {
        return context;
    }
    public static BasicApplication getApplication() {
        return application;
    }
}


还差最后一个就是Activity的简单封装,写一个基础的Activity,然后让所有的Activity继承这个,同时实现里面的抽象方法。


这里需要先定义一个接口,由这个基础Activity来实现,在basic下面新建一个UiCallBack接口。里面的代码如下:


package com.llw.goodmusic.basic;
import android.os.Bundle;
/**
 * UI回调接口
 *
 * @author llw
 */
public interface UiCallBack {
    /**
     * 初始化savedInstanceState
     *
     * @param savedInstanceState
     */
    void initBeforeView(Bundle savedInstanceState);
    /**
     * 初始化数据 相当于onCreate
     *
     * @param savedInstanceState
     */
    void initData(Bundle savedInstanceState);
    /**
     * 绑定布局
     *
     * @return
     */
    int getLayoutId();
}


里面的方法比较简单就是一个Activity所需要的基本的东西,初始化参数和布局。然后在basic下面新建一个抽象的BasicActivity。里面的代码如下:


package com.llw.goodmusic.basic;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.Nullable;
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


20201013144139974.png


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


2020101314430968.png


里面colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#26252B</color>
    <color name="colorPrimaryDark">#26252B</color>
    <color name="colorAccent">#D81B60</color>
    <!--App主要颜色-->
    <color name="app_color">#26252B</color>
    <!--App背景颜色-->
    <color name="app_bg">#333439</color>
    <color name="white">#FFFFFF</color>
    <color name="white_2">#22FFFFFF</color><!--白色透明度22%-->
    <color name="white_4">#44FFFFFF</color><!--白色透明度44%-->
    <color name="white_6">#66FFFFFF</color><!--白色透明度66%-->
    <color name="white_8">#88FFFFFF</color><!--白色透明度88%-->
    <color name="transparent">#00000000</color><!--透明-->
    <color name="gold_color">#FF9D00</color>
</resources>


④ 页面设计


20201014110552508.png


这个图就是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" />
            </LinearLayout>
        </LinearLayout>
    </LinearLayout>
</layout>


然后当然是要在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">
        <!--Toolbar-->
        <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" />
            </LinearLayout>
            <!--列表-->
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv_music"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </RelativeLayout>
    </LinearLayout>
</layout>


<?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">
        <!--Toolbar-->
        <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" />
            </LinearLayout>
            <!--列表-->
            <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/rv_music"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </RelativeLayout>
    </LinearLayout>
</layout>


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


⑤ 权限请求


之前在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<String> deniedList, boolean beforeRequest) {
                        scope.showRequestReasonDialog(deniedList, "即将申请的权限是程序必须依赖的权限", "我已明白");
                    }
                })
                .onForwardToSettings(new ForwardToSettingsCallback() {
                    @Override
                    public void onForwardToSettings(ForwardScope scope, List<String> deniedList) {
                        scope.showForwardToSettingsDialog(deniedList, "您需要去应用程序设置当中手动开启权限", "我已明白");
                    }
                })
                .setDialogTintColor(R.color.white, R.color.app_color)
                .request(new RequestCallback() {
                    @Override
                    public void onResult(boolean allGranted, List<String> grantedList, List<String> 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<Song> getMusicData(Context context) {
        List<Song> list = new ArrayList<Song>();
        // 媒体库查询语句(写一个工具类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,不过这些加密文件有一个共同点,不允许其他播放器播放,这个就很恶心了,也就是说哪怕你通过文件夹路径扫描到添加到你自己的音乐播放列表里面之后也播放不了。因为加密规则你不知道,你就不能去解密,解密不了自然播放不了。


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


20201014141534286.png


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


⑦ 数据显示


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


20201014142256925.png


在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" />
        </LinearLayout>
    </LinearLayout>
</LinearLayout>


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


<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--尺寸-->
    <dimen name="dp_0">0dp</dimen>
    <dimen name="dp_0_1">0.1dp</dimen>
    <dimen name="dp_0_5">0.5dp</dimen>
    <dimen name="dp_1">1dp</dimen>
    <dimen name="dp_1_5">1.5dp</dimen>
    <dimen name="dp_2">2dp</dimen>
    <dimen name="dp_2_5">2.5dp</dimen>
    <dimen name="dp_3">3dp</dimen>
    <dimen name="dp_3_5">3.5dp</dimen>
    <dimen name="dp_4">4dp</dimen>
    <dimen name="dp_4_5">4.5dp</dimen>
    <dimen name="dp_5">5dp</dimen>
    <dimen name="dp_6">6dp</dimen>
    <dimen name="dp_7">7dp</dimen>
    <dimen name="dp_8">8dp</dimen>
    <dimen name="dp_9">9dp</dimen>
    <dimen name="dp_10">10dp</dimen>
    <dimen name="dp_11">11dp</dimen>
    <dimen name="dp_12">12dp</dimen>
    <dimen name="dp_13">13dp</dimen>
    <dimen name="dp_14">14dp</dimen>
    <dimen name="dp_15">15dp</dimen>
    <dimen name="dp_16">16dp</dimen>
    <dimen name="dp_17">17dp</dimen>
    <dimen name="dp_18">18dp</dimen>
    <dimen name="dp_19">19dp</dimen>
    <dimen name="dp_20">20dp</dimen>
    <dimen name="dp_21">21dp</dimen>
    <dimen name="dp_22">22dp</dimen>
    <dimen name="dp_23">23dp</dimen>
    <dimen name="dp_24">24dp</dimen>
    <dimen name="dp_25">25dp</dimen>
    <dimen name="dp_26">26dp</dimen>
    <dimen name="dp_27">27dp</dimen>
    <dimen name="dp_28">28dp</dimen>
    <dimen name="dp_29">29dp</dimen>
    <dimen name="dp_30">30dp</dimen>
    <dimen name="dp_31">31dp</dimen>
    <dimen name="dp_32">32dp</dimen>
    <dimen name="dp_33">33dp</dimen>
    <dimen name="dp_34">34dp</dimen>
    <dimen name="dp_35">35dp</dimen>
    <dimen name="dp_36">36dp</dimen>
    <dimen name="dp_37">37dp</dimen>
    <dimen name="dp_38">38dp</dimen>
    <dimen name="dp_39">39dp</dimen>
    <dimen name="dp_40">40dp</dimen>
    <dimen name="dp_41">41dp</dimen>
    <dimen name="dp_42">42dp</dimen>
    <dimen name="dp_43">43dp</dimen>
    <dimen name="dp_44">44dp</dimen>
    <dimen name="dp_45">45dp</dimen>
    <dimen name="dp_46">46dp</dimen>
    <dimen name="dp_47">47dp</dimen>
    <dimen name="dp_48">48dp</dimen>
    <dimen name="dp_49">49dp</dimen>
    <dimen name="dp_50">50dp</dimen>
    <dimen name="dp_51">51dp</dimen>
    <dimen name="dp_52">52dp</dimen>
    <dimen name="dp_53">53dp</dimen>
    <dimen name="dp_54">54dp</dimen>
    <dimen name="dp_55">55dp</dimen>
    <dimen name="dp_56">56dp</dimen>
    <dimen name="dp_57">57dp</dimen>
    <dimen name="dp_58">58dp</dimen>
    <dimen name="dp_59">59dp</dimen>
    <dimen name="dp_60">60dp</dimen>
    <dimen name="dp_61">61dp</dimen>
    <dimen name="dp_62">62dp</dimen>
    <dimen name="dp_63">63dp</dimen>
    <dimen name="dp_64">64dp</dimen>
    <dimen name="dp_65">65dp</dimen>
    <dimen name="dp_66">66dp</dimen>
    <dimen name="dp_67">67dp</dimen>
    <dimen name="dp_68">68dp</dimen>
    <dimen name="dp_69">69dp</dimen>
    <dimen name="dp_70">70dp</dimen>
    <dimen name="dp_71">71dp</dimen>
    <dimen name="dp_72">72dp</dimen>
    <dimen name="dp_73">73dp</dimen>
    <dimen name="dp_74">74dp</dimen>
    <dimen name="dp_75">75dp</dimen>
    <dimen name="dp_76">76dp</dimen>
    <dimen name="dp_77">77dp</dimen>
    <dimen name="dp_78">78dp</dimen>
    <dimen name="dp_79">79dp</dimen>
    <dimen name="dp_80">80dp</dimen>
    <dimen name="dp_81">81dp</dimen>
    <dimen name="dp_82">82dp</dimen>
    <dimen name="dp_83">83dp</dimen>
    <dimen name="dp_84">84dp</dimen>
    <dimen name="dp_85">85dp</dimen>
    <dimen name="dp_86">86dp</dimen>
    <dimen name="dp_87">87dp</dimen>
    <dimen name="dp_88">88dp</dimen>
    <dimen name="dp_89">89dp</dimen>
    <dimen name="dp_90">90dp</dimen>
    <dimen name="dp_91">91dp</dimen>
    <dimen name="dp_92">92dp</dimen>
    <dimen name="dp_93">93dp</dimen>
    <dimen name="dp_94">94dp</dimen>
    <dimen name="dp_95">95dp</dimen>
    <dimen name="dp_96">96dp</dimen>
    <dimen name="dp_97">97dp</dimen>
    <dimen name="dp_98">98dp</dimen>
    <dimen name="dp_99">99dp</dimen>
    <dimen name="dp_100">100dp</dimen>
    <dimen name="dp_101">101dp</dimen>
    <dimen name="dp_102">102dp</dimen>
    <dimen name="dp_103">103dp</dimen>
    <dimen name="dp_104">104dp</dimen>
    <dimen name="dp_105">105dp</dimen>
    <dimen name="dp_106">106dp</dimen>
    <dimen name="dp_107">107dp</dimen>
    <dimen name="dp_108">108dp</dimen>
    <dimen name="dp_109">109dp</dimen>
    <dimen name="dp_110">110dp</dimen>
    <dimen name="dp_111">111dp</dimen>
    <dimen name="dp_112">112dp</dimen>
    <dimen name="dp_113">113dp</dimen>
    <dimen name="dp_114">114dp</dimen>
    <dimen name="dp_115">115dp</dimen>
    <dimen name="dp_116">116dp</dimen>
    <dimen name="dp_117">117dp</dimen>
    <dimen name="dp_118">118dp</dimen>
    <dimen name="dp_119">119dp</dimen>
    <dimen name="dp_120">120dp</dimen>
    <dimen name="dp_121">121dp</dimen>
    <dimen name="dp_122">122dp</dimen>
    <dimen name="dp_123">123dp</dimen>
    <dimen name="dp_124">124dp</dimen>
    <dimen name="dp_125">125dp</dimen>
    <dimen name="dp_126">126dp</dimen>
    <dimen name="dp_127">127dp</dimen>
    <dimen name="dp_128">128dp</dimen>
    <dimen name="dp_129">129dp</dimen>
    <dimen name="dp_130">130dp</dimen>
    <dimen name="dp_131">131dp</dimen>
    <dimen name="dp_132">132dp</dimen>
    <dimen name="dp_133">133dp</dimen>
    <dimen name="dp_134">134dp</dimen>
    <dimen name="dp_135">135dp</dimen>
    <dimen name="dp_136">136dp</dimen>
    <dimen name="dp_137">137dp</dimen>
    <dimen name="dp_138">138dp</dimen>
    <dimen name="dp_139">139dp</dimen>
    <dimen name="dp_140">140dp</dimen>
    <dimen name="dp_141">141dp</dimen>
    <dimen name="dp_142">142dp</dimen>
    <dimen name="dp_143">143dp</dimen>
    <dimen name="dp_144">144dp</dimen>
    <dimen name="dp_145">145dp</dimen>
    <dimen name="dp_146">146dp</dimen>
    <dimen name="dp_147">147dp</dimen>
    <dimen name="dp_148">148dp</dimen>
    <dimen name="dp_149">149dp</dimen>
    <dimen name="dp_150">150dp</dimen>
    <dimen name="dp_151">151dp</dimen>
    <dimen name="dp_152">152dp</dimen>
    <dimen name="dp_153">153dp</dimen>
    <dimen name="dp_154">154dp</dimen>
    <dimen name="dp_155">155dp</dimen>
    <dimen name="dp_156">156dp</dimen>
    <dimen name="dp_157">157dp</dimen>
    <dimen name="dp_158">158dp</dimen>
    <dimen name="dp_159">159dp</dimen>
    <dimen name="dp_160">160dp</dimen>
    <dimen name="dp_161">161dp</dimen>
    <dimen name="dp_162">162dp</dimen>
    <dimen name="dp_163">163dp</dimen>
    <dimen name="dp_164">164dp</dimen>
    <dimen name="dp_165">165dp</dimen>
    <dimen name="dp_166">166dp</dimen>
    <dimen name="dp_167">167dp</dimen>
    <dimen name="dp_168">168dp</dimen>
    <dimen name="dp_169">169dp</dimen>
    <dimen name="dp_170">170dp</dimen>
    <dimen name="dp_171">171dp</dimen>
    <dimen name="dp_172">172dp</dimen>
    <dimen name="dp_173">173dp</dimen>
    <dimen name="dp_174">174dp</dimen>
    <dimen name="dp_175">175dp</dimen>
    <dimen name="dp_176">176dp</dimen>
    <dimen name="dp_177">177dp</dimen>
    <dimen name="dp_178">178dp</dimen>
    <dimen name="dp_179">179dp</dimen>
    <dimen name="dp_180">180dp</dimen>
    <dimen name="dp_181">181dp</dimen>
    <dimen name="dp_182">182dp</dimen>
    <dimen name="dp_183">183dp</dimen>
    <dimen name="dp_184">184dp</dimen>
    <dimen name="dp_185">185dp</dimen>
    <dimen name="dp_186">186dp</dimen>
    <dimen name="dp_187">187dp</dimen>
    <dimen name="dp_188">188dp</dimen>
    <dimen name="dp_189">189dp</dimen>
    <dimen name="dp_190">190dp</dimen>
    <dimen name="dp_191">191dp</dimen>
    <dimen name="dp_192">192dp</dimen>
    <dimen name="dp_193">193dp</dimen>
    <dimen name="dp_194">194dp</dimen>
    <dimen name="dp_195">195dp</dimen>
    <dimen name="dp_196">196dp</dimen>
    <dimen name="dp_197">197dp</dimen>
    <dimen name="dp_198">198dp</dimen>
    <dimen name="dp_199">199dp</dimen>
    <dimen name="dp_200">200dp</dimen>
    <dimen name="dp_201">201dp</dimen>
    <dimen name="dp_202">202dp</dimen>
    <dimen name="dp_203">203dp</dimen>
    <dimen name="dp_204">204dp</dimen>
    <dimen name="dp_205">205dp</dimen>
    <dimen name="dp_206">206dp</dimen>
    <dimen name="dp_207">207dp</dimen>
    <dimen name="dp_208">208dp</dimen>
    <dimen name="dp_209">209dp</dimen>
    <dimen name="dp_210">210dp</dimen>
    <dimen name="dp_211">211dp</dimen>
    <dimen name="dp_212">212dp</dimen>
    <dimen name="dp_213">213dp</dimen>
    <dimen name="dp_214">214dp</dimen>
    <dimen name="dp_215">215dp</dimen>
    <dimen name="dp_216">216dp</dimen>
    <dimen name="dp_217">217dp</dimen>
    <dimen name="dp_218">218dp</dimen>
    <dimen name="dp_219">219dp</dimen>
    <dimen name="dp_220">220dp</dimen>
    <dimen name="dp_221">221dp</dimen>
    <dimen name="dp_222">222dp</dimen>
    <dimen name="dp_223">223dp</dimen>
    <dimen name="dp_224">224dp</dimen>
    <dimen name="dp_225">225dp</dimen>
    <dimen name="dp_226">226dp</dimen>
    <dimen name="dp_227">227dp</dimen>
    <dimen name="dp_228">228dp</dimen>
    <dimen name="dp_229">229dp</dimen>
    <dimen name="dp_230">230dp</dimen>
    <dimen name="dp_231">231dp</dimen>
    <dimen name="dp_232">232dp</dimen>
    <dimen name="dp_233">233dp</dimen>
    <dimen name="dp_234">234dp</dimen>
    <dimen name="dp_235">235dp</dimen>
    <dimen name="dp_236">236dp</dimen>
    <dimen name="dp_237">237dp</dimen>
    <dimen name="dp_238">238dp</dimen>
    <dimen name="dp_239">239dp</dimen>
    <dimen name="dp_240">240dp</dimen>
    <dimen name="dp_241">241dp</dimen>
    <dimen name="dp_242">242dp</dimen>
    <dimen name="dp_243">243dp</dimen>
    <dimen name="dp_244">244dp</dimen>
    <dimen name="dp_245">245dp</dimen>
    <dimen name="dp_246">246dp</dimen>
    <dimen name="dp_247">247dp</dimen>
    <dimen name="dp_248">248dp</dimen>
    <dimen name="dp_249">249dp</dimen>
    <dimen name="dp_250">250dp</dimen>
    <dimen name="dp_251">251dp</dimen>
    <dimen name="dp_252">252dp</dimen>
    <dimen name="dp_253">253dp</dimen>
    <dimen name="dp_254">254dp</dimen>
    <dimen name="dp_255">255dp</dimen>
    <dimen name="dp_256">256dp</dimen>
    <dimen name="dp_257">257dp</dimen>
    <dimen name="dp_258">258dp</dimen>
    <dimen name="dp_259">259dp</dimen>
    <dimen name="dp_260">260dp</dimen>
    <dimen name="dp_261">261dp</dimen>
    <dimen name="dp_262">262dp</dimen>
    <dimen name="dp_263">263dp</dimen>
    <dimen name="dp_264">264dp</dimen>
    <dimen name="dp_265">265dp</dimen>
    <dimen name="dp_266">266dp</dimen>
    <dimen name="dp_267">267dp</dimen>
    <dimen name="dp_268">268dp</dimen>
    <dimen name="dp_269">269dp</dimen>
    <dimen name="dp_270">270dp</dimen>
    <dimen name="dp_271">271dp</dimen>
    <dimen name="dp_272">272dp</dimen>
    <dimen name="dp_273">273dp</dimen>
    <dimen name="dp_274">274dp</dimen>
    <dimen name="dp_275">275dp</dimen>
    <dimen name="dp_276">276dp</dimen>
    <dimen name="dp_277">277dp</dimen>
    <dimen name="dp_278">278dp</dimen>
    <dimen name="dp_279">279dp</dimen>
    <dimen name="dp_280">280dp</dimen>
    <dimen name="dp_281">281dp</dimen>
    <dimen name="dp_282">282dp</dimen>
    <dimen name="dp_283">283dp</dimen>
    <dimen name="dp_284">284dp</dimen>
    <dimen name="dp_285">285dp</dimen>
    <dimen name="dp_286">286dp</dimen>
    <dimen name="dp_287">287dp</dimen>
    <dimen name="dp_288">288dp</dimen>
    <dimen name="dp_289">289dp</dimen>
    <dimen name="dp_290">290dp</dimen>
    <dimen name="dp_291">291dp</dimen>
    <dimen name="dp_292">292dp</dimen>
    <dimen name="dp_293">293dp</dimen>
    <dimen name="dp_294">294dp</dimen>
    <dimen name="dp_295">295dp</dimen>
    <dimen name="dp_296">296dp</dimen>
    <dimen name="dp_297">297dp</dimen>
    <dimen name="dp_298">298dp</dimen>
    <dimen name="dp_299">299dp</dimen>
    <dimen name="dp_300">300dp</dimen>
    <dimen name="dp_301">301dp</dimen>
    <dimen name="dp_302">302dp</dimen>
    <dimen name="dp_303">303dp</dimen>
    <dimen name="dp_304">304dp</dimen>
    <dimen name="dp_305">305dp</dimen>
    <dimen name="dp_306">306dp</dimen>
    <dimen name="dp_307">307dp</dimen>
    <dimen name="dp_308">308dp</dimen>
    <dimen name="dp_309">309dp</dimen>
    <dimen name="dp_310">310dp</dimen>
    <dimen name="dp_311">311dp</dimen>
    <dimen name="dp_312">312dp</dimen>
    <dimen name="dp_313">313dp</dimen>
    <dimen name="dp_314">314dp</dimen>
    <dimen name="dp_315">315dp</dimen>
    <dimen name="dp_316">316dp</dimen>
    <dimen name="dp_317">317dp</dimen>
    <dimen name="dp_318">318dp</dimen>
    <dimen name="dp_319">319dp</dimen>
    <dimen name="dp_320">320dp</dimen>
    <dimen name="dp_321">321dp</dimen>
    <dimen name="dp_322">322dp</dimen>
    <dimen name="dp_323">323dp</dimen>
    <dimen name="dp_324">324dp</dimen>
    <dimen name="dp_325">325dp</dimen>
    <dimen name="dp_326">326dp</dimen>
    <dimen name="dp_327">327dp</dimen>
    <dimen name="dp_328">328dp</dimen>
    <dimen name="dp_329">329dp</dimen>
    <dimen name="dp_330">330dp</dimen>
    <dimen name="dp_331">331dp</dimen>
    <dimen name="dp_332">332dp</dimen>
    <dimen name="dp_333">333dp</dimen>
    <dimen name="dp_334">334dp</dimen>
    <dimen name="dp_335">335dp</dimen>
    <dimen name="dp_336">336dp</dimen>
    <dimen name="dp_337">337dp</dimen>
    <dimen name="dp_338">338dp</dimen>
    <dimen name="dp_339">339dp</dimen>
    <dimen name="dp_340">340dp</dimen>
    <dimen name="dp_341">341dp</dimen>
    <dimen name="dp_342">342dp</dimen>
    <dimen name="dp_343">343dp</dimen>
    <dimen name="dp_344">344dp</dimen>
    <dimen name="dp_345">345dp</dimen>
    <dimen name="dp_346">346dp</dimen>
    <dimen name="dp_347">347dp</dimen>
    <dimen name="dp_348">348dp</dimen>
    <dimen name="dp_349">349dp</dimen>
    <dimen name="dp_350">350dp</dimen>
    <dimen name="dp_351">351dp</dimen>
    <dimen name="dp_352">352dp</dimen>
    <dimen name="dp_353">353dp</dimen>
    <dimen name="dp_354">354dp</dimen>
    <dimen name="dp_355">355dp</dimen>
    <dimen name="dp_356">356dp</dimen>
    <dimen name="dp_357">357dp</dimen>
    <dimen name="dp_358">358dp</dimen>
    <dimen name="dp_359">359dp</dimen>
    <dimen name="dp_360">360dp</dimen>
    <dimen name="dp_365">365dp</dimen>
    <dimen name="dp_370">370dp</dimen>
    <dimen name="dp_400">400dp</dimen>
    <dimen name="dp_410">410dp</dimen>
    <dimen name="dp_422">422dp</dimen>
    <dimen name="dp_472">472dp</dimen>
    <dimen name="dp_500">500dp</dimen>
    <dimen name="dp_600">600dp</dimen>
    <dimen name="dp_640">640dp</dimen>
    <dimen name="dp_720">720dp</dimen>
    <!--字体-->
    <dimen name="sp_6">6sp</dimen>
    <dimen name="sp_7">7sp</dimen>
    <dimen name="sp_8">8sp</dimen>
    <dimen name="sp_9">9sp</dimen>
    <dimen name="sp_10">10sp</dimen>
    <dimen name="sp_11">11sp</dimen>
    <dimen name="sp_12">12sp</dimen>
    <dimen name="sp_13">13sp</dimen>
    <dimen name="sp_14">14sp</dimen>
    <dimen name="sp_15">15sp</dimen>
    <dimen name="sp_16">16sp</dimen>
    <dimen name="sp_17">17sp</dimen>
    <dimen name="sp_18">18sp</dimen>
    <dimen name="sp_19">19sp</dimen>
    <dimen name="sp_20">20sp</dimen>
    <dimen name="sp_21">21sp</dimen>
    <dimen name="sp_22">22sp</dimen>
    <dimen name="sp_23">23sp</dimen>
    <dimen name="sp_24">24sp</dimen>
    <dimen name="sp_25">25sp</dimen>
    <dimen name="sp_26">26sp</dimen>
    <dimen name="sp_27">27sp</dimen>
    <dimen name="sp_28">28sp</dimen>
    <dimen name="sp_29">29sp</dimen>
    <dimen name="sp_30">30sp</dimen>
    <dimen name="sp_31">31sp</dimen>
    <dimen name="sp_32">32sp</dimen>
    <dimen name="sp_33">33sp</dimen>
    <dimen name="sp_34">34sp</dimen>
    <dimen name="sp_35">35sp</dimen>
    <dimen name="sp_36">36sp</dimen>
    <dimen name="sp_37">37sp</dimen>
    <dimen name="sp_38">38sp</dimen>
    <dimen name="sp_40">40sp</dimen>
    <dimen name="sp_42">42sp</dimen>
    <dimen name="sp_48">48sp</dimen>
</resources>


item布局有了,还要一个适配器,在com.llw.goodmusic包下新建一个adapter包,然后新建一个MusicListAdapter类。里面的代码如下:


package com.llw.goodmusic.adapter;
import androidx.annotation.Nullable;
import com.chad.library.adapter.base.BaseQuickAdapter;
import com.chad.library.adapter.base.BaseViewHolder;
import com.llw.goodmusic.R;
import com.llw.goodmusic.bean.Song;
import com.llw.goodmusic.utils.DateTimeUtils;
import java.util.List;
/**
 * 音乐列表适配器
 *
 * @author llw
 */
public class MusicListAdapter extends BaseQuickAdapter<Song, BaseViewHolder> {
    public MusicListAdapter(int layoutResId, @Nullable List<Song> data) {
        super(layoutResId, data);
    }
    @Override
    protected void convert(BaseViewHolder helper, Song item) {
        //给控件赋值
        int duration = item.duration;
        String time = DateTimeUtils.formatTime(duration);
        //歌曲名称
        helper.setText(R.id.tv_song_name, item.getSong().trim())
                //歌手 - 专辑
                .setText(R.id.tv_singer, item.getSinger() + " - " + item.getAlbum())
                //歌曲时间
                .setText(R.id.tv_duration_time, time)
                //歌曲序号,因为getAdapterPosition得到的位置是从0开始,故而加1,
                //是因为位置和1都是整数类型,直接赋值给TextView会报错,故而拼接了""
                .setText(R.id.tv_position, helper.getAdapterPosition() + 1 + "");
        //给item添加点击事件,点击之后传递数据到播放页面或者在本页面进行音乐播放
        helper.addOnClickListener(R.id.item_music);
        //点击后改变文字颜色
        if (item.isCheck()) {
            helper.setTextColor(R.id.tv_position, mContext.getColor(R.color.gold_color))
                    .setTextColor(R.id.tv_song_name, mContext.getColor(R.color.gold_color))
                    .setTextColor(R.id.tv_singer, mContext.getColor(R.color.gold_color))
                    .setTextColor(R.id.tv_duration_time, mContext.getColor(R.color.gold_color));
        } else {
            helper.setTextColor(R.id.tv_position, mContext.getColor(R.color.white))
                    .setTextColor(R.id.tv_song_name, mContext.getColor(R.color.white))
                    .setTextColor(R.id.tv_singer, mContext.getColor(R.color.white))
                    .setTextColor(R.id.tv_duration_time, mContext.getColor(R.color.white));
        }
    }
    /**
     * 刷新数据
     */
    public void changeState() {
        notifyDataSetChanged();
    }
}


上面的代码,除了基本的数据填充之外也没有什么好说,只有一个点击歌曲时更改文字颜色,类似与一般的音乐APP歌曲的效果。当在Activity点击item时,调用changeState方法刷新数据。


好了,一切的准备工作都做完了。看起来这个功能好像没啥东西,但是要想的细节是很多的。下面回到LocalMusicActivity。


定义一些需要的变量


  private Toolbar toolbar;
    /**
     * 歌曲列表
     */
    private RecyclerView rvMusic;
    /**
     * 扫描歌曲布局
     */
    private LinearLayout layScanMusic;
    /**
     * 歌曲适配器
     */
    private MusicListAdapter mAdapter;
    /**
     * 歌曲列表
     */
    private List<Song> mList = new ArrayList<>();
    /**
     * 上一次点击的位置
     */
    private int oldPosition = -1;
  /**
     * 初始化控件级页面的业务逻辑
     */
    private void initView() {
        ActivityLocalMusicBinding binding = DataBindingUtil.setContentView(context, R.layout.activity_local_music);
        toolbar = binding.toolbar;
        rvMusic = binding.rvMusic;
        layScanMusic = binding.layScanMusic;
        Back(toolbar);
        //当进入页面时发现有缓存数据时,则隐藏扫描布局,直接获取本地数据。
        if (SPUtils.getBoolean(Constant.LOCAL_MUSIC_DATA, false, context)) {
            //省去一个点击扫描的步骤
            layScanMusic.setVisibility(View.GONE);
            permissionsRequest();
        }
    }



新建一个方法,在权限通过时调用


20201014143819291.png


方法如下:


  /**
     * 获取音乐列表
     */
    private void getMusicList() {
        //清除列表数据
        mList.clear();
        //将扫描到的音乐赋值给音乐列表
        mList = MusicUtils.getMusicData(this);
        if (mList != null && mList.size() > 0) {
            //是否有缓存歌曲
            SPUtils.putBoolean(Constant.LOCAL_MUSIC_DATA, true, context);
            layScanMusic.setVisibility(View.GONE);
            //显示本地音乐
            showLocalMusicData();
        } else {
            show("兄嘚,你是一无所有啊~");
        }
    }


然后再看showLocalMusicData方法:


  /**
     * 显示本地音乐数据
     */
    private void showLocalMusicData() {
        //指定适配器的布局和数据源
        mAdapter = new MusicListAdapter(R.layout.item_music_rv_list, mList);
        //线性布局管理器,可以设置横向还是纵向,RecyclerView默认是纵向的,所以不用处理,如果不需要设置方向,代码还可以更加的精简如下
        rvMusic.setLayoutManager(new LinearLayoutManager(this));
        //设置适配器
        rvMusic.setAdapter(mAdapter);
        //item的点击事件
        mAdapter.setOnItemChildClickListener(new BaseQuickAdapter.OnItemChildClickListener() {
            @Override
            public void onItemChildClick(BaseQuickAdapter adapter, View view, int position) {
                if (view.getId() == R.id.item_music) {
                    if (oldPosition == -1) {
                        //未点击过 第一次点击
                        oldPosition = position;
                        mList.get(position).setCheck(true);
                    } else {
                        //大于 1次
                        if (oldPosition != position) {
                            mList.get(oldPosition).setCheck(false);
                            mList.get(position).setCheck(true);
                            //重新设置位置,当下一次点击时position又会和oldPosition不一样
                            oldPosition = position;
                        }
                    }
                    mAdapter.changeState();
                }
            }
        });
    }


这里通过点击的位置进行控制歌曲的状态。OK,代码就写完了。这里我再说一下业务逻辑,当第一次点击扫描按钮时,会请求文件读写取权限,我只放了写入的权限,因为文件操作的权限是在一个权限组里,通过一个就是通过一组,拿到权限之后获取工具类扫描到的歌曲数据,有数据则显示,而对于点击位置的控制,则是通过一个全局变量来操作,oldPosition初始值为-1,当第一次点击时,加入点击了第三项,那么oldPosition就是为2。这个时候将position对应的数据的check设置为true。然后调用mAdapter.changeState();刷新数据。然后可以点击第三项,这个时候oldPosition是不等于position的。


              mList.get(oldPosition).setCheck(false);
                            mList.get(position).setCheck(true);
                            //重新设置位置,当下一次点击时position又会和oldPosition不一样
                            oldPosition = position;


所以把oldPosition对应数据取消选中,而把position对应数据进行选中,之后把 oldPosition = position;最后退出判断之后又会刷新数据。


下面来运行一下看一下效果。20201014152228812.gif


结语


  目前才刚开始写,这一篇写音乐扫描和获取,饭要一口一口吃,功能要一个一个来写,感兴趣的也可以看一下。有哪里不明白的评论告诉我,我会及时回复,谢谢你的阅读,做一个有内容的作者。


相关文章
|
4月前
|
物联网 Java API
Harmony Ble 蓝牙App (一)扫描(上)
Harmony Ble 蓝牙App (一)扫描(上)
170 0
|
3月前
|
Android开发 开发者 iOS开发
APP开发后如何上架,上架Android应用市场前要准备什么
移动应用程序(APP)的开发已经成为现代企业和开发者的常见实践。然而,开发一个成功的APP只是第一步,将其上架到应用商店让用户下载和使用是实现其潜力的关键一步。
|
1月前
|
设计模式 测试技术 数据库
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
|
2月前
|
安全 Java 数据挖掘
当 App 有了系统权限,真的可以为所欲为? Android Performance Systrace
当 App 有了系统权限,真的可以为所欲为? Android Performance Systrace 转载自: https://androidperformance.com/2023/05/14/bad-android-app-with-system-permissions/#/0-Dex-%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF
30 0
|
3月前
|
Android开发
闲暇时间收集和整理的Android的一些常用的App
闲暇时间收集和整理的Android的一些常用的App
14 0
|
3月前
|
Android开发 UED 开发者
解释Android App Bundle是什么,它的优势是什么?
解释Android App Bundle是什么,它的优势是什么?
55 0
|
3月前
|
JavaScript Android开发
Cordova 后台运行 Android APP
Cordova 后台运行 Android APP
|
4月前
|
物联网 Android开发
Harmony Ble 蓝牙App (一)扫描(下)
Harmony Ble 蓝牙App (一)扫描(下)
|
4月前
|
物联网 Android开发
Android Ble蓝牙App(七)扫描过滤
Android Ble蓝牙App(七)扫描过滤