前言:我听歌比较喜欢播放本地音乐,但是那些本地播放APP总会有些这样或那样让我不顺的问题,比如中文/日文识别为乱码,比如换一个文件夹它就不知道我上一次在这文件夹里播的啥音乐,再比如无法按文件名排序。

最近公司项目做APP,学了react native。于是想用react native把本地音乐播放器给造一个出来了,本来一天就能搞定的事,前前后后磨了快一星期,终于搞定了。

1.由于我之前用荣耀10做测试,后来换成小米13,直接把react-native-document-picker的多选给搞废了,组件不兼容多选必须得用安卓原生来,然后开始研究原生代码该怎么写……

2.react-native-track-player没有是否已经初始化的判定,导致我修改完代码每次保存后,都还再次运行useEffect提示说已经注册了插件,最后还好用useRef的current是否存在来解决。

3.小米底部安全区域遮挡问题还困扰了我一下,最后在MainActivity.java重写onCreate + styles.xml增加navigationBarColor配置下搞定。

4.最大的障碍!!!react-native-track-player究竟能播放哪种路径的文件?

由于我用的react-native-document-picker选择文件,它默认返回的【content://com.android.externalstorage.documents/document/primary%3AMusic%2FEnglish%2FXXX.mp3】是不能直接播放的。

然后我找到了它的API文档,有个copyTo的配置,然后返回的【file:///data/user/0/com.caicemusic/files/d99163f2-aa31-45d3-a4a7-947ecf13d18e/XXX.mp3】是可以播的,但是!这存在两个问题,一是我选择的音乐至少都上百首,全部copy后,APP的存储体积飙升几个G。二是,我的小米13由于上述的1.无法多选导致这路被封死了。

所以我的研究方向就变成了,怎么把content:\\文件转化为file:\\,就是这问题卡了我一周!

什么react-native-fsrn-fetch-blob等被我装装卸卸了好多次,后来用原生java转,在DocumentFile.fromTreeUri(getReactApplicationContext(), uri)拿到文件后,通过file.getUri().toString()确实也拿到了缓存路径可以播,但【content://com.android.externalstorage.documents/tree/primary%3AMusic%2FEnglish/document/primary%3AMusic%2FEnglish%2FXXX.mp3】的路径在退出APP之后再进来,就无法再次使用了。

之后我才想到,可以参考那些已有项目,看看别人是怎么写音乐播放器,于是找到了,它直接用react-native-get-music-files来获取文件路径【/storage/emulated/0/Music/English/XXX.mp3】,这个路径好眼熟,我曾经用rn-fetch-blob拿到过呀,原来它就能播?

最终,这个符合我个人需求的音乐播放器总算是做完了。


后续:【content://com.android.externalstorage.documents/tree/primary%3AMusic%2FEnglish/document/primary%3AMusic%2FEnglish%2FXXX.mp3】现在这种路径的音乐也可以在退出APP再进入的时候播放了,通过原生代码就不会出现这种问题。

package com.caicemusic.app;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.DocumentsContract;
import android.util.Log;

import androidx.annotation.NonNull;

import com.facebook.react.bridge.ActivityEventListener;
import com.facebook.react.bridge.BaseActivityEventListener;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class FilePathModule extends ReactContextBaseJavaModule {

    private Promise folderPromise;

    public FilePathModule(@NonNull ReactApplicationContext reactContext) {
        super(reactContext);
        reactContext.addActivityEventListener(activityEventListener);
    }

    @NonNull
    @Override
    public String getName() {
        return "FilePathModule";
    }

    /**
     * 打开文件夹选择器
     */
    @ReactMethod
    public void pickFolder(Promise promise) {
        Activity currentActivity = getCurrentActivity();
        if (currentActivity == null) {
            promise.reject("ENOACT", "Activity 不存在");
            return;
        }

        folderPromise = promise;

        try {
            Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
                    | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);

            currentActivity.startActivityForResult(intent, 12345);
        } catch (Exception e) {
            promise.reject("ERROR", e.getMessage());
        }
    }

    @ReactMethod
    public void listFilesInFolder(String folderUriStr, Promise promise) {
        try {
            Uri folderUri = Uri.parse(folderUriStr);
            ContentResolver resolver = getReactApplicationContext().getContentResolver();

            Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(
                    folderUri, DocumentsContract.getTreeDocumentId(folderUri));

            Cursor cursor = resolver.query(childrenUri,
                    new String[]{
                            DocumentsContract.Document.COLUMN_DISPLAY_NAME,
                            DocumentsContract.Document.COLUMN_DOCUMENT_ID,
                            DocumentsContract.Document.COLUMN_MIME_TYPE
                    },
                    null, null, null);

            if (cursor == null) {
                promise.resolve("[]");
                return;
            }

            List<JSONObject> fileList = new ArrayList<>();

            while (cursor.moveToNext()) {
                String name = cursor.getString(0);
                String docId = cursor.getString(1);
                String mime = cursor.getString(2);

                if (!DocumentsContract.Document.MIME_TYPE_DIR.equals(mime)) {
                    Uri fileUri = DocumentsContract.buildDocumentUriUsingTree(folderUri, docId);
                    JSONObject fileObj = new JSONObject();
                    fileObj.put("name", name);
                    fileObj.put("uri", fileUri.toString());
                    fileList.add(fileObj);
                }
            }
            cursor.close();

            // 按文件名正序排序
            Collections.sort(fileList, new Comparator<JSONObject>() {
                @Override
                public int compare(JSONObject o1, JSONObject o2) {
                    try {
                        return o1.getString("name").compareToIgnoreCase(o2.getString("name"));
                    } catch (JSONException e) {
                        return 0;
                    }
                }
            });

            // 转回 JSONArray
            JSONArray files = new JSONArray();
            for (JSONObject obj : fileList) {
                files.put(obj);
            }

            promise.resolve(files.toString());

        } catch (Exception e) {
            Log.e("FilePathModule", "遍历文件失败", e);
            promise.reject("ERROR", e.getMessage());
        }
    }

    private final ActivityEventListener activityEventListener = new BaseActivityEventListener() {
        @Override
        public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
            if (requestCode == 12345 && folderPromise != null) {
                if (resultCode == Activity.RESULT_OK && data != null) {
                    Uri uri = data.getData();
                    if (uri != null) {
                        final int takeFlags = data.getFlags()
                                & (Intent.FLAG_GRANT_READ_URI_PERMISSION
                                | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
                        activity.getContentResolver().takePersistableUriPermission(uri, takeFlags);

                        folderPromise.resolve(uri.toString());
                    } else {
                        folderPromise.reject("ENOURI", "未选择文件夹");
                    }
                } else {
                    folderPromise.reject("CANCELLED", "用户取消选择");
                }
                folderPromise = null;
            }
        }
    };
}

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐