【稀飯】react native 實戰系列教程之自定義原生UI元件
上一節,講了關於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={{marginLef