[置顶] 【稀饭】react native 实战系列教程之自定义原生UI组件

时间:2022-08-31 22:24:34

上一节,讲了关于RN的自定义原生模块,本节是关于自定义原生UI组件,学习完本节,你将了解到原生UI组件的开发流程,以及js如何向native发送命令和native如何向js发送事件。

原生UI组件之VideoView视频播放器开发

React Native并没有给我们提供VideoView这个组件,那我们要播放视频的话,有两种方法:一种是借助WebView,一种就是使用原生的播放器。这里我们就介绍下,如何使用原生VideoView,封装成一个组件,提供给JS使用。

实现JAVA端的组件

开发View组件,需要Manager和Package。

新建VideoViewManager类,并继承SimpleViewManager,SimpleViewManager类需要传入一个泛型,该泛型继承android的View,也就是说该泛型是要使用android 平台的哪个View就传入该View,比如,我要使用android的VideoView,这个泛型就传入VideoView。

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方法返回实例对象,可以在初始化对象时设置一些属性。

接着,我们需要让该组件提供视频的url地址。

我们可以通过@ReactProp(或@ReactPropGroup)注解来导出属性的设置方法。该方法有两个参数,第一个参数是泛型View的实例对象,第二个参数是要设置的属性值。方法的返回值类型必须为void,而且访问控制必须被声明为public。组件的每一个属性的设置都会调用JAVA层被对应ReactProp注解的方法。 如下给VideoView提供source的属性设置:

@ReactProp(name = "source")
public void setSource(RCTVideoView videoView,@Nullable String source){
if(source != null){
videoView.setVideoURI(Uri.parse(source));
videoView.start();
}
}

@ReactProp注解必须包含一个字符串类型的参数name。这个参数指定了对应属性在JavaScript端的名字。那么现在JS端可以这么设置source属性值

<VideoView source='http://qiubai-video.qiushibaike.com/A14EXG7JQ53PYURP.mp4'/>

但是在设置播放地址的时候,我们可能需要同时设置header(为什么不能像上面source一样来提供一个方法setHeader呢?思考一下),现在改造一下setSource方法

@ReactProp(name = "source")
public void setSource(VideoView 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),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();
}
}
}

setSource的第二个参数变为ReadableMap,这是一个键值对类型的,用于JS传递参数给JAVA。url必修要有,headers不一定有,现在JS端可能变这样:

<VideoView
source={
{
url:'http://qiubai-video.qiushibaike.com/A14EXG7JQ53PYURP.mp4',
headers:{
'refer':'myRefer'
}
}
}
/>

可以发现不同的参数类型,在JS端使用的个中差异。JavaScript所得知的属性类型会由方法的第二个参数的类型来自动决定。支持的类型有:boolean, int, float, double, String, Boolean, Integer, ReadableArray, ReadableMap。

当前阶段VideoViewManager类的完整代码如下

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();
}
}
}
}

接着,我们需要和自定义模块一样,创建VideoViewPackage,并注册到ReactNativeHost

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()
);
}
}

MainApplication.java

@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new OrientationPackage(),
new VideoViewPackage()
);
}

好了,写完java端,现在需要在JS端调用它。

实现JS端的组件

在项目js/component文件夹下新建VideoView.js

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;

首先和自定义模块导入的NativeModules不同,组件使用的模块是requireNativeComponent,接着我们需要给组件定义声明一些属性name(用于调试信息显示)、propTypes。

其中重要的是propTypes,它定义了该组件拥有哪些属性可以使用,对应到原生视图上。由于source是url、headers一组属性值构成的,所以使用PropTypes.shape来定义。

最后不要遗漏了 …View.propTypes 这句,它包含了默认View的属性,如果没有这句就会报错

requireNativeComponent通常接受两个参数,第一个参数是原生视图的名字(JAVA层VideoViewManager$getName的值),而第二个参数是一个描述组件接口的对象。最后通过module.exports导出提供给其他组件使用。

在VideoPlayScene.js中使用

import React, {Component} from 'react';
import {
View,
WebView,
NativeModules,
} from 'react-native';
import VideoView from './component/VideoView';

export default class VideoPlayScene extends Component {
constructor(props) {
super(props);
}

render() {
return (
<View style={{flex:1,alignItems:'center',justifyContent:'center',}}>
<VideoView
style={{height:250,width:380}}
source={
{
url:'http://qiubai-video.qiushibaike.com/A14EXG7JQ53PYURP.mp4',
headers:{
'refer':'myRefer'
}
}
}
/>

</View>
);
}
}

然后运行。注意:如果改动涉及到JAVA层的修改,那么需要关闭掉React Packager窗口,并在cmd重新执行react-native run-android 命令。

[置顶]        【稀饭】react native 实战系列教程之自定义原生UI组件

可以看到视频正常播放了,但好像只是仅仅能使用native层的播放器,然而native层的一些信息我们还无法获取到,比如:视频的总时长、视频当前播放的时间点等;而且还不能控制组件的状态,比如:视频的快进、暂停、播放等。接下来我们将实现这些。

native层向js发送消息事件

我们声明一个VideoViewManager的内部类RCTVideoView,它继承VideoView,并实现了一些必要的接口。

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 what, 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);
}
}

这里并没有实现什么逻辑,只是打印一下信息。接着将VideoViewManager$createViewInstance使用RCTVideoView对象

@Override
protected VideoView createViewInstance(ThemedReactContext reactContext) {
RCTVideoView video = new RCTVideoView(reactContext);
return video;
}

@Override
public void onDropViewInstance(VideoView view) {//销毁对象时释放一些资源
super.onDropViewInstance(view);
((ThemedReactContext) view.getContext()).removeLifecycleEventListener((RCTVideoView) view);
view.stopPlayback();
}

setSource传入的第一个参数也是RCTVideoView对象

@ReactProp(name = "source")
public void setSource(RCTVideoView videoView,@Nullable ReadableMap source){
//省略其它代码
}

接着我们在java层的onPrepared方法中获取视频播放时长,并想js发送事件通知。

@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改为如下:

class VideoView extends Component{
constructor(props){
super(props);
}

_onChange(event){
if(!this.props.onPrepared){
return;
}
this.props.onPrepared(event.nativeEvent.duration);
}

render(){
return <RCTVideoView {...this.props} onChange={this._onChange.bind(this)}/>;
};
}

VideoView.name = "VideoView";
VideoView.propTypes = {
onPrepared:PropTypes.func,
style: View.propTypes.style,
source:PropTypes.shape({
url:PropTypes.string,
headers:PropTypes.object,
}),
...View.propTypes,
};
//需要注意下面这两句
var RCTVideoView = requireNativeComponent('VideoView',VideoView,{
nativeOnly: {onChange: true}
});
module.exports = VideoView;

我们在java中发送的事件中携带的数据WritableMap中,定义的key与在js中event.nativeEvent.duration一致,nativeEvent和key就可以获取到value。

有时候有一些特殊的属性,想从原生组件中导出,但是又不希望它们成为对应React封装组件的属性,可以使用nativeOnly来声明。如果没有什么特殊属性需要设置的话,requireNativeComponent第三个参数可以不用。

需要注意的是,之前VideoView.js以下两句是这样

var RCTVideoView = requireNativeComponent('VideoView',VideoView);
module.exports = RCTVideoView;

修改之后变这样

var RCTVideoView = requireNativeComponent('VideoView',VideoView,{
nativeOnly: {onChange: true}
});
module.exports = VideoView;

不一样的地方在于一个exports RCTVideoView,一个exports VideoView

如果你不小心还是使用之前exports RCTVideoView 的那样,那么会一直接收不到onChange事件的回调!(本人踩到的坑)

ok,最后在VideoPlayScene.js调用

_onPrepared(duration){
console.log("JS duration = "+duration);
}

render() {
return (
<View style=
{{flex: 1, alignItems: 'center', justifyContent: 'center',}}>
<VideoView
style=
{{height: 250, width: 380}}
source={
{
url: 'http://qiubai-video.qiushibaike.com/A14EXG7JQ53PYURP.mp4',
headers: {
'refer': 'myRefer'
}
}
}
onPrepared={this._onPrepared}
/>

</View>
);
}

VideoView增加了onPrepared回调方法,运行程序后,可以看到打印了duration信息。但是如果native层需要发送的事件比较多的情况下,那么如果我们使用单一的topChange事件,就会导致回调的onChange不是单一职责。那么,我们是否可以自定义该事件的名称呢,使每一个事件对应各自的回调方法呢?下面我们就讲讲如何自定义事件名称。

自定义事件名称

我们以播放器播放完成的事件为例,监听onCompletion事件。

首先,在VideoViewManager类中重写getExportedCustomDirectEventTypeConstants方法,然后自定义事件名称。

@Override
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"onCompletion", MapBuilder.of("registrationName", "onCompletion"));
}

第一个onCompletion字符串是java端发送事件是的名称,即receiveEvent方法的第二个参数值;第二个onCompletion字符串是定义在js端的回调方法;registrationName字符串的值是固定的,不能修改。对比一下topChange事件就知道了

@Override
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"topChange", MapBuilder.of("registrationName", "onChange"));
}

接着,在内部类RCTVideoView的onCompletion方法发送事件

@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属性。

VideoView.propTypes = {
onCompletion:PropTypes.func,
//省略其它代码
};

最后在VideoPlayScene.js中使用VideoView时,增加onCompletion属性即可。

<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");
}}
/>

运行程序后就可以看到log输出了(打开debug js remotely在浏览器查看,或者在android studio中查看)

[置顶]        【稀饭】react native 实战系列教程之自定义原生UI组件

其他的事件的定义流程都一样,比如获取当前进度信息、缓存进度、错误回调等。目前为止,VideoViewManager.java的完整代码如下:

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(),MapBuilder.of("registrationName", 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),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();
}
}
}

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 what, 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 what, 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",progress);
dispatchEvent(VideoEvent.EVENT_PROGRESS.toString(),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完整代码如下:

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,
style: View.propTypes.style,
source:PropTypes.shape({
url:PropTypes.string,
headers:PropTypes.object,
}),
...View.propTypes,
};

var RCTVideoView = requireNativeComponent('VideoView',VideoView,{
nativeOnly: {onChange: true}
});
module.exports = VideoView;

VideoView的使用(省略其它代码),VideoPlayScene.js

<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");
}}
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方法。

private static final int COMMAND_PAUSE_ID = 1;
private static final String COMMAND_PAUSE_NAME = "pause";

@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of(
COMMAND_PAUSE_NAME,COMMAND_PAUSE_ID
);
}

getCommandsMap接收多组命令,每组命令需要包括名称(js端调用的方法名)和命令id,如上面的COMMAND_PAUSE_NAME 和 COMMAND_PAUSE_ID。

然后重写receiveCommand方法,处理相应的命令。

@Override
public void receiveCommand(VideoView root, int commandId, @Nullable ReadableArray args) {
switch (commandId){
case COMMAND_PAUSE_ID:
root.pause();
break;
default:
break;
}
}

我们在接收到COMMAND_PAUSE_ID 命令时,调用了VideoView的pause方法进行暂停播放。

接下来就是js端如何发起该命令了。

打开VideoView.js,代码添加如下

import {
requireNativeComponent,
View,
UIManager,
findNodeHandle,
} from 'react-native';

var RCT_VIDEO_REF = 'VideoView';

class VideoView extends Component{
//省略其它代码
pause(){
//向native层发送命令
UIManager.dispatchViewManagerCommand(
findNodeHandle(this.refs[RCT_VIDEO_REF]),
UIManager.VideoView.Commands.pause,//Commands.pause与native层定义的COMMAND_PAUSE_NAME一致
null//命令携带的参数数据
);
}
render(){
return <RCTVideoView
ref = {RCT_VIDEO_REF}
//省略其它代码
/>
;

};
}

主要是定义了一个pause函数,该函数内使用UIManager.dispatchViewManagerCommand向native层发送命令,该方法接收三个参数:第一个参数是组件的实例对象;第二个是发送的命令名称,与native层定义的command name一致;第三个是命令携带的参数数据。

打开VideoPlayScene.js,给视频播放添加暂停功能。

export default class VideoPlayScene extends Component {
//暂停播放
_onPressPause(){
this.video.pause();
}

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>
</View>
</View>
);
}
}

好了,运行程序,你发现已经可以暂停播放了。同样的流程,我们再给播放器添加‘开始播放’的功能。

VideoViewManager.java 添加开始播放的代码

private static final int COMMAND_START_ID = 2;
private static final String COMMAND_START_NAME = "start";
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of(
COMMAND_PAUSE_NAME,COMMAND_PAUSE_ID,
COMMAND_START_NAME,COMMAND_START_ID);
}

@Override
public void receiveCommand(VideoView root, int commandId, @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;
}
}

VideoView.js 添加开始播放的代码

start(){
UIManager.dispatchViewManagerCommand(
findNodeHandle(this.refs[RCT_VIDEO_REF]),
UIManager.VideoView.Commands.start,
null
);
}

VideoPlayScene.js添加开始播放的功能

_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>
);
}

最后运行程序,效果如下

[置顶]        【稀饭】react native 实战系列教程之自定义原生UI组件

ok,上面的pause和start方法都是没有带参数的,那么如果native层需要参数呢,比如seekTo(快进),这个方法需要有一个参数,设置视频快进到的位置,那么如何处理呢?

VideoViewManager.java增加seekTo命令

private static final int COMMAND_SEEK_TO_ID = 3;
private static final String COMMAND_SEEK_TO_NAME = "seekTo";
@Override
public Map<String, Integer> getCommandsMap() {
return MapBuilder.of(
COMMAND_PAUSE_NAME,COMMAND_PAUSE_ID,
COMMAND_START_NAME,COMMAND_START_ID,
COMMAND_SEEK_TO_NAME, COMMAND_SEEK_TO_ID
);
}
@Override
public void receiveCommand(VideoView root, int commandId, @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;
case COMMAND_SEEK_TO_ID://快进
if(args != null) {
int msec = args.getInt(0);//获取第一个位置的数据
root.seekTo(msec);
}
break;
default:
break;
}
}

在receiveCommand的case COMMAND_SEEK_TO_ID分支,可以看到,args是个Array,通过index获取到对应的数据,如获取第一个int类型的数据,使用args.getInt(0)。

VideoView.js 增加seekTo函数

seekTo(millSecond){
UIManager.dispatchViewManagerCommand(
findNodeHandle(this.refs[RCT_VIDEO_REF]),
UIManager.VideoView.Commands.seekTo,
[millSecond]//数据形如:["第一个参数","第二个参数",3]
);
}

dispatchViewManagerCommand的第三个参数,接收一组数据(array),可以是不同数据类型,native层通过index获取数据。

VideoPlayScene.js

_onPressSeekTo(){
var millSecond = this.state.time + 1000;
this.video.seekTo(millSecond);
}
//省略其它代码
<TouchableOpacity style={{marginLeft:10}} onPress={this._onPressSeekTo.bind(this)}>
<Text>快进</Text>
</TouchableOpacity>

这样就完成了原生UI组件的开发了,完整的代码太长了,就不贴出来了,需要的话,可以查看我的github

总结

本节讲述了React Native android端的自定义UI组件开发流程,包括设置UI属性、native层向js层发送事件、js层向native层发送命令,完整的讲述了react native与原生之间的通信过程。到此,这个小项目已经阶段性完成了,下一节,我们将讲述android端的打包成安装包apk的流程。