将 onPictureInPictureModeChanged 结果传递给 React Native 模块

Pass onPictureInPictureModeChanged result into a react native module

我正在尝试使用 react-native 做一些画中画模式。我写了一个反应模块

我需要生成与此类似的东西,但在 React Native 模块中

public class MainActivity extends AppCompatActivity {
    private PlayerView playerView;
    private Player player;
    private boolean playerShouldPause = true;

...

    @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);

        // Hiding the ActionBar
        if (isInPictureInPictureMode) {
            getSupportActionBar().hide();
        } else {
            getSupportActionBar().show();
        }
        playerView.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
    }
...
}

在 ReactContextBaseJavaModule 中有一些方法可以用同样的方式做到这一点

public class ReactNativeBitmovinPlayerModule extends ReactContextBaseJavaModule {

...
 @Override
    public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);

        // Hiding the ActionBar
        if (isInPictureInPictureMode) {
            getSupportActionBar().hide();
        } else {
            getSupportActionBar().show();
        }
        playerView.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
    }
...
}

是的,有可能实现。实际上,有不止一种方法可以从本机模块侦听画中画模式事件。

快捷方式

最简单的方法是将基础 java 模块设为 LifecycleStateObserver 并在 Activity.isInPictureInPictureMode() 上检查每个 activity 状态更新的变化。

public class ReactNativeCustomModule extends ReactContextBaseJavaModule implements LifecycleEventObserver {
  private boolean isInPiPMode = false;
  private final ReactApplicationContext reactContext;

  public ReactNativeCustomModule(ReactApplicationContext reactContext) {
    super(reactContext);
    this.reactContext = reactContext;
  }

  private void sendEvent(String eventName, @Nullable WritableMap args) {
    reactContext
      .getJSModule(RCTDeviceEventEmitter.class)
      .emit(eventName, args);
  }

  @ReactMethod
  public void registerLifecycleEventObserver() {
    AppCompatActivity activity = (AppCompatActivity) reactContext.getCurrentActivity();
    if (activity != null) {
      activity.getLifecycle().addObserver(this);
    } else {
      Log.d(this.getName(), "App activity is null.");
    }
  }

  @Override
  public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
      AppCompatActivity activity = (AppCompatActivity) source;
      boolean isInPiPMode = activity.isInPictureInPictureMode();
      // Check for changes on pip mode.
      if (this.isInPiPMode != isInPiPMode) {
        this.isInPiPMode = isInPiPMode;
        Log.d(this.getName(), "Activity pip mode has changed to " + isInPiPMode);
        // Dispatch onPictureInPicutreModeChangedEvent to js.
        WritableMap args = Arguments.createMap();
        args.putBoolean("isInPiPMode", isInPiPMode)
        sendEvent("onPictureInPictureModeChanged", args);
      }
    }
  }
  // ...
}

请注意,不可能在模块的构造函数中注册生命周期观察器,因为 activity 仍然是 null。需要在java脚本端注册。

因此,在您的组件初始化时调用 registerLifecycleEventObserver,以便它可以开始接收 activity 状态更新。

import React, { useEffect } from 'react';
import { NativeEventEmitter, NativeModules } from 'react-native';

const ReactNativeCustomModule = NativeModules.ReactNativeCustomModule;
const eventEmitter = new NativeEventEmitter(ReactNativeCustomModule);

// JS wrapper.
export const CustomComponent = () => {
  useEffect(() => {
    // Register module to receive activity's state updates.
    ReactNativeCustomModule.registerLifecycleEventObserver();
    const listener = eventEmitter.addListener('onPictureInPictureModeChanged', (args) => {
      console.log('isInPiPMode:', args.isInPiPMode);
    });
    return () => listener.remove();
  }, []);

  return (
    // jsx
  );
};

顺便说一下,我在 react-native-bitmovin-player 上打开了一个拉取请求来实现这个功能。请检查一下。

辛苦了

还有另一种监听 PiP 变化的方法,但它更复杂并且需要更深入地了解 android 和 RN 平台。但是,有了它,您可以获得在 onPictureInPictureModeChanged 方法上访问 newConfig 的优势(如果需要),而不是监听任何 activity 的生命周期事件。

首先将您的自定义本机视图(无论它是什么)嵌入到 Fragment 中,然后重写片段的 onPictureInPictureModeChanged 方法,最后在那里调度一个 RN 事件。以下是逐步完成的方法:

  1. 为您的自定义视图创建片段:
// Make sure to use android.app's version of Fragment if you need
// to access the `newConfig` argument.
import android.app.Fragment;

// If not, use androidx.fragment.app's version.
// This is the preferable way nowadays, but doesn't include `newConfig`.
// import androidx.fragment.app.Fragment;

// For the sake of example, lets use android.app's version here.
public class CustomViewFragment extends Fragment {
  interface OnPictureInPictureModeChanged {
    void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig);
  }

  private CustomView customView;
  private OnPictureInPictureModeChanged listener;

  @Nullable
  @Override
  public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
    super.onCreateView(inflater, container, savedInstanceState);
    customView = new CustomView();
    // Do all UI setups needed for customView here.
    return customView;
  }

  // Android calls this method on the fragment everytime its activity counterpart is also called.
  @Override
  public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
    if (listener != null) {
      this.listener.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig);
    }
  }

  public void setOnPictureInPictureModeChanged(OnPictureInPictureModeChanged listener) {
    this.listener = listener;
  }

  // OnViewCreated, onPause, onResume, onDestroy...
}
  1. 创建一个RN ViewGroupManager来保存片段并导出 java脚本的功能和道具:
public class CustomViewManager extends ViewGroupManager<FrameLayout> implements CustomViewFragment.OnPictureInPictureModeChanged {
  public static final String REACT_CLASS = "CustomViewManager";
  public final int COMMAND_CREATE = 1;

  ReactApplicationContext reactContext;

  public CustomViewManager(ReactApplicationContext reactContext) {
    this.reactContext = reactContext;
  }

  // Expose `onPictureInPictureModeChanged` prop to javascript.  
  public Map getExportedCustomBubblingEventTypeConstants() {
    return MapBuilder.builder().put(
      "onPictureInPictureModeChanged",
      MapBuilder.of(
        "phasedRegistrationNames",
         MapBuilder.of("bubbled", "onPictureInPictureModeChanged")
      )
    ).build();
  }

  @Override
  public void onPictureInPictureModeChanged(boolean isInPiPMode, Configuration newConfig) {
    Log.d(this.getName(), "PiP mode changed to " + isInPiPMode + " with config " + newConfig.toString());

    // Dispatch onPictureInPictureModeChanged to js.
    final WritableMap args = Arguments.createMap();
    args.putBoolean("isInPiPMode", isInPiPMode);
    args.putMap("newConfig", asMap(newConfig));
    reactContext
      .getJSModule(RCTEventEmitter.class)
      .receiveEvent(getId(), "onPictureInPictureModeChanged", args);
  }

  // Get the JS representation of a Configuration object.
  private ReadableMap asMap(Configuration config) {
    final WritableMap map = Arguments.createMap();
    map.putBoolean("isNightModeActive", newConfig.isNightModeActive());
    map.putBoolean("isScreenHdr", newConfig.isScreenHdr());
    map.putBoolean("isScreenRound", newConfig.isScreenRound());
    // ...
    return map;
  }

  @Override
  public String getName() {
    return REACT_CLASS;
  }

  @Override
  public FrameLayout createViewInstance(ThemedReactContext reactContext) {
    return new FrameLayout(reactContext);
  }

  // Map the "create" command to an integer
  @Nullable
  @Override
  public Map<String, Integer> getCommandsMap() {
    return MapBuilder.of("create", COMMAND_CREATE);
  }

  // Handle "create" command (called from JS) and fragment initialization
  @Override
  public void receiveCommand(@NonNull FrameLayout root, String commandId, @Nullable ReadableArray args) {
    super.receiveCommand(root, commandId, args);
    int reactNativeViewId = args.getInt(0);
    int commandIdInt = Integer.parseInt(commandId);
    switch (commandIdInt) {
      case COMMAND_CREATE:
        createFragment(root, reactNativeViewId);
        break;
      default: {}
    }
  }

  // Replace RN's underlying native view with your own
  public void createFragment(FrameLayout root, int reactNativeViewId) {
    ViewGroup parentView = (ViewGroup) root.findViewById(reactNativeViewId).getParent();
    // You'll very likely need to manually layout your parent view as well to make sure
    // it stays updated with the props from RN.
    //
    // I recommend taking a look at android's `view.Choreographer` and  RN's docs on how to do it.
    // And, as I said, this requires some knowledge of native Android UI development.
    setupLayout(parentView);

    final CustomViewFragment fragment = new CustomViewFragment();
    fragment.setOnPictureInPictureModeChanged(this);

    FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity();
    // Make sure to use activity.getSupportFragmentManager() if you're using
    // androidx's Fragment.
    activity.getFragmentManager()
      .beginTransaction()
      .replace(reactNativeViewId, fragment, String.valueOf(reactNativeViewId))
      .commit();
  }
}
  1. 在一个包中注册CustomViewManager
public class CustomPackage implements ReactPackage {
  @Override
  public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
    return Arrays.<ViewManager>asList(
      new CustomViewManager(reactContext)
    );
  }
}
  1. 实施java脚本端:
import React, { useEffect, useRef } from 'react';
import { UIManager, findNodeHandle, requireNativeComponent } from 'react-native';

const CustomViewManager = requireNativeComponent('CustomViewManager');

const createFragment = (viewId) =>
  UIManager.dispatchViewManagerCommand(
    viewId,
    UIManager.CustomViewManager.Commands.create.toString(), // we are calling the 'create' command
    [viewId]
  );

export const CustomView = ({ style }) => {
  const ref = useRef(null);

  useEffect(() => {
    const viewId = findNodeHandle(ref.current);
    createFragment(viewId!);
  }, []);

  const onPictureInPictureModeChanged = (event) => {
    console.log('isInPiPMode:', event.nativeEvent.isInPiPMode);
    console.log('newConfig:', event.nativeEvent.newConfig);
  }

  return (
    <CustomViewManager
      style={{
        ...(style || {}),
        height: style && style.height !== undefined ? style.height || '100%',
        width: style && style.width !== undefined ? style.width || '100%'
      }}
      ref={ref}
      onPictureInPictureModeChanged={onPictureInPictureModeChanged}
    />
  );
};

最后一个示例很大程度上基于 RN's documentation。如果你努力的话,我怎么强调阅读它的重要性都不为过。

无论如何,我希望这个小指南能对您有所帮助。

此致。