React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发

时间:2023-12-24 23:26:01

React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发

  2016/09/23 |  React Native技术文章 |  Sky丶清|  4 条评论 |  11232 views

尊重版权,未经授权不得转载

本文来自:画虎烂的专栏(http://blog.csdn.net/it_talk/article/details/52638456)

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

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

刚创建的React Native交流九群:536404410,欢迎各位大牛,React Native技术爱好者加入交流!同时博客右侧欢迎微信扫描关注订阅号,移动技术干货,精彩文章技术推送!

(二)实现Java端组件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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的属性设置:

1
2
3
4
5
6
7
@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属性值

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@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端可能变这样:

1
2
3
4
5
6
7
8
9
10
<VideoView
    source={
        {
            headers:{
                'refer':'myRefer'
            }
        }
    }
/>

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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代码

1
2
3
4
5
6
7
8
@Override
protected List<ReactPackage> getPackages() {
    return Arrays.<ReactPackage>asList(
            new MainReactPackage(),
            new OrientationPackage(),
            new VideoViewPackage()
    );
}

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

(三)实现JS端的组件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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组件和VideoView视频播放器开发

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

(四)native层向js发送消息事件

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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对象

1
2
3
4
5
6
7
8
9
10
11
12
@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对象

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@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改为如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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以下两句是这样

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

修改之后变这样

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

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

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

ok,最后在VideoPlayScene.js调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
_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方法,然后自定义事件名称。

1
2
3
4
5
@Override
public Map getExportedCustomDirectEventTypeConstants() {
    return MapBuilder.of(
            "onCompletion", MapBuilder.of("registrationName", "onCompletion"));
}

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

1
2
3
4
5
@Override
public Map getExportedCustomDirectEventTypeConstants() {
    return MapBuilder.of(
            "topChange", MapBuilder.of("registrationName", "onChange"));
}

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

1
2
3
4
5
6
7
8
9
10
@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属性。

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<VideoView
    style={{height: 250, width: 380}}
    source={
    {
        headers: {
            'refer': 'myRefer'
        }
    }
    }
    onPrepared={this._onPrepared}
    onCompletion={()=>{
        console.log("JS onCompletion");
    }}
/>

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

React Native实战系列教程之自定义原生UI组件和VideoView视频播放器开发

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
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完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<VideoView
    style={{height: 250, width: 380}}
    source={
    {
        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方法。

1
2
3
4
5
6
7
8
9
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方法,处理相应的命令。

1
2
3
4
5
6
7
8
9
10
@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,代码添加如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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,给视频播放添加暂停功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 添加开始播放的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 添加开始播放的代码

1
2
3
4
5
6
7
start(){
    UIManager.dispatchViewManagerCommand(
        findNodeHandle(this.refs[RCT_VIDEO_REF]),
        UIManager.VideoView.Commands.start,
        null
    );
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
_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组件和VideoView视频播放器开发

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

VideoViewManager.java增加seekTo命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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函数

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

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

VideoPlayScene.js

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

这样就完成了原生UI组件的开发了,完整的代码太长了,就不贴出来了,需要的话,项目地址:https://github.com/helengray/XiFan

(七)最后总结

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

刚创建的React Native交流九群:536404410,欢迎各位大牛,React Native技术爱好者加入交流!同时博客右侧欢迎微信扫描关注订阅号,移动技术干货,精彩文章技术推送!

关注我的订阅号(codedev123),每天推送分享移动开发技术(Android/iOS),React Native技术文章,项目管理,程序猿日常点滴以及精品技术资讯文章(欢迎关注,精彩第一时间推送)。

引用原文:http://www.lcode.org/react-native%E5%AE%9E%E6%88%98%E7%B3%BB%E5%88%97%E6%95%99%E7%A8%8B%E4%B9%8B%E8%87%AA%E5%AE%9A%E4%B9%89%E5%8E%9F%E7%94%9Fui%E7%BB%84%E4%BB%B6%E5%92%8Cvideoview%E8%A7%86%E9%A2%91%E6%92%AD%E6%94%BE/

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,文章可以转载,无需版权。希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家一起探讨,代码如有问题,欢迎各位大神指正!