以视频播放器为例,封装一个可供android和ios使用的react native视频播放组件,展现基本上React Native封装原生组件会需要用到的全部。以使用方法简单的支持多平台使用的七牛播放器第三方库视频库导出到React Native使用。
android
依赖安装
官方github,查看其相关文档,把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); } }); }@Overridepublic 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); }}复制代码
视图中需要暴露给视图管理器相关的方法,在更新prop时调用,如需要发送事件到js端,则需要使用RCTEventEmitter,该方法在视图中封装。
视图管理器
ViewGroupManager用于容器视图,其提供addView等方法,SimpleViewManager用于普通视图,视图管理器主要导出视图props,提供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{ 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 getCommandsMap() { Map 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 getExportedCustomDirectEventTypeConstants() { MapBuilder.Builder 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 ListcreateViewManagers(ReactApplicationContext reactContext) { return Arrays. asList( new PLVideoViewManager() ); }}复制代码
包导出
public class MainApplication extends Application implements ReactApplication { @Override protected ListgetPackages() { return Arrays.asList( new MyReactPackage() );}复制代码
ios
依赖安装
官方github,查看其集成说明,使用pod或手动集成。
实现
视图
.h
#import#import #import #import @class RCTEventDispatcher;@interface RTCPLVideo : UIView - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher NS_DESIGNATED_INITIALIZER;// prop函数@property (nonatomic, copy) RCTBubblingEventBlock onPlayEnd;- (void) stop;@end复制代码
.m
#import "RTCPLVideo.h"@interface RTCPLVideo()@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@interface RTCPLVideoManager : RCTViewManager@end复制代码
.m
#import "RTCPLVideoManager.h"#import "RTCPLVideo.h"@implementation RTCPLVideoManager// 导出模块RCT_EXPORT_MODULE()//导出propRCT_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('RTCPLVideo');export default class PLVideo extends React.Component { private plVideo?: any; private callNative(name: string, args: Array = []) { 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 ( this.plVideo = plVideo!} {...this.props}/> ); }}复制代码
总结
在React Native原生视图封装中,知道prop导出、js -> native、native -> js就能封装导出绝大部分的原生组件。