以視訊播放器為例,封裝一個可供android和ios使用的react native視訊播放元件,展現基本上React Native封裝原生元件會需要用到的全部。以使用方法簡單的支援多平臺使用的七牛播放器第三方庫視訊庫匯出到React Native使用。
android
依賴安裝
官方githubPLDroidPlayer,檢視其相關文件,把jar和so下載複製進專案中。
實現
自定義視訊播放器view
在android檢視渲染機制中,子檢視改變大小,事件一直冒泡到根檢視被處理,而在react native中根檢視的處理方法以及是空的,即不做任何處理,所以在view中如果要改變檢視大小,必須手動在requestLayout中重新調整大小。
import android.content.Context;
import android.util.Log;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.pili.pldroid.player.PLOnCompletionListener;
import com.pili.pldroid.player.PLOnPreparedListener;
import com.pili.pldroid.player.widget.PLVideoView;
import javax.annotation.Nullable;
public class MyPLVideoView extends PLVideoView {
private final static String TAG = "MyPLVideoView";
public MyPLVideoView(Context context) {
super(context);
setOnPreparedListener(new PLOnPreparedListener() {
@Override
public void onPrepared(int i) {
reLayout();
}
});
setOnCompletionListener(new PLOnCompletionListener() {
@Override
public void onCompletion() {
seekTo(0);
MyPLVideoView.this.start();
sendEvent("onPlayEnd", null);
}
});
}
@Override
public void requestLayout() {
super.requestLayout();
// 避免在切換解析度後無法正常
reLayout();
}
public void reLayout() {
if (getWidth() > 0 && getHeight() > 0) {
int w = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.EXACTLY);
int h = MeasureSpec.makeMeasureSpec(getHeight(), MeasureSpec.EXACTLY);
measure(w, h);
layout(getPaddingLeft() + getLeft(), getPaddingTop() + getTop(), getWidth() + getPaddingLeft() + getLeft(), getHeight() + getPaddingTop() + getTop());
}
}
// 事件傳送
public void sendEvent(String name, @Nullable WritableMap event) {
ReactContext reactContext = (ReactContext) getContext();
reactContext.getJSModule(RCTEventEmitter.class)
.receiveEvent(getId(), name, event);
}
}
複製程式碼
檢視管理器
ViewGroupManager用於容器檢視,SimpleViewManager用於普通檢視,檢視管理器主要匯出檢視,提供js -> native呼叫,native -> js呼叫。
@ReactProp註解匯出prop,在元件設定或者修改prop時會呼叫該函式,第一個引數為當前檢視,第二個引數為prop的值。
getName返回元件名,在js層用這個名稱來找到native元件。
native -> js: prop型別為函式的需在getExportedCustomDirectEventTypeConstants註冊,在觸發回撥時sendEvent。
js -> native: ref的方法在getCommandsMap中註冊,在receiveCommand處理。
import android.net.Uri;
import android.util.Log;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
public class PLVideoViewManager extends SimpleViewManager<MyPLVideoView> {
private static final String TAG = "PLVideoViewManager";
@Override
public String getName() {
return "RTCPLVideo";
}
@Override
protected MyPLVideoView createViewInstance(ThemedReactContext reactContext) {
return new MyPLVideoView(reactContext);
}
// 視訊uri prop
@ReactProp(name = "uri")
public void uri(MyPLVideoView root, String uri) {
root.setVideoURI(Uri.parse(uri));
}
// 視訊暫停 prop
@ReactProp(name = "paused")
public void paused(MyPLVideoView root, Boolean paused) {
if (paused) {
root.pause();
} else {
root.start();
}
}
@Nullable
@Override
public Map<String, Integer> getCommandsMap() {
Map<String, Integer> commandsMap = new HashMap<>();
// ref方法註冊
commandsMap.put("stop", 1);
return commandsMap;
}
@Override
public void receiveCommand(MyPLVideoView root, int commandId, @Nullable ReadableArray args) {
switch (commandId) {
case 1:
// 停止播放,釋放播放器
root.stopPlayback();
break;
}
}
@Nullable
@Override
public Map<String, Object> getExportedCustomDirectEventTypeConstants() {
MapBuilder.Builder<String, Object> builder = MapBuilder.builder();
// prop函式註冊
String[] events = {
"onPlayEnd"
};
for (String event: events) {
builder.put(event, MapBuilder.of("registrationName", event));
}
return builder.build();
}
}
複製程式碼
檢視匯出
public class MyReactPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new PLVideoViewManager()
);
}
}
複製程式碼
包匯出
public class MainApplication extends Application implements ReactApplication {
@Override
protected List<ReactPackage> getPackages() {
return Arrays.asList(
new MyReactPackage()
);
}
複製程式碼
ios
依賴安裝
官方githubPLPlayerKit,檢視其整合說明,使用pod或手動整合。
實現
檢視
.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <React/RCTView.h>
#import <PLPlayerKit/PLPlayerKit.h>
@class RCTEventDispatcher;
@interface RTCPLVideo : UIView<PLPlayerDelegate>
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;
// prop函式
@property (nonatomic, copy) RCTBubblingEventBlock onPlayEnd;
- (void) stop;
@end
複製程式碼
.m
#import "RTCPLVideo.h"
@interface RTCPLVideo()<PLPlayerDelegate>
@property (nonatomic, strong) PLPlayer *player;
@end
@implementation RTCPLVideo
{
RCTEventDispatcher *_eventDispatcher;
}
- (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher
{
if ((self = [super init])) {
}
return self;
}
- (void)player:(nonnull PLPlayer *)player statusDidChange:(PLPlayerStatus)state {
if (state == PLPlayerStatusCompleted) {
CMTime start = CMTimeMakeWithSeconds(0, 600);
[self.player seekTo: start];
if (self.onPlayEnd) {
// 呼叫prop函式
self.onPlayEnd(@{});
}
}
}
- (void) setUri:(NSString *) uri
{
NSURL *url = [NSURL URLWithString:uri];
if (self.player == nil) {
PLPlayerOption *option = [PLPlayerOption defaultOption];
[option setOptionValue:@15 forKey:PLPlayerOptionKeyTimeoutIntervalForMediaPackets];
self.player = [PLPlayer playerWithURL:url option:option];
self.player.delegate = self;
[self addSubview:self.player.playerView];
[self.player play];
} else {
[self.player playWithURL:url sameSource:NO];
}
}
- (void) setPaused: (BOOL) paused
{
if (self.player) {
if (paused) {
[self.player pause];
} else {
[self.player play];
}
}
}
- (void) cache:(NSString *)url
{
if (self.player) {
NSURL *uri = [NSURL URLWithString:url];
[self.player openPlayerWithURL:uri];
}
}
- (void) stop
{
if (self.player) {
[self.player stop];
}
}
@end
複製程式碼
檢視管理
.h
#import <React/RCTUIManager.h>
@interface RTCPLVideoManager : RCTViewManager
@end
複製程式碼
.m
#import "RTCPLVideoManager.h"
#import "RTCPLVideo.h"
@implementation RTCPLVideoManager
// 匯出模組
RCT_EXPORT_MODULE()
//匯出prop
RCT_EXPORT_VIEW_PROPERTY(onPlayEnd, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(uri, NSString)
RCT_EXPORT_VIEW_PROPERTY(paused, BOOL)
- (UIView *)view
{
return [[RTCPLVideo alloc] initWithEventDispatcher:self.bridge.eventDispatcher];
}
typedef void(^js_call_black)(RTCPLVideo *view);
// js -> native呼叫不在主執行緒,執行view相關方法需要切到主執行緒
- (void) js_call: (NSNumber *) node black: (js_call_black) black
{
dispatch_async(dispatch_get_main_queue(), ^(){
UIView* temp = [self.bridge.uiManager viewForReactTag:node];
if ([[temp class] isEqual:[RTCPLVideo class]])
{
RTCPLVideo* view = (RTCPLVideo*) temp;
black(view);
}
});
}
RCT_EXPORT_METHOD(stop: (nonnull NSNumber *) node)
{
[self js_call:node black:^(RTCPLVideo *view) {
// 執行相應方法
}];
}
@end
複製程式碼
typescript
import React from 'react';
import {findNodeHandle, requireNativeComponent, UIManager, ViewStyle} from 'react-native';
interface IProps {
uri: string;
paused: boolean;
style?: ViewStyle;
onPlayEnd: () => void;
}
const RTCPLVideo = requireNativeComponent<IProps>('RTCPLVideo');
export default class PLVideo extends React.Component<IProps> {
private plVideo?: any;
private callNative(name: string, args: Array<any> = []) {
const commandId = (UIManager as any).RTCPLVideo.Commands[name];
(UIManager as any).dispatchViewManagerCommand(findNodeHandle(this.plVideo), commandId, args);
}
private stop() {
this.plVideo && this.callNative('stop');
}
componentWillUnmount() {
this.stop();
}
render() {
return (
<RTCPLVideo ref={plVideo => this.plVideo = plVideo!} {...this.props}/>
);
}
}
複製程式碼
總結
在React Native原生檢視封裝中,知道prop匯出、js -> native、native -> js就能封裝匯出絕大部分的原生元件。