最近在阅读lx-music-mobile的代码,原作者代码写的很好,我在他的基础上增加了下载与本地音乐播放功能,原作者不实现肯定有他的考虑,我恰好有这个需求而已。
以上功能完成以后,我就想着能不能进一步直接在文件管理器中用lx.music打开某音乐,经过一番研究,发现在AndroidManifest.xml加上这些代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <activity> //... <intent-filter> <action android:name="android.intent.action.VIEW"/> <category android:name="android.intent.category.DEFAULT"/> <category android:name="android.intent.category.APP_MUSIC"/> <data android:mimeType="audio/*"/> <data android:mimeType="application/ogg"/> <data android:mimeType="application/x-ogg"/> <data android:mimeType="application/itunes"/> <data android:scheme="content"/> <data android:scheme="file"/> </intent-filter> //... </activity>
|
在文件管理器里面打开某音乐时,lx.music就会出现在可选择列表里。这样点击lx.music仅仅会唤起app而已,后面还需要实现“响应这个操作并播放音乐文件”。
先说一下这个intent,以我的初步理解,这东西是android内部不同app之间进行通讯的,在文件管理器打开音乐文件时发送了一个intent,lx.music匹配到这个intent(参见intent-filter的规则)后,需要解析出intent里面的内容,由于播放器的逻辑是rn实现的,所以需要给rn发送一个事件,js那边通过DeviceEventEmitter监听这个事件拿到音乐文件真实的路径。
这里的顺序很重要,需要js先通过DeviceEventEmitter监听这个事件,然后android发送这个事件,js那边才能收到。
app冷启动时,首先启动MainActivity,依次执行onCreate onStart onResume这些生命周期,然后加载并执行js代码,执行完毕后,会初始化reactContext对象。在onCreate onStart onResume这些生命周期里是拿不到reactContext的,所以就需要这样的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class MainActivity{ @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); final ReactInstanceManager reactInstanceManager = ((MainApplication) getApplication()).getReactNativeHost().getReactInstanceManager(); reactInstanceManager.addReactInstanceEventListener(new ReactInstanceManager.ReactInstanceEventListener() { @Override public void onReactContextInitialized(ReactContext context) { reactContext = context; } }); if (reactInstanceManager.hasStartedCreatingInitialContext()) { reactContext = reactInstanceManager.getCurrentReactContext(); } } }
|
reactContext不为null,说明js代码执行完毕,在android端执行:
1 2
| reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) .emit("onPathReceived", event);
|
在js端,通过以下代码接收这个消息:
1 2 3
| const eventListener = DeviceEventEmitter.addListener('onPathReceived', eventParams => { });
|
值得注意的是,刚才说android会在js执行完毕后初始化reactContext对象,这里的“执行完毕”具体指的是什么,都执行了哪些代码。假如是在浏览器上,一个标准的react组件:
1 2 3 4 5 6 7 8
| const app = ()=>{ useEffect(()=>{ },[]) return ( <div>a page</div> ) }
|
这个组件是如何渲染到浏览器上的?首先会执行render函数,把虚拟dom转换成真实的dom挂载到浏览器上,dom只是浏览器内部的一种数据结构,真正把画面渲染出来是浏览器做的工作,浏览器渲染完毕,会执行useEffect的回调函数。
在react-native上,js的宿主环境不是浏览器,渲染工作是由原生完成的,由此可见,上面说的“执行完毕”仅仅执行了render函数,所以不能把DeviceEventEmitter.addListener放在useEffect里面,应该更提前,比如放在组件外面:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const eventListener = DeviceEventEmitter.addListener('onPathReceived', eventParams => { global.event.emit('onPathReceived',eventParams.path) }); const app = ()=>{ useEffect(()=>{ global.event.on('onPathReceived',path=>{ }) },[]) return ( <div>a page</div> ) }
|
这样又带来一个问题,当onPathReceived触发时,很可能涉及音乐播放的组件还没有渲染(比如有的组件是动态加载的),即使发射了onPathReceived事件,也没有任何响应,这里可以设置一个缓存,比如这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| class Event { cache: Map<string, Array<any>>
constructor() { super() this.cache = new Map() }
on(eventName: string, listener: (...args: any[]) => any) {
let cachedEvents = this.cache.get(eventName) if (cachedEvents) { for (let args of cachedEvents) { listener(...args) } this.cache.delete(eventName) } }
emit(eventName: string, ...args: any[]) {
if (!this.listeners.has(eventName)) { let cachedEvents = this.cache.get(eventName) if (!cachedEvents) this.cache.set(eventName, cachedEvents = []) cachedEvents.push(args) } }
}
|
这样就ok了。