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

以視頻播放器爲例,封裝一個可供android和ios使用的react native視頻播放組件,展示基本上React Native封裝原生組件會須要用到的所有。以使用方法簡單的支持多平臺使用的七牛播放器第三方庫視頻庫導出到React Native使用。java

android

依賴安裝

官方githubPLDroidPlayer,查看其相關文檔,把jar和so下載複製進項目中。node

實現

自定義視頻播放器view

在android視圖渲染機制中,子視圖改變大小,事件一直冒泡到根視圖被處理,而在react native中根視圖的處理方法是空的,即不作任何處理,因此在view中若是要改變視圖大小,必須手動在requestLayout中從新調整大小。react

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,該方法在視圖中封裝。android

視圖管理器

ViewGroupManager用於容器視圖,其提供addView等方法,SimpleViewManager用於普通視圖,視圖管理器主要導出視圖props,提供js -> native調用,native -> js調用。ios

@ReactProp註解導出prop,在組件設置或者修改prop時會調用該函數,第一個參數爲當前視圖,第二個參數爲prop的值。git

getName返回組件名,在js層用這個名稱來找到native組件。github

native -> js: prop類型爲函數的需在getExportedCustomDirectEventTypeConstants註冊,在觸發回調時sendEvent。typescript

js -> native: ref的方法在getCommandsMap中註冊,在receiveCommand處理。react-native

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或手動集成。async

實現

視圖

.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就能封裝導出絕大部分的原生組件。

相關文章
相關標籤/搜索