React Native 原生视图封装全解析:视频播放器示例

以视频播放器为例,封装一个可供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);
  }
}
复制代码

视图中需要暴露给视图管理器相关的方法,在更新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<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 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 List createViewManagers(ReactApplicationContext reactContext) {
    return Arrays.asList(
        new PLVideoViewManager()
    );
  }
}
复制代码

包导出

public class MainApplication extends Application implements ReactApplication {
  @Override
    protected List getPackages() {
      return Arrays.asList(
        new MyReactPackage()
      );
}
复制代码

ios

依赖安装

官方githubPLPlayerKit,查看其集成说明,使用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()

//导出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('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就能封装导出绝大部分的原生组件。

你可能感兴趣的:(React Native 原生视图封装全解析:视频播放器示例)