I am trying to do some picture in picture mode using react-native. I wrote a react module
I need to generate something similar to this but inside the react native module
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);
}
...
}
There is some way to do it the same way but inside 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);
}
...
}
Yes, it's possible to achieve it. And there's actually more than one way to listen for PiP mode events from native modules.
The easiest way is by making your base java module a LifecycleStateObserver
and checking changes on Activity.isInPictureInPictureMode()
for every activity state update.
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);
}
}
}
// ...
}
Note it's impossible to register the lifecycle observer inside the module's constructor because the activity's still null
. It needs to be registered on the javascript side.
Therefore, call registerLifecycleEventObserver
at your component's initialization so it can start receiving activity state updates.
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
);
};
By the way, I opened a pull request on react-native-bitmovin-player
implementing this very feature. Please, check it out 😉.
There's yet another way to listen for PiP changes, but it's more complex and requires a deeper knowledge of both android and RN platforms. However, with it, you get the advantage of accessing newConfig
on the onPictureInPictureModeChanged
method (if required) and not listening to any of the activity's lifecycle events.
Start by embedding your custom native view (whatever it is) into a Fragment
, then override the fragment's onPictureInPictureModeChanged
method and finally dispatch an RN event there. Here's how it can be done step by step:
// 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...
}
ViewGroupManager
to hold the fragment and export
functions and props to javascript: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();
}
}
CustomViewManager
in a package:public class CustomPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new CustomViewManager(reactContext)
);
}
}
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}
/>
);
};
This last example was heavily based on RN's documentation. And I can't stress enough how important it is to read it if you go the hard way.
Anyways, I hope this little guide may be of help.
Best regards.