React Native 原生檢視封裝全解析:視訊播放器示例

qiushijie發表於2019-04-02

以視訊播放器為例,封裝一個可供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就能封裝匯出絕大部分的原生元件。

相關文章