【稀飯】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中查看)

log輸出

其他的事件的定義流程都一樣,比如獲取當前進度信息、緩存進度、錯誤回調等。目前爲止,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的流程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章