在React Native开发过程中,有时我们想要使用原生的一个UI组件或者是js比较难以实现的功能时,我们可以在react Naitve应用程序中封装和植入已有的原生组件。 本文我们实现一个VideoView的本地调用。
React Native并没有给我们提供VideoView这个组件,那我们要播放视频的话,有两种方法:一种是借助WebView,一种就是使用原生的播放器。
Java端实现
新建VideoViewManager类,并继承SimpleViewManager,SimpleViewManager类需要传入一个泛型,该泛型继承Android的View,也就是说该泛型是要使用android 平台的哪个View就传入该View,比如,我要使用android的VideoView,这个泛型就传入VideoView。相关的代码如下:
@H_404_8@public class VideoViewManager extends SimpleViewManager<VideoView>{ @Override public String getName() {//组件名称 return "VideoView"; } @Override protected VideoView createViewInstance(ThemedReactContext reactContext) { VideoView video = new VideoView(reactContext); return video; } }getName返回组件名称(可以加前缀RCT),createViewInstance方法返回实例对象,可以在初始化对象时设置一些属性。
其中,可以通过@ReactProp(或@ReactPropGroup)注解来导出属性的设置方法。该方法有两个参数,第一个参数是泛型View的实例对象,第二个参数是要设置的属性值。方法的返回值类型必须为void,而且访问控制必须被声明为public。组件的每一个属性的设置都会调用Java层被对应ReactProp注解的方法。
@ReactProp注解必须包含一个字符串类型的参数name。这个参数指定了对应属性在JavaScript端的名字。那么现在JS端可以这么设置source属性值。
但是在设置播放地址的时候,我们可能需要同时设置header信息,所以对上面的代码优化如下:
VideoViewManager类的完整代码如下:
@H_404_8@public class VideoViewManager extends SimpleViewManager<VideoView>{ @Override public String getName() { return "VideoView"; } @Override protected VideoView createViewInstance(ThemedReactContext reactContext) { VideoView video = new VideoView(reactContext); return video; } @Override public void onDropViewInstance(VideoView view) {//对象销毁时 super.onDropViewInstance(view); view.stopPlayback();//停止播放 } @ReactProp(name = "source") public void setSource(VideoView videoView,@Nullable ReadableMap source){ if(source != null){ if (source.hasKey("url")) { String url = source.getString("url"); System.out.println("url = "+url); HashMap<String,String> headerMap = new HashMap<>(); if (source.hasKey("headers")) { ReadableMap headers = source.getMap("headers"); ReadableMapKeySetIterator iter = headers.keySetIterator(); while (iter.hasNextKey()) { String key = iter.nextKey(); headerMap.put(key,headers.getString(key)); } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { videoView.setVideoURI(Uri.parse(url),headerMap); }else{ try { Method setVideoURIMethod = videoView.getClass().getMethod("setVideoURI",Uri.class,Map.class); setVideoURIMethod.invoke(videoView,Uri.parse(url),headerMap); } catch (Exception e) { e.printStackTrace(); } } videoView.start(); } } } }接着我们需要将UI组件注册到系统中去。创建VideoViewPackage,并注册到ReactNativeHost。
@H_404_8@public class VideoViewPackage implements ReactPackage { @Override public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { return Collections.emptyList(); } @Override public List<Class<? extends JavaScriptModule>> createJSModules() { return Collections.emptyList(); } @Override public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { return Arrays.<ViewManager>asList( new VideoViewManager() ); } }然后向Application注册,以前的版本是向MainActivity注册。
@H_404_8@@Override protected List<ReactPackage> getPackages() { return Arrays.<ReactPackage>asList( new MainReactPackage(),new OrientationPackage(),new VideoViewPackage() ); }Js端实现
在项目js/component文件夹下新建VideoView.js。代码如下:
@H_404_8@import React,{ PropTypes }from 'react'; import {requireNativeComponent,View} from 'react-native'; var VideoView = { name:'VideoView',propTypes:{ style: View.propTypes.style,source:PropTypes.shape({ url:PropTypes.string,headers:PropTypes.object,}),...View.propTypes,//包含默认的View的属性,如果没有这句会报‘has no propType for native prop’错误 } }; var RCTVideoView = requireNativeComponent('VideoView',VideoView); module.exports = RCTVideoView; @H_404_8@import React,{Component} from 'react'; import { View,StyleSheet,Text,TouchableOpacity } from 'react-native'; import VideoView from './component/VideoView'; export default class VideoPlayView extends Component { constructor(props) { super(props); } render() { return ( <View style={styles.videoContainer}> <Text style={styles.text}>康熙王朝</Text> <VideoView style={styles.video} source={ { url: 'http://ohe65w0xx.bkt.clouddn.com/test3.mp4',headers: { 'refer': 'myRefer' } } } /> </View> ); } } const styles = StyleSheet.create({ videoContainer: { flex: 1,justifyContent: 'center',alignItems: 'center',},text: { fontSize: 20,video: { marginTop:10,height: 250,width: 380 },});最终运行效果:
到此,React Native调用原生组件就基本实现了,不过,native层的一些信息我们还无法获取到,比如:视频的总时长、视频当前播放的时间点等。所以我们希望实现相关的功能。
@H_437_403@native层向js发送消息事件声明一个VideoViewManager的内部类RCTVideoView,它继承VideoView,并实现了一些必要的接口。
@H_404_8@private static class RCTVideoView extends VideoView implements LifecycleEventListener,MediaPlayer.OnPreparedListener,MediaPlayer.OnCompletionListener,MediaPlayer.OnErrorListener,MediaPlayer.OnInfoListener,MediaPlayer.OnBufferingUpdateListener{ public RCTVideoView(ThemedReactContext reactContext) { super(reactContext); reactContext.addLifecycleEventListener(this); setOnPreparedListener(this); setOnCompletionListener(this); setOnErrorListener(this); } @Override public void onHostResume() { FLog.e(VideoViewManager.class,"onHostResume"); } @Override public void onHostPause() { FLog.e(VideoViewManager.class,"onHostPause"); pause(); } @Override public void onHostDestroy() { FLog.e(VideoViewManager.class,"onHostDestroy"); } @Override public void onPrepared(MediaPlayer mp) {//视频加载成功准备播放 FLog.e(VideoViewManager.class,"onPrepared duration = "+mp.getDuration()); mp.setOnInfoListener(this); mp.setOnBufferingUpdateListener(this); } @Override public void onCompletion(MediaPlayer mp) {//视频播放结束 FLog.e(VideoViewManager.class,"onCompletion"); } @Override public boolean onError(MediaPlayer mp,int what,int extra) {//视频播放出错 FLog.e(VideoViewManager.class,"onError what = "+ what+" extra = "+extra); return false; } @Override public boolean onInfo(MediaPlayer mp,int extra) { FLog.e(VideoViewManager.class,"onInfo"); switch (what) { /** * 开始缓冲 */ case MediaPlayer.MEDIA_INFO_BUFFERING_START: FLog.e(VideoViewManager.class,"开始缓冲"); break; /** * 结束缓冲 */ case MediaPlayer.MEDIA_INFO_BUFFERING_END: FLog.e(VideoViewManager.class,"结束缓冲"); break; /** * 开始渲染视频第一帧画面 */ case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START: FLog.e(VideoViewManager.class,"开始渲染视频第一帧画面"); break; default: break; } return false; } @Override public void onBufferingUpdate(MediaPlayer mp,int percent) {//视频缓冲进度 FLog.e(VideoViewManager.class,"onBufferingUpdate percent = "+percent); } }接着我们在java层的onPrepared方法中获取视频播放时长,并想js发送事件通知。
@H_404_8@@Override public void onPrepared(MediaPlayer mp) {//视频加载成功准备播放 int duration = mp.getDuration(); FLog.e(VideoViewManager.class,"onPrepared duration = "+duration); mp.setOnInfoListener(this); mp.setOnBufferingUpdateListener(this); //向js发送事件 WritableMap event = Arguments.createMap(); event.putInt("duration",duration);//key用于js中的nativeEvent ReactContext reactContext = (ReactContext) getContext(); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( getId(),//native层和js层两个视图会依据getId()而关联在一起 "topChange",//事件名称 event//事件携带的数据 ); }receiveEvent接收三个参数,参数说明如注释所示,这个事件名topChange在JavaScript端映射到onChange回调属性上(这个映射关系在UIManagerModuleConstants.java文件里),这个回调会被原生事件执行。
当Js层收到通知之后,我们对VideoView.js代码进行优化。
我们在java中发送的事件中携带的数据WritableMap中,定义的key与在js中event.nativeEvent.duration一致,nativeEvent和key就可以获取到value。
有时候有一些特殊的属性,想从原生组件中导出,但是又不希望它们成为对应React封装组件的属性,可以使用nativeOnly来声明。如果没有什么特殊属性需要设置的话,requireNativeComponent第三个参数可以不用。
需要注意的是,之前VideoView.js以下两句是这样:
我们需要将它改为下面的这样:
@H_404_8@var RCTVideoView = requireNativeComponent('VideoView',{ nativeOnly: {onChange: true} }); module.exports = VideoView;如果你不小心还是使用之前exports RCTVideoView 的那样,那么会一直接收不到onChange事件的回调!
VideoView增加了onPrepared回调方法,运行程序后,可以看到打印了duration信息。但是如果native层需要发送的事件比较多的情况下,那么如果我们使用单一的topChange事件,就会导致回调的onChange不是单一职责。那么,我们是否可以自定义该事件的名称呢,使每一个事件对应各自的回调方法呢?下面我们就讲讲如何自定义事件名称。
自定义事件名称
首先,在VideoViewManager类中重写getExportedCustomDirectEventTypeConstants方法,然后自定义事件名称。
@H_404_8@@Override public Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( "onCompletion",MapBuilder.of("registrationName","onCompletion")); }第一个onCompletion字符串是java端发送事件是的名称,即receiveEvent方法的第二个参数值;第二个onCompletion字符串是定义在js端的回调方法;registrationName字符串的值是固定的,不能修改。对比一下topChange事件就知道了。
@H_404_8@@Override public Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( "topChange","onChange")); }接着,在内部类RCTVideoView的onCompletion方法发送事件。相关代码如下:
@H_404_8@@Override public void onCompletion(MediaPlayer mp) {//视频播放结束 FLog.e(VideoViewManager.class,"onCompletion"); ReactContext reactContext = (ReactContext) getContext(); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( getId(),//native和js两个视图会依据getId()而关联在一起 "onCompletion",//事件名称 null ); }由于只是通知js端,告诉它播放结束,不用携带任何数据,所以receiveEvent的第三个参数为null即可。然后在VideoView.js增加propTypes属性。
@H_404_8@VideoView.propTypes = { onCompletion:PropTypes.func,//省略其它代码 };最后在VideoPlayScene.js中使用VideoView时,增加onCompletion属性即可。相关的逻辑如下:
@H_404_8@<VideoView style={{height: 250,width: 380}} source={ { url: 'http://qiubai-video.qiushibaike.com/A14EXG7JQ53PYURP.mp4',headers: { 'refer': 'myRefer' } } } onPrepared={this._onPrepared} onCompletion={()=>{ console.log("JS onCompletion"); }} />当我们运行时,在浏览器就可以看到相关的打印日志。
其他的事件的定义流程都一样,比如获取当前进度信息、缓存进度、错误回调等。然后,我们看看VideoViewManager的完整实现。
@H_404_8@public class VideoViewManager extends SimpleViewManager<VideoView>{ private enum VideoEvent{ EVENT_PREPARE("onPrepared"),EVENT_PROGRESS("onProgress"),EVENT_UPDATE("onBufferUpdate"),EVENT_ERROR("onError"),EVENT_COMPLETION("onCompletion"); private String mName; VideoEvent(String name) { this.mName = name; } @Override public String toString() { return mName; } } @Override public String getName() { return "VideoView"; } @Override protected VideoView createViewInstance(ThemedReactContext reactContext) { RCTVideoView video = new RCTVideoView(reactContext); return video; } @Nullable @Override public Map<String,Integer> getCommandsMap() { return super.getCommandsMap(); } @Override public void receiveCommand(VideoView root,int commandId,@Nullable ReadableArray args) { super.receiveCommand(root,commandId,args); } @Nullable @Override public Map<String,Object> getExportedCustomDirectEventTypeConstants() { MapBuilder.Builder<String,Object> builder = MapBuilder.builder(); for (VideoEvent event:VideoEvent.values()){ builder.put(event.toString(),event.toString())); } return builder.build(); } @Override public void onDropViewInstance(VideoView view) {//销毁对象时释放一些资源 super.onDropViewInstance(view); ((ThemedReactContext) view.getContext()).removeLifecycleEventListener((RCTVideoView) view); view.stopPlayback(); } @ReactProp(name = "source") public void setSource(RCTVideoView videoView,@Nullable ReadableMap source){ if(source != null){ if (source.hasKey("url")) { String url = source.getString("url"); FLog.e(VideoViewManager.class,"url = "+url); HashMap<String,String> headerMap = new HashMap<>(); if (source.hasKey("headers")) { ReadableMap headers = source.getMap("headers"); ReadableMapKeySetIterator iter = headers.keySetIterator(); while (iter.hasNextKey()) { String key = iter.nextKey(); String value = headers.getString(key); FLog.e(VideoViewManager.class,key+" = "+value); headerMap.put(key,value); } } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { videoView.setVideoURI(Uri.parse(url),Uri.class,Map.class); setVideoURIMethod.invoke(videoView,headerMap); } catch (Exception e) { e.printStackTrace(); } } videoView.start(); } } } private static class RCTVideoView extends VideoView implements LifecycleEventListener,MediaPlayer.OnPreparedListener,MediaPlayer.OnCompletionListener,MediaPlayer.OnErrorListener,MediaPlayer.OnInfoListener,MediaPlayer.OnBufferingUpdateListener,Runnable{ private Handler mHandler; public RCTVideoView(ThemedReactContext reactContext) { super(reactContext); reactContext.addLifecycleEventListener(this); setOnPreparedListener(this); setOnCompletionListener(this); setOnErrorListener(this); mHandler = new Handler(); } @Override public void onHostResume() { FLog.e(VideoViewManager.class,"onHostResume"); } @Override public void onHostPause() { FLog.e(VideoViewManager.class,"onHostPause"); pause(); } @Override public void onHostDestroy() { FLog.e(VideoViewManager.class,"onHostDestroy"); mHandler.removeCallbacks(this); } @Override public void onPrepared(MediaPlayer mp) {//视频加载成功准备播放 int duration = mp.getDuration(); FLog.e(VideoViewManager.class,"onPrepared duration = "+duration); mp.setOnInfoListener(this); mp.setOnBufferingUpdateListener(this); WritableMap event = Arguments.createMap(); event.putInt("duration",duration);//key用于js中的nativeEvent dispatchEvent(VideoEvent.EVENT_PREPARE.toString(),event); mHandler.post(this); } @Override public void onCompletion(MediaPlayer mp) {//视频播放结束 FLog.e(VideoViewManager.class,"onCompletion"); dispatchEvent(VideoEvent.EVENT_COMPLETION.toString(),null); mHandler.removeCallbacks(this); int progress = getDuration(); WritableMap event = Arguments.createMap(); event.putInt("progress",progress); dispatchEvent(VideoEvent.EVENT_PROGRESS.toString(),event); } @Override public boolean onError(MediaPlayer mp,int extra) {//视频播放出错 FLog.e(VideoViewManager.class,"onError what = "+ what+" extra = "+extra); mHandler.removeCallbacks(this); WritableMap event = Arguments.createMap(); event.putInt("what",what); event.putInt("extra",what); dispatchEvent(VideoEvent.EVENT_ERROR.toString(),event); return true; } @Override public boolean onInfo(MediaPlayer mp,int extra) { FLog.e(VideoViewManager.class,"onInfo"); switch (what) { /** * 开始缓冲 */ case MediaPlayer.MEDIA_INFO_BUFFERING_START: FLog.e(VideoViewManager.class,"开始缓冲"); break; /** * 结束缓冲 */ case MediaPlayer.MEDIA_INFO_BUFFERING_END: FLog.e(VideoViewManager.class,"结束缓冲"); break; /** * 开始渲染视频第一帧画面 */ case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START: FLog.e(VideoViewManager.class,"开始渲染视频第一帧画面"); break; default: break; } return false; } @Override public void onBufferingUpdate(MediaPlayer mp,int percent) {//视频缓冲进度 FLog.e(VideoViewManager.class,"onBufferingUpdate percent = "+percent); int buffer = (int) Math.round((double) (mp.getDuration() * percent) / 100.0); WritableMap event = Arguments.createMap(); event.putInt("buffer",buffer); dispatchEvent(VideoEvent.EVENT_UPDATE.toString(),event); } @Override public void run() { int progress = getCurrentPosition(); WritableMap event = Arguments.createMap(); event.putInt("progress",event); mHandler.postDelayed(this,1000); } private void dispatchEvent(String eventName,WritableMap eventData){ ReactContext reactContext = (ReactContext) getContext(); reactContext.getJSModule(RCTEventEmitter.class).receiveEvent( getId(),//native和js两个视图会依据getId()而关联在一起 eventName,//事件名称 eventData ); } } }对于VideoView.js我们改造如下:
@H_404_8@class VideoView extends Component{ constructor(props){ super(props); } /*_onChange(event){ if(!this.props.onPrepared){ return; } this.props.onPrepared(event.nativeEvent.duration); }*/ _onPrepared(event){ if(!this.props.onPrepared){ return; } this.props.onPrepared(event.nativeEvent.duration); } _onError(event){ if(!this.props.onError){ return; } this.props.onError(event.nativeEvent); } _onBufferUpdate(event){ if(!this.props.onBufferUpdate){ return; } this.props.onBufferUpdate(event.nativeEvent.buffer); } _onProgress(event){ if(!this.props.onProgress){ return; } this.props.onProgress(event.nativeEvent.progress); } render(){ //return <RCTVideoView {...this.props} onChange={this._onChange.bind(this)}/>; return <RCTVideoView {...this.props} onPrepared={this._onPrepared.bind(this)} onError={this._onError.bind(this)} onBufferUpdate={this._onBufferUpdate.bind(this)} onProgress={this._onProgress.bind(this)} />; }; } VideoView.name = "VideoView"; VideoView.propTypes = { onPrepared:PropTypes.func,onCompletion:PropTypes.func,onError:PropTypes.func,onBufferUpdate:PropTypes.func,onProgress:PropTypes.func,}; var RCTVideoView = requireNativeComponent('VideoView',{ nativeOnly: {onChange: true} }); module.exports = VideoView;VideoView的使用(省略其它代码),VideoPlayScene.js
@H_404_8@<VideoView style={{height: 250,headers: { 'refer': 'myRefer' } } } onPrepared={this._onPrepared} onCompletion={()=>{ console.log("JS onCompletion"); }} onError={(e)=>{ console.log("what="+e.what+" extra="+e.extra); }} onBufferUpdate={(buffer)=>{ console.log("JS buffer = "+buffer); }} onProgress={(progress)=>{ console.log("JS progress = "+progress); }} />js层向native层发送命令
讲完native层向js发送事件后,那么js如何向native命令呢?继续往下看。比如在js端我想通过点击某个按钮,来控制视频暂停,那么就需要native层来响应这个操作,因为native掌握着VideoView的所有权,暂停可以通过调用VideoView对象的pause方法。首先,我们需要在native层定义这些命令,并在接收到命令时处理相关操作。
在VideoViewManager重写getCommandsMap方法。
getCommandsMap接收多组命令,每组命令需要包括名称(js端调用的方法名)和命令id,如上面的COMMAND_PAUSE_NAME 和 COMMAND_PAUSE_ID。
然后重写receiveCommand方法,处理相应的命令。
Native端在接收到COMMAND_PAUSE_ID 命令时,调用了VideoView的pause方法进行暂停播放。
接下来就是js端如何发起该命令了,打开VideoView.js,添加如下代码:
主要是定义了一个pause函数,该函数内使用UIManager.dispatchViewManagerCommand向native层发送命令,该方法接收三个参数:第一个参数是组件的实例对象;第二个是发送的命令名称,与native层定义的command name一致;第三个是命令携带的参数数据。
打开VideoPlayScene.js,给视频播放添加暂停功能。
运行程序,你发现已经可以暂停播放了。同样的流程,我们再给播放器添加‘开始播放’的功能。开发VideoViewManager.java,添加开始播放功能。
@H_404_8@private static final int COMMAND_START_ID = 2; private static final String COMMAND_START_NAME = "start"; @Override public Map<String,COMMAND_PAUSE_ID,COMMAND_START_NAME,COMMAND_START_ID); } @Override public void receiveCommand(VideoView root,@Nullable ReadableArray args) { FLog.e(VideoViewManager.class,"receiveCommand id = "+commandId); switch (commandId){ case COMMAND_PAUSE_ID: root.pause(); break; case COMMAND_START_ID: root.start(); break; default: break; } } @H_404_8@start(){ UIManager.dispatchViewManagerCommand( findNodeHandle(this.refs[RCT_VIDEO_REF]),UIManager.VideoView.Commands.start,null ); } @H_404_8@_onPressPause(){ this.video.pause(); } _onPressStart(){ this.video.start(); } render() { return ( <View style={{flex: 1,justifyContent: 'center',}}> <VideoView ref={(video)=>{this.video = video}} //省略其它代码 /> <View style={{height:50,flexDirection:'row',justifyContent:'flex-start'}}> <Text style={{width:100}}>{this.state.time}/{this.state.totalTime}</Text> <TouchableOpacity style={{marginLeft:10}} onPress={this._onPressPause.bind(this)}> <Text>暂停</Text> </TouchableOpacity> <TouchableOpacity style={{marginLeft:10}} onPress={this._onPressStart.bind(this)}> <Text>开始</Text> </TouchableOpacity> </View> </View> ); }然后再次运行,效果如下:
ok,上面的pause和start方法都是没有带参数的,那么如果native层需要参数呢,比如seekTo(快进),这个方法需要有一个参数,设置视频快进到的位置,那么如何处理呢?关于快进等功能的实现,请访问项目稀饭APP源码