上一節,講了關於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 命令。
可以看到視頻正常播放了,但好像只是僅僅能使用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中查看)
其他的事件的定義流程都一樣,比如獲取當前進度信息、緩存進度、錯誤回調等。目前爲止,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>
);
}
最後運行程序,效果如下
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的流程。